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:
Gabriel Grubba 2024-03-18 17:00:28 +01:00 committed by GitHub
parent 5023ff480e
commit 8ae462c724
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 357 additions and 4 deletions

View File

@ -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") } },
});
}
}

View File

@ -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"));

View File

@ -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}}

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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") }

View File

@ -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

View File

@ -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

View File

@ -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