FEATURE: add language picker for theme translations in admin UI (#26150)
Allows editing translations of a theme in locales other than the current localy.
This commit is contained in:
parent
5023ff480e
commit
8ae462c724
|
@ -17,7 +17,7 @@ export default class ThemeTranslation extends SiteSettingComponent {
|
|||
|
||||
return ajax(this.updateUrl, {
|
||||
type: "PUT",
|
||||
data: { theme: { translations } },
|
||||
data: { theme: { translations, locale: this.get("model.locale") } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
readOnly,
|
||||
} from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
|
@ -24,12 +25,15 @@ const THEME_UPLOAD_VAR = 2;
|
|||
export default class AdminCustomizeThemesShowController extends Controller {
|
||||
@service dialog;
|
||||
@service router;
|
||||
@service siteSettings;
|
||||
@service modal;
|
||||
|
||||
editRouteName = "adminCustomizeThemes.edit";
|
||||
|
||||
@url("model.id", "/admin/customize/themes/%@/export") downloadUrl;
|
||||
@url("model.id", "/admin/themes/%@/preview") previewUrl;
|
||||
@url("model.id", "model.locale", "/admin/themes/%@/translations/%@")
|
||||
getTranslationsUrl;
|
||||
@empty("selectedChildThemeId") addButtonDisabled;
|
||||
@mapBy("model.parentThemes", "name") parentThemesNames;
|
||||
@filterBy("allThemes", "component", false) availableParentThemes;
|
||||
|
@ -293,6 +297,22 @@ export default class AdminCustomizeThemesShowController extends Controller {
|
|||
model.saveChanges("theme_fields").catch((e) => popupAjaxError(e));
|
||||
}
|
||||
|
||||
get availableLocales() {
|
||||
return JSON.parse(this.siteSettings.available_locales);
|
||||
}
|
||||
|
||||
get locale() {
|
||||
return this.get("model.locale") || this.siteSettings.default_locale;
|
||||
}
|
||||
|
||||
@action
|
||||
updateLocale(value) {
|
||||
this.set("model.locale", value);
|
||||
ajax(this.getTranslationsUrl).then(({ translations }) =>
|
||||
this.set("model.translations", translations)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
cancelChangeScheme() {
|
||||
this.set("colorSchemeId", this.get("model.color_scheme_id"));
|
||||
|
|
|
@ -460,12 +460,23 @@
|
|||
|
||||
{{#if this.hasTranslations}}
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n
|
||||
"admin.customize.theme.theme_translations"
|
||||
}}</div>
|
||||
<div class="translation-selector-container">
|
||||
<span class="mini-title">
|
||||
{{i18n "admin.customize.theme.theme_translations"}}
|
||||
</span>
|
||||
<ComboBox
|
||||
@valueProperty="value"
|
||||
@content={{this.availableLocales}}
|
||||
@value={{this.locale}}
|
||||
@onChange={{this.updateLocale}}
|
||||
@options={{hash filterable=true}}
|
||||
class="translation-selector"
|
||||
/>
|
||||
</div>
|
||||
<section
|
||||
class="form-horizontal theme settings translations control-unit"
|
||||
>
|
||||
|
||||
{{#each this.translations as |translation|}}
|
||||
<ThemeTranslation
|
||||
@translation={{translation}}
|
||||
|
|
|
@ -168,6 +168,25 @@
|
|||
.control-unit {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 2em;
|
||||
|
||||
.translation-selector-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 79.7%;
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 700px) and (max-width: 768px) {
|
||||
width: 73%;
|
||||
}
|
||||
|
||||
.translation-selector {
|
||||
width: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.control {
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -311,6 +311,25 @@ class Admin::ThemesController < Admin::AdminController
|
|||
exporter.cleanup!
|
||||
end
|
||||
|
||||
def get_translations
|
||||
params.require(:locale)
|
||||
unless I18n.available_locales.include?(params[:locale].to_sym)
|
||||
raise Discourse::InvalidParameters.new(:locale)
|
||||
end
|
||||
|
||||
I18n.locale = params[:locale]
|
||||
|
||||
@theme = Theme.find_by(id: params[:id])
|
||||
raise Discourse::InvalidParameters.new(:id) unless @theme
|
||||
|
||||
translations =
|
||||
@theme.translations.map do |translation|
|
||||
{ key: translation.key, value: translation.value, default: translation.default }
|
||||
end
|
||||
|
||||
render json: { translations: translations }, status: :ok
|
||||
end
|
||||
|
||||
def update_single_setting
|
||||
params.require("name")
|
||||
@theme = Theme.find_by(id: params[:id])
|
||||
|
@ -369,6 +388,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||
:component,
|
||||
:enabled,
|
||||
:auto_update,
|
||||
:locale,
|
||||
settings: {
|
||||
},
|
||||
translations: {
|
||||
|
@ -408,6 +428,14 @@ class Admin::ThemesController < Admin::AdminController
|
|||
def update_translations
|
||||
return unless target_translations = theme_params[:translations]
|
||||
|
||||
locale = theme_params[:locale].presence
|
||||
if locale
|
||||
unless I18n.available_locales.include?(locale.to_sym)
|
||||
raise Discourse::InvalidParameters.new(:locale)
|
||||
end
|
||||
I18n.locale = locale
|
||||
end
|
||||
|
||||
target_translations.each_pair do |translation_key, new_value|
|
||||
@theme.update_translation(translation_key, new_value)
|
||||
end
|
||||
|
|
|
@ -233,6 +233,7 @@ Discourse::Application.routes.draw do
|
|||
constraints: AdminConstraint.new do
|
||||
member do
|
||||
get "preview" => "themes#preview"
|
||||
get "translations/:locale" => "themes#get_translations"
|
||||
put "setting" => "themes#update_single_setting"
|
||||
end
|
||||
collection do
|
||||
|
|
|
@ -1129,6 +1129,172 @@ RSpec.describe Admin::ThemesController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#update_translations" do
|
||||
fab!(:theme)
|
||||
|
||||
before do
|
||||
theme.set_field(
|
||||
target: :translations,
|
||||
name: :en,
|
||||
value: { en: { group: { hello: "Hello there!" } } }.deep_stringify_keys.to_yaml,
|
||||
)
|
||||
theme.set_field(
|
||||
target: :translations,
|
||||
name: :fr,
|
||||
value: { fr: { group: { hello: "Bonjour Mes Amis!" } } }.deep_stringify_keys.to_yaml,
|
||||
)
|
||||
theme.save!
|
||||
end
|
||||
|
||||
context "when logged in as an admin" do
|
||||
before { sign_in(admin) }
|
||||
|
||||
it "should update a theme translation" do
|
||||
put "/admin/themes/#{theme.id}.json",
|
||||
params: {
|
||||
theme: {
|
||||
translations: {
|
||||
"group.hello" => "Hello there! updated",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
theme.reload.translations.map { |t| expect(t.value).to eq("Hello there! updated") }
|
||||
end
|
||||
|
||||
it "should update a theme translation with locale" do
|
||||
put "/admin/themes/#{theme.id}.json",
|
||||
params: {
|
||||
theme: {
|
||||
translations: {
|
||||
"group.hello" => "Hello there! updated",
|
||||
},
|
||||
locale: "en",
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
theme.reload.translations.map { |t| expect(t.value).to eq("Hello there! updated") }
|
||||
end
|
||||
|
||||
it "should fail update a theme translation when locale is wrong" do
|
||||
put "/admin/themes/#{theme.id}.json",
|
||||
params: {
|
||||
theme: {
|
||||
translations: {
|
||||
"group.hello" => "Hello there! updated",
|
||||
},
|
||||
locale: "foo",
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(400)
|
||||
expect(response.parsed_body["errors"]).to include(
|
||||
I18n.t("invalid_params", message: :locale),
|
||||
)
|
||||
end
|
||||
|
||||
it "should update other locale and do not change current one" do
|
||||
put "/admin/themes/#{theme.id}.json",
|
||||
params: {
|
||||
theme: {
|
||||
translations: {
|
||||
"group.hello" => "Bonjour Mes Amis! updated",
|
||||
},
|
||||
locale: "fr",
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
theme.reload.translations.map { |t| expect(t.value).to eq("Hello there!") }
|
||||
|
||||
get "/admin/themes/#{theme.id}/translations/fr.json"
|
||||
translations = response.parsed_body["translations"]
|
||||
expect(translations.first["value"]).to eq("Bonjour Mes Amis! updated")
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "theme update not allowed" do
|
||||
it "prevents updates with a 404 response" do
|
||||
put "/admin/themes/#{theme.id}.json",
|
||||
params: {
|
||||
theme: {
|
||||
translations: {
|
||||
"group.hello" => "Bonjour Mes Amis! updated",
|
||||
},
|
||||
locale: "fr",
|
||||
},
|
||||
}
|
||||
expect(response.status).to eq(404)
|
||||
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when logged in as a moderator" do
|
||||
before { sign_in(moderator) }
|
||||
|
||||
include_examples "theme update not allowed"
|
||||
end
|
||||
|
||||
context "when logged in as a non-staff user" do
|
||||
before { sign_in(user) }
|
||||
|
||||
include_examples "theme update not allowed"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_translations" do
|
||||
fab!(:theme)
|
||||
|
||||
before do
|
||||
theme.set_field(
|
||||
target: :translations,
|
||||
name: :en,
|
||||
value: { en: { group: { hello: "Hello there!" } } }.deep_stringify_keys.to_yaml,
|
||||
)
|
||||
theme.save!
|
||||
end
|
||||
|
||||
context "when logged in as an admin" do
|
||||
before { sign_in(admin) }
|
||||
|
||||
it "get translations from theme" do
|
||||
get "/admin/themes/#{theme.id}/translations/en.json"
|
||||
translations = response.parsed_body["translations"]
|
||||
expect(translations.first["value"]).to eq("Hello there!")
|
||||
end
|
||||
|
||||
it "fail if get translations from theme with wrong locale" do
|
||||
get "/admin/themes/#{theme.id}/translations/foo.json"
|
||||
expect(response.status).to eq(400)
|
||||
expect(response.parsed_body["errors"]).to include(
|
||||
I18n.t("invalid_params", message: :locale),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "get theme translations not allowed" do
|
||||
it "prevents updates with a 404 response" do
|
||||
get "/admin/themes/#{theme.id}/translations/en.json"
|
||||
expect(response.status).to eq(404)
|
||||
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when logged in as a moderator" do
|
||||
before { sign_in(moderator) }
|
||||
|
||||
include_examples "get theme translations not allowed"
|
||||
end
|
||||
|
||||
context "when logged in as a non-staff user" do
|
||||
before { sign_in(user) }
|
||||
|
||||
include_examples "get theme translations not allowed"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#bulk_destroy" do
|
||||
fab!(:theme) { Fabricate(:theme, name: "Awesome Theme") }
|
||||
fab!(:theme_2) { Fabricate(:theme, name: "Another awesome Theme") }
|
||||
|
|
|
@ -83,4 +83,56 @@ describe "Admin Customize Themes", type: :system do
|
|||
expect(ace_content.text).to eq("console.log('test')")
|
||||
end
|
||||
end
|
||||
|
||||
describe "when editing theme translations" do
|
||||
it "should allow admin to edit and save the theme translations" do
|
||||
theme.set_field(
|
||||
target: :translations,
|
||||
name: "en",
|
||||
value: { en: { group: { hello: "Hello there!" } } }.deep_stringify_keys.to_yaml,
|
||||
)
|
||||
|
||||
theme.save!
|
||||
|
||||
visit("/admin/customize/themes/#{theme.id}")
|
||||
|
||||
theme_translations_settings_editor =
|
||||
PageObjects::Components::AdminThemeTranslationsSettingsEditor.new
|
||||
|
||||
theme_translations_settings_editor.fill_in("Hello World")
|
||||
theme_translations_settings_editor.save
|
||||
|
||||
visit("/admin/customize/themes/#{theme.id}")
|
||||
|
||||
expect(theme_translations_settings_editor.get_input_value).to have_content("Hello World")
|
||||
end
|
||||
|
||||
it "should allow admin to edit and save the theme translations from other languages" do
|
||||
theme.set_field(
|
||||
target: :translations,
|
||||
name: "en",
|
||||
value: { en: { group: { hello: "Hello there!" } } }.deep_stringify_keys.to_yaml,
|
||||
)
|
||||
theme.set_field(
|
||||
target: :translations,
|
||||
name: "fr",
|
||||
value: { fr: { group: { hello: "Bonjour!" } } }.deep_stringify_keys.to_yaml,
|
||||
)
|
||||
theme.save!
|
||||
|
||||
visit("/admin/customize/themes/#{theme.id}")
|
||||
|
||||
theme_translations_settings_editor =
|
||||
PageObjects::Components::AdminThemeTranslationsSettingsEditor.new
|
||||
expect(theme_translations_settings_editor.get_input_value).to have_content("Hello there!")
|
||||
|
||||
theme_translations_picker = PageObjects::Components::SelectKit.new(".translation-selector")
|
||||
theme_translations_picker.select_row_by_value("fr")
|
||||
|
||||
expect(theme_translations_settings_editor.get_input_value).to have_content("Bonjour!")
|
||||
|
||||
theme_translations_settings_editor.fill_in("Hello World in French")
|
||||
theme_translations_settings_editor.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Components
|
||||
class AdminThemeTranslationsSettingsEditor < Base
|
||||
def fill_in(translation)
|
||||
editor.fill_input(translation)
|
||||
self
|
||||
end
|
||||
|
||||
def save
|
||||
find(".btn.no-text.btn-icon.ok").click
|
||||
self
|
||||
end
|
||||
|
||||
def get_input_value
|
||||
editor.get_input
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def editor
|
||||
@editor ||= ThemeTranslationTextArea.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Components
|
||||
class ThemeTranslationTextArea < PageObjects::Components::Base
|
||||
def type_input(content)
|
||||
editor_input.send_keys(content)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_input(content)
|
||||
editor_input.fill_in(with: content)
|
||||
self
|
||||
end
|
||||
|
||||
def clear_input
|
||||
fill_input("")
|
||||
end
|
||||
|
||||
def editor_input
|
||||
find(".theme.translations .row:nth-child(1) textarea")
|
||||
end
|
||||
|
||||
def get_input
|
||||
editor_input.value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue