From 235c673fe8929cb0e11c01e5f638e691ca5d5644 Mon Sep 17 00:00:00 2001 From: Natalie Tay Date: Tue, 29 Jul 2025 11:48:45 +0800 Subject: [PATCH] FEATURE: Localize language names (#33790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds localized language names to settings. The language names are localized in the frontend, not the backend, due to setting initialization complexity. This change affects these areas: - `SiteSetting.available_locales` - this "setting" is a lookup table to get language names. use `languageNameLookup` service to get the name for a locale - it returns an object that looks like the following, then gets re-hydrated with client localized values when initializing the `siteSettingService` in the frontend. ``` [ {"native_name":"اللغة العربية","value":"ar","name":"languages.ar.name"}, ... ] ``` - `SiteSetting.default_locale` - this is a single-value `enum` setting that has always been hardcoded. This caused quite an issue as it is not initialized the same way as other site settings in the yml file. It has always relied on reading directly from a `names.yml` file to load native language names, thus bypassing the need for I18n to be initialized from the backend. A new locale_enum type has been introduced for this setting, and any future settings. - `SiteSetting.content_localization_supported_locales` - this is a `enum_list` setting, - enum_list is introduced, leveraging both `list` and `enum` - theme translations - site texts - Wizard's default_locale choices - it was set up from the backend using `LocaleSiteSetting.value`. This proved problematic, as a Japanese user would be getting the locales in English because the values are initialized using English even without memoization - therefore we're now initializing the choices in the frontend using `available_locales` as defined above - content localization meta data - post language in the composer, localization composer, post history modal, post language tooltip, language switcher /t/151409 --- .../admin/addon/components/site-setting.gjs | 2 + .../components/site-settings/locale-enum.gjs | 34 +++++++ .../components/site-settings/locale-list.gjs | 43 ++++++++ .../admin-customize-themes-show-index.js | 2 +- .../controllers/admin-site-text-index.js | 2 +- .../addon/routes/admin-site-text-edit.js | 2 +- .../app/components/language-switcher.gjs | 15 ++- .../components/modal/history/revisions.gjs | 17 ++-- .../app/components/post-language-selector.gjs | 15 ++- .../components/post-translation-editor.gjs | 16 ++- .../components/post/meta-data/language.gjs | 6 +- .../app/controllers/preferences/interface.js | 2 +- .../discourse/app/controllers/topic.js | 4 +- .../app/services/language-name-lookup.js | 14 +++ .../discourse/app/services/site-settings.js | 17 ++++ .../wizard/components/fields/dropdown.gjs | 24 ++++- .../tests/acceptance/admin-site-text-test.js | 2 +- .../integration/components/post/post-test.gjs | 14 ++- app/models/locale_site_setting.rb | 20 ++-- app/serializers/post_revision_serializer.rb | 5 +- app/serializers/post_serializer.rb | 2 +- config/locales/client.en.yml | 99 +++++++++++++++++++ config/site_settings.yml | 4 +- lib/site_setting_extension.rb | 8 +- lib/site_settings/type_supervisor.rb | 10 +- lib/wizard/builder.rb | 17 ++-- .../components/ai-composer-helper-menu.gjs | 4 +- .../discourse/components/region-input.js | 2 +- .../acceptance/admin-holidays-test.js | 2 +- .../components/region-input-test.gjs | 8 +- spec/models/locale_site_setting_spec.rb | 28 ++---- spec/requests/steps_controller_spec.rb | 3 +- .../post_revision_serializer_spec.rb | 4 +- spec/serializers/post_serializer_spec.rb | 2 +- spec/system/admin_site_setting_locale_spec.rb | 51 ++++++++++ spec/system/anon_language_switcher_spec.rb | 4 +- spec/system/content_localization_spec.rb | 7 +- .../page_objects/pages/admin_site_settings.rb | 10 ++ spec/system/post_translation_spec.rb | 8 +- 39 files changed, 419 insertions(+), 110 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/components/site-settings/locale-enum.gjs create mode 100644 app/assets/javascripts/admin/addon/components/site-settings/locale-list.gjs create mode 100644 app/assets/javascripts/discourse/app/services/language-name-lookup.js create mode 100644 spec/system/admin_site_setting_locale_spec.rb diff --git a/app/assets/javascripts/admin/addon/components/site-setting.gjs b/app/assets/javascripts/admin/addon/components/site-setting.gjs index 60f47b22a98..0b989cacff7 100644 --- a/app/assets/javascripts/admin/addon/components/site-setting.gjs +++ b/app/assets/javascripts/admin/addon/components/site-setting.gjs @@ -43,6 +43,8 @@ const CUSTOM_TYPES = [ "file_size_restriction", "file_types_list", "font_list", + "locale_list", + "locale_enum", ]; export default class SiteSettingComponent extends Component { diff --git a/app/assets/javascripts/admin/addon/components/site-settings/locale-enum.gjs b/app/assets/javascripts/admin/addon/components/site-settings/locale-enum.gjs new file mode 100644 index 00000000000..eeab52edb8f --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/site-settings/locale-enum.gjs @@ -0,0 +1,34 @@ +import Component from "@glimmer/component"; +import { hash } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import ComboBox from "select-kit/components/combo-box"; + +export default class LocaleEnum extends Component { + @service languageNameLookup; + + get content() { + return this.args.setting.validValues.map(({ value }) => ({ + name: this.languageNameLookup.getLanguageName(value), + value, + })); + } + + @action + onChangeLocale(value) { + this.args.changeValueCallback(value); + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/site-settings/locale-list.gjs b/app/assets/javascripts/admin/addon/components/site-settings/locale-list.gjs new file mode 100644 index 00000000000..3972933619d --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/site-settings/locale-list.gjs @@ -0,0 +1,43 @@ +import Component from "@glimmer/component"; +import { hash } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import ListSetting from "select-kit/components/list-setting"; + +export default class LocaleList extends Component { + @service siteSettings; + + tokenSeparator = "|"; + + get choices() { + const allLocales = this.siteSettings.available_locales; + return this.args.setting.validValues.map(({ value, name }) => ({ + name: allLocales.find((locale) => locale.value === value)?.name || name, + value, + })); + } + + get settingValue() { + return this.args.value + .toString() + .split(this.tokenSeparator) + .filter(Boolean); + } + + @action + onChangeListSetting(value) { + this.args.changeValueCallback(value.join(this.tokenSeparator)); + } + + +} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show-index.js b/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show-index.js index 8f9c5a1814a..72961091e1c 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show-index.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show-index.js @@ -285,7 +285,7 @@ export default class AdminCustomizeThemesShowIndexController extends Controller } get availableLocales() { - return JSON.parse(this.siteSettings.available_locales); + return this.siteSettings.available_locales; } get locale() { diff --git a/app/assets/javascripts/admin/addon/controllers/admin-site-text-index.js b/app/assets/javascripts/admin/addon/controllers/admin-site-text-index.js index 07aa61b57ef..51ba7a1b8ca 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-site-text-index.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-site-text-index.js @@ -79,7 +79,7 @@ export default class AdminSiteTextIndexController extends Controller { } get availableLocales() { - return JSON.parse(this.siteSettings.available_locales); + return this.siteSettings.available_locales; } get fallbackLocaleFullName() { diff --git a/app/assets/javascripts/admin/addon/routes/admin-site-text-edit.js b/app/assets/javascripts/admin/addon/routes/admin-site-text-edit.js index 817f91f389d..d96e492c4cb 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-site-text-edit.js +++ b/app/assets/javascripts/admin/addon/routes/admin-site-text-edit.js @@ -15,7 +15,7 @@ export default class AdminSiteTextEditRoute extends Route { } setupController(controller, siteText) { - const locales = JSON.parse(this.siteSettings.available_locales); + const locales = this.siteSettings.available_locales; const localeFullName = locales.find((locale) => { return locale.value === controller.locale; diff --git a/app/assets/javascripts/discourse/app/components/language-switcher.gjs b/app/assets/javascripts/discourse/app/components/language-switcher.gjs index 1c23e975386..fa872f17055 100644 --- a/app/assets/javascripts/discourse/app/components/language-switcher.gjs +++ b/app/assets/javascripts/discourse/app/components/language-switcher.gjs @@ -11,6 +11,7 @@ export default class LanguageSwitcher extends Component { @service site; @service siteSettings; @service router; + @service languageNameLookup; @action async changeLocale(locale) { @@ -21,6 +22,15 @@ export default class LanguageSwitcher extends Component { window.location.reload(); } + get content() { + return this.siteSettings.available_content_localization_locales.map( + ({ value }) => ({ + name: this.languageNameLookup.getLanguageName(value), + value, + }) + ); + } + @action onRegisterApi(api) { this.dMenu = api; @@ -36,10 +46,7 @@ export default class LanguageSwitcher extends Component { > <:content> - {{#each - this.siteSettings.available_content_localization_locales - as |option| - }} + {{#each this.content as |option|}} diff --git a/app/assets/javascripts/discourse/app/components/post-language-selector.gjs b/app/assets/javascripts/discourse/app/components/post-language-selector.gjs index 749249007fa..a9698213fd8 100644 --- a/app/assets/javascripts/discourse/app/components/post-language-selector.gjs +++ b/app/assets/javascripts/discourse/app/components/post-language-selector.gjs @@ -8,6 +8,7 @@ import DMenu from "float-kit/components/d-menu"; export default class PostLanguageSelector extends Component { @service siteSettings; + @service languageNameLookup; get selectedLanguage() { return ( @@ -17,6 +18,15 @@ export default class PostLanguageSelector extends Component { ); } + get content() { + return this.siteSettings.available_content_localization_locales.map( + ({ value }) => ({ + name: this.languageNameLookup.getLanguageName(value), + value, + }) + ); + } + @action selectPostLanguage(locale) { this.args.composerModel.locale = locale; @@ -40,10 +50,7 @@ export default class PostLanguageSelector extends Component { > <:content> - {{#each - this.siteSettings.available_content_localization_locales - as |locale| - }} + {{#each this.content as |locale|}} locale.value !== originalPostLocale - ); + return this.siteSettings.available_content_localization_locales + .filter(({ value }) => value !== originalPostLocale) + .map(({ native_name, name, value }) => { + name = + i18n(name) === native_name + ? native_name + : `${i18n(name)} (${native_name})`; + + return { name, value }; + }); } @action diff --git a/app/assets/javascripts/discourse/app/components/post/meta-data/language.gjs b/app/assets/javascripts/discourse/app/components/post/meta-data/language.gjs index a4691facd7e..0625d56aade 100644 --- a/app/assets/javascripts/discourse/app/components/post/meta-data/language.gjs +++ b/app/assets/javascripts/discourse/app/components/post/meta-data/language.gjs @@ -1,12 +1,16 @@ import Component from "@glimmer/component"; +import { service } from "@ember/service"; import { i18n } from "discourse-i18n"; import DTooltip from "float-kit/components/d-tooltip"; export default class PostMetaDataLanguage extends Component { // TODO (glimmer-post-stream) once we switch to glimmer, we can remove `this.args.data.x` from the following 2 getters + @service languageNameLookup; + get language() { - return this.args.data?.language || this.args.post?.language; + const lang = this.args.data?.language || this.args.post?.language; + return this.languageNameLookup.getLanguageName(lang); } get outdated() { diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js index 7794b7486a8..ad3cd7c1c15 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js @@ -91,7 +91,7 @@ export default class InterfaceController extends Controller { @discourseComputed() availableLocales() { - return JSON.parse(this.siteSettings.available_locales); + return this.siteSettings.available_locales; } @discourseComputed diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index 83c6ae8a8ea..3945bb99b95 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -73,6 +73,7 @@ export default class TopicController extends Controller { @service siteSettings; @service site; @service appEvents; + @service languageNameLookup; @tracked model; @@ -897,9 +898,10 @@ export default class TopicController extends Controller { return this._openComposerForEdit(topic, post); } + const language = this.languageNameLookup.getLanguageName(post.language); return this.dialog.alert({ message: i18n("post.localizations.edit_warning.message", { - language: post.language, + language, }), buttons: [ { diff --git a/app/assets/javascripts/discourse/app/services/language-name-lookup.js b/app/assets/javascripts/discourse/app/services/language-name-lookup.js new file mode 100644 index 00000000000..13ed14403dd --- /dev/null +++ b/app/assets/javascripts/discourse/app/services/language-name-lookup.js @@ -0,0 +1,14 @@ +import Service, { service } from "@ember/service"; +import { disableImplicitInjections } from "discourse/lib/implicit-injections"; + +@disableImplicitInjections +export default class LanguageNameLookup extends Service { + @service siteSettings; + + getLanguageName(locale) { + const name = this.siteSettings.available_locales.find( + ({ value }) => value === locale + )?.name; + return name || locale; + } +} diff --git a/app/assets/javascripts/discourse/app/services/site-settings.js b/app/assets/javascripts/discourse/app/services/site-settings.js index f5df616a0fa..6de9918574c 100644 --- a/app/assets/javascripts/discourse/app/services/site-settings.js +++ b/app/assets/javascripts/discourse/app/services/site-settings.js @@ -1,6 +1,7 @@ import { TrackedObject } from "@ember-compat/tracked-built-ins"; import { disableImplicitInjections } from "discourse/lib/implicit-injections"; import PreloadStore from "discourse/lib/preload-store"; +import i18n from "discourse-i18n"; export function createSiteSettingsFromPreloaded( siteSettings, @@ -15,6 +16,22 @@ export function createSiteSettingsFromPreloaded( settings.themeSiteSettingOverrides = themeSiteSettingOverrides; } + // localize locale names here as they are not localized in the backend + // due to initialization order and caching + if (settings.available_locales) { + const locales = JSON.parse(settings.available_locales); + const localizedLocales = locales.map(({ native_name, value, name }) => { + const localized_name = i18n.t(name); + const displayName = + localized_name && localized_name !== native_name + ? `${localized_name} (${native_name})` + : native_name; + return { value, name: displayName }; + }); + + settings.available_locales = localizedLocales; + } + settings.groupSettingArray = (groupSetting) => { const setting = settings[groupSetting]; if (!setting) { diff --git a/app/assets/javascripts/discourse/app/static/wizard/components/fields/dropdown.gjs b/app/assets/javascripts/discourse/app/static/wizard/components/fields/dropdown.gjs index b6366671af0..64d7c0a6971 100644 --- a/app/assets/javascripts/discourse/app/static/wizard/components/fields/dropdown.gjs +++ b/app/assets/javascripts/discourse/app/static/wizard/components/fields/dropdown.gjs @@ -1,6 +1,7 @@ import Component from "@glimmer/component"; import { hash } from "@ember/helper"; import { action, set } from "@ember/object"; +import { service } from "@ember/service"; import { Choice } from "discourse/static/wizard/models/wizard"; import { i18n } from "discourse-i18n"; import ColorPalettes from "select-kit/components/color-palettes"; @@ -9,6 +10,8 @@ import FontSelector from "select-kit/components/font-selector"; import HomepageStyleSelector from "select-kit/components/homepage-style-selector"; export default class Dropdown extends Component { + @service siteSettings; + constructor() { super(...arguments); @@ -69,6 +72,16 @@ export default class Dropdown extends Component { ); } } + + if (this.args.field.id === "default_locale") { + this.args.field.choices = this.siteSettings.available_locales.map( + (locale) => + new Choice({ + id: locale.value, + label: locale.name, + }) + ); + } } get component() { @@ -86,6 +99,15 @@ export default class Dropdown extends Component { } } + get nameProperty() { + switch (this.args.field.id) { + case "default_locale": + return "name"; + default: + return "label"; + } + } + keyPress(event) { event.stopPropagation(); } @@ -101,7 +123,7 @@ export default class Dropdown extends Component { class="wizard-container__dropdown" value=@field.value content=@field.choices - nameProperty="label" + nameProperty=this.nameProperty tabindex="9" onChange=this.onChangeValug options=(hash translatedNone=false) diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-site-text-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-site-text-test.js index 1dcf0fc5a37..783811c1236 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-site-text-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-site-text-test.js @@ -5,7 +5,7 @@ import { acceptance } from "discourse/tests/helpers/qunit-helpers"; acceptance("Admin - Site Texts", function (needs) { needs.user(); needs.settings({ - available_locales: JSON.stringify([{ name: "English", value: "en" }]), + available_locales: [{ name: "English", value: "en" }], default_locale: "en", }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/post/post-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/post/post-test.gjs index eae3b73e19b..6cbefff243a 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/post/post-test.gjs +++ b/app/assets/javascripts/discourse/tests/integration/components/post/post-test.gjs @@ -292,29 +292,35 @@ module("Integration | Component | Post", function (hooks) { test("language", async function (assert) { this.post.is_localized = true; - this.post.language = "English"; + this.post.language = "en"; + this.siteSettings.available_locales = [ + { value: "en", name: "English (US)" }, + ]; await renderComponent(this.post); await triggerEvent(".fk-d-tooltip__trigger", "pointermove"); assert.dom(".post-language").hasText( i18n("post.original_language", { - language: "English", + language: "English (US)", }) ); }); test("outdated localization", async function (assert) { this.post.is_localized = true; - this.post.language = "English"; + this.post.language = "en"; this.post.localization_outdated = true; + this.siteSettings.available_locales = [ + { value: "en", name: "English (US)" }, + ]; await renderComponent(this.post); await triggerEvent(".fk-d-tooltip__trigger", "pointermove"); assert.dom(".post-language").hasText( i18n("post.original_language_and_outdated", { - language: "English", + language: "English (US)", }) ); }); diff --git a/app/models/locale_site_setting.rb b/app/models/locale_site_setting.rb index 5d23a146566..704690c6d9d 100644 --- a/app/models/locale_site_setting.rb +++ b/app/models/locale_site_setting.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true class LocaleSiteSetting < EnumSiteSetting + def self.translate_names? + true + end + def self.valid_value?(val) - supported_locales.include?(val) + val.split("|").all? { |v| supported_locales.include?(v) } end def self.values - @values ||= - supported_locales.map do |locale| - lang = language_names[locale] || language_names[locale.split("_")[0]] - { name: lang ? lang["nativeName"] : locale, value: locale } - end + supported_locales.map do |locale| + lang = language_names[locale] || language_names[locale.split("_").first] + native_name = lang&.dig("nativeName") + { native_name:, value: locale, name: "languages.#{locale}.name" } + end end @lock = Mutex.new @@ -53,10 +57,6 @@ class LocaleSiteSetting < EnumSiteSetting @lock.synchronize { @values = @language_names = @supported_locales = nil } end - def self.get_language_name(locale) - values.find { |v| v[:value] == locale.to_s.sub("-", "_") }&.[](:name) - end - FALLBACKS = { en_GB: :en } def self.fallback_locale(locale) diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb index da4bf5da057..fa4f92bec0f 100644 --- a/app/serializers/post_revision_serializer.rb +++ b/app/serializers/post_revision_serializer.rb @@ -194,9 +194,8 @@ class PostRevisionSerializer < ApplicationSerializer end def locale_changes - prev = LocaleSiteSetting.get_language_name(previous["locale"]) - cur = LocaleSiteSetting.get_language_name(current["locale"]) - + prev = previous["locale"].presence + cur = current["locale"].presence { previous: prev, current: cur } end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index cb5a6828864..b3d3ae8b881 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -688,7 +688,7 @@ class PostSerializer < BasicPostSerializer end def language - LocaleSiteSetting.get_language_name(object.locale) || locale + locale end def include_language? diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 15bd736d12b..cbae1ab3577 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -155,6 +155,105 @@ en: placeholder: date from_placeholder: "from date" to_placeholder: "to date" + languages: + ar: + name: Arabic + be: + name: Belarusian + bg: + name: Bulgarian + bs_BA: + name: Bosnian + ca: + name: Catalan + cs: + name: Czech + da: + name: Danish + de: + name: German + el: + name: Greek + en: + name: English (US) + en_GB: + name: English (UK) + es: + name: Spanish + et: + name: Estonian + fa_IR: + name: Persian + fi: + name: Finnish + fr: + name: French + gl: + name: Galician + he: + name: Hebrew + hr: + name: Croatian + hu: + name: Hungarian + hy: + name: Armenian + id: + name: Indonesian + it: + name: Italian + ja: + name: Japanese + ko: + name: Korean + lt: + name: Lithuanian + lv: + name: Latvian + nb_NO: + name: Norwegian Bokmål + nl: + name: Dutch + pl_PL: + name: Polish + pt: + name: Portuguese + pt_BR: + name: Portuguese + ro: + name: Romanian + ru: + name: Russian + sk: + name: Slovak + sl: + name: Slovene + sq: + name: Albanian + sr: + name: Serbian + sv: + name: Swedish + sw: + name: Swahili + te: + name: Telugu + th: + name: Thai + tr_TR: + name: Turkish + ug: + name: Uyghur + uk: + name: Ukrainian + ur: + name: Urdu + vi: + name: Vietnamese + zh_TW: + name: Chinese + zh_CN: + name: Chinese Simplified share: topic_html: 'Topic: %{topicTitle}' post: "post #%{postNumber} by @%{username}" diff --git a/config/site_settings.yml b/config/site_settings.yml index cac27714e9a..52806c34af4 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1638,9 +1638,9 @@ content_localization: default: "" type: list client: true - list_type: named + list_type: locale allow_any: false - choices: "LocaleSiteSetting.values" + enum: "LocaleSiteSetting" area: "localization" validator: "ContentLocalizationLocalesValidator" content_localization_max_locales: diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index a579189024b..ae90df2c9e9 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -273,7 +273,7 @@ module SiteSettingExtension default: SiteSettings::DefaultsProvider::DEFAULT_LOCALE, category: "required", description: description("default_locale"), - type: SiteSetting.types[SiteSetting.types[:enum]], + type: SiteSetting.types[SiteSetting.types[:locale_enum]], preview: nil, value: self.default_locale, valid_values: LocaleSiteSetting.values, @@ -963,10 +963,8 @@ module SiteSettingExtension plugins[name] = opts[:plugin] if opts[:plugin] - type_supervisor.load_setting( - name, - opts.extract!(*SiteSettings::TypeSupervisor::CONSUMED_OPTS), - ) + choices_opts = opts.extract!(*SiteSettings::TypeSupervisor::CONSUMED_OPTS) + type_supervisor.load_setting(name, choices_opts) if !shadowed_val.nil? setup_shadowed_methods(name, shadowed_val) diff --git a/lib/site_settings/type_supervisor.rb b/lib/site_settings/type_supervisor.rb index dc16881b9eb..46b81e282d6 100644 --- a/lib/site_settings/type_supervisor.rb +++ b/lib/site_settings/type_supervisor.rb @@ -61,6 +61,7 @@ class SiteSettings::TypeSupervisor tag_group_list: 26, file_size_restriction: 27, objects: 28, + locale_enum: 29, ) end @@ -180,9 +181,10 @@ class SiteSettings::TypeSupervisor def type_hash(name) name = name.to_sym type = get_type(name) + list_type = get_list_type(name) result = { type: type.to_s } - if type == :enum + if type == :enum || list_type == "locale" if (klass = get_enum_class(name)) result.merge!(valid_values: klass.values, translate_names: klass.translate_names?) else @@ -231,7 +233,7 @@ class SiteSettings::TypeSupervisor end def validate_value(name, type, val) - if type == self.class.types[:enum] + if type == self.class.types[:enum] || get_list_type(name) == "locale" if get_enum_class(name) unless get_enum_class(name).valid_value?(val) raise Discourse::InvalidParameters.new("Invalid value `#{val}` for `#{name}`") @@ -243,9 +245,7 @@ class SiteSettings::TypeSupervisor raise Discourse::InvalidParameters.new(:value) if choice.exclude?(val) end - end - - if type == self.class.types[:list] || type == self.class.types[:string] + elsif type == self.class.types[:list] || type == self.class.types[:string] if @allow_any.key?(name) && !@allow_any[name] split = val.to_s.split("|") resolved_choices = @choices[name] diff --git a/lib/wizard/builder.rb b/lib/wizard/builder.rb index a40ffaeb25f..2c011223ffa 100644 --- a/lib/wizard/builder.rb +++ b/lib/wizard/builder.rb @@ -39,17 +39,12 @@ class Wizard value: SiteSetting.site_description, ) - languages = - step.add_field( - id: "default_locale", - type: "dropdown", - required: false, - value: SiteSetting.default_locale, - ) - - LocaleSiteSetting.values.each do |locale| - languages.add_choice(locale[:value], label: locale[:name]) - end + step.add_field( + id: "default_locale", + type: "dropdown", + required: false, + value: SiteSetting.default_locale, + ) step.on_update do |updater| updater.ensure_changed(:title) diff --git a/plugins/discourse-ai/assets/javascripts/discourse/components/ai-composer-helper-menu.gjs b/plugins/discourse-ai/assets/javascripts/discourse/components/ai-composer-helper-menu.gjs index 40a35e425c3..3523e14e6cc 100644 --- a/plugins/discourse-ai/assets/javascripts/discourse/components/ai-composer-helper-menu.gjs +++ b/plugins/discourse-ai/assets/javascripts/discourse/components/ai-composer-helper-menu.gjs @@ -42,9 +42,7 @@ export default class AiComposerHelperMenu extends Component { // Since we want site default translations (and we are using: force_default_locale) // we need to replace the translated_name with the site default locale name const siteLocale = this.siteSettings.default_locale; - const availableLocales = JSON.parse( - this.siteSettings.available_locales - ); + const availableLocales = this.siteSettings.available_locales; const locale = availableLocales.find((l) => l.value === siteLocale); const translatedName = i18n( "discourse_ai.ai_helper.context_menu.translate_prompt", diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/region-input.js b/plugins/discourse-calendar/assets/javascripts/discourse/components/region-input.js index 372bf508946..b62df85756a 100644 --- a/plugins/discourse-calendar/assets/javascripts/discourse/components/region-input.js +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/region-input.js @@ -22,7 +22,7 @@ export default class RegionInput extends ComboBoxComponent { const localeNames = {}; let regions = []; - JSON.parse(this.siteSettings.available_locales).forEach((locale) => { + this.siteSettings.available_locales.forEach((locale) => { localeNames[locale.value] = locale.name; }); diff --git a/plugins/discourse-calendar/test/javascripts/acceptance/admin-holidays-test.js b/plugins/discourse-calendar/test/javascripts/acceptance/admin-holidays-test.js index 50f097a8979..e6d6f885a3d 100644 --- a/plugins/discourse-calendar/test/javascripts/acceptance/admin-holidays-test.js +++ b/plugins/discourse-calendar/test/javascripts/acceptance/admin-holidays-test.js @@ -7,7 +7,7 @@ acceptance("Admin - Discourse Calendar - Holidays", function (needs) { needs.user(); needs.settings({ calendar_enabled: true, - available_locales: JSON.stringify([{ name: "English", value: "en" }]), + available_locales: [{ name: "English", value: "en" }], }); needs.pretender((server, helper) => { diff --git a/plugins/discourse-calendar/test/javascripts/integration/components/region-input-test.gjs b/plugins/discourse-calendar/test/javascripts/integration/components/region-input-test.gjs index 5b6825144d2..74c5800dc4d 100644 --- a/plugins/discourse-calendar/test/javascripts/integration/components/region-input-test.gjs +++ b/plugins/discourse-calendar/test/javascripts/integration/components/region-input-test.gjs @@ -8,9 +8,7 @@ module("Integration | Component | region-input", function (hooks) { setupRenderingTest(hooks); test("displaying the 'None' region option", async function (assert) { - this.siteSettings.available_locales = JSON.stringify([ - { name: "English", value: "en" }, - ]); + this.siteSettings.available_locales = [{ name: "English", value: "en" }]; await render( @@ -26,9 +24,7 @@ module("Integration | Component | region-input", function (hooks) { }); test("hiding the 'None' region option", async function (assert) { - this.siteSettings.available_locales = JSON.stringify([ - { name: "English", value: "en" }, - ]); + this.siteSettings.available_locales = [{ name: "English", value: "en" }]; await render( diff --git a/spec/models/locale_site_setting_spec.rb b/spec/models/locale_site_setting_spec.rb index 8df0cbe6fa2..38ecffb1d1f 100644 --- a/spec/models/locale_site_setting_spec.rb +++ b/spec/models/locale_site_setting_spec.rb @@ -8,7 +8,7 @@ RSpec.describe LocaleSiteSetting do def native_locale_name(locale) value = LocaleSiteSetting.values.find { |v| v[:value] == locale } - value[:name] + value[:native_name] end describe ".valid_value?" do @@ -56,7 +56,11 @@ RSpec.describe LocaleSiteSetting do end after do - DiscoursePluginRegistry.reset! + DiscoursePluginRegistry.unregister_locale("foo") + DiscoursePluginRegistry.unregister_locale("bar") + DiscoursePluginRegistry.unregister_locale("de") + DiscoursePluginRegistry.unregister_locale("de_AT") + DiscoursePluginRegistry.unregister_locale("tlh") LocaleSiteSetting.reset! end @@ -64,6 +68,7 @@ RSpec.describe LocaleSiteSetting do it "returns true for locales from core" do expect(LocaleSiteSetting.valid_value?("en")).to eq(true) expect(LocaleSiteSetting.valid_value?("de")).to eq(true) + expect(LocaleSiteSetting.valid_value?("en|de")).to eq(true) end it "returns true for locales added by plugins" do @@ -82,8 +87,8 @@ RSpec.describe LocaleSiteSetting do expect(native_locale_name("de")).to eq("Deutsch") end - it "returns the language code when no nativeName is set" do - expect(native_locale_name("tlh")).to eq("tlh") + it "returns nothing when no nativeName is set" do + expect(native_locale_name("tlh")).to eq(nil) end end @@ -116,19 +121,4 @@ RSpec.describe LocaleSiteSetting do end end end - - describe ".get_language_name" do - it "returns the language name for a valid locale" do - expect(LocaleSiteSetting.get_language_name("en")).to eq("English (US)") - expect(LocaleSiteSetting.get_language_name("es")).to eq("Español") - end - - it "returns nil for a locale that doesn't exist" do - expect(LocaleSiteSetting.get_language_name("xx")).to be_nil - end - - it "handles symbol locales" do - expect(LocaleSiteSetting.get_language_name(:en_GB)).to eq("English (UK)") - end - end end diff --git a/spec/requests/steps_controller_spec.rb b/spec/requests/steps_controller_spec.rb index 5defbd84fe5..e31529b4511 100644 --- a/spec/requests/steps_controller_spec.rb +++ b/spec/requests/steps_controller_spec.rb @@ -43,7 +43,8 @@ RSpec.describe StepsController do end it "returns errors if the field has them" do - put "/wizard/steps/introduction.json", params: { fields: { title: "" } } + title = SiteSetting.title + put "/wizard/steps/introduction.json", params: { fields: { title: } } expect(response.status).to eq(422) end diff --git a/spec/serializers/post_revision_serializer_spec.rb b/spec/serializers/post_revision_serializer_spec.rb index ce4e7739ff6..b8b81a5a2e3 100644 --- a/spec/serializers/post_revision_serializer_spec.rb +++ b/spec/serializers/post_revision_serializer_spec.rb @@ -156,7 +156,7 @@ RSpec.describe PostRevisionSerializer do root: false, ).as_json - expect(json[:locale_changes][:previous]).to eq("日本語") + expect(json[:locale_changes][:previous]).to eq("ja") expect(json[:locale_changes][:current]).to eq(nil) end @@ -173,7 +173,7 @@ RSpec.describe PostRevisionSerializer do ).as_json expect(json[:locale_changes][:previous]).to eq(nil) - expect(json[:locale_changes][:current]).to eq("日本語") + expect(json[:locale_changes][:current]).to eq("ja") end end end diff --git a/spec/serializers/post_serializer_spec.rb b/spec/serializers/post_serializer_spec.rb index 8927e36784e..967baa531a9 100644 --- a/spec/serializers/post_serializer_spec.rb +++ b/spec/serializers/post_serializer_spec.rb @@ -788,7 +788,7 @@ RSpec.describe PostSerializer do SiteSetting.content_localization_enabled = true post.update!(locale: "ja") - expect(json[:language]).to eq("日本語") + expect(json[:language]).to eq("ja") end it "defaults to locale if language does not exist" do diff --git a/spec/system/admin_site_setting_locale_spec.rb b/spec/system/admin_site_setting_locale_spec.rb new file mode 100644 index 00000000000..a3eba49b152 --- /dev/null +++ b/spec/system/admin_site_setting_locale_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +describe "Admin Site Setting Locales", type: :system do + let(:settings_page) { PageObjects::Pages::AdminSiteSettings.new } + fab!(:admin) + + before do + sign_in(admin) + SiteSetting.default_locale = "es" + SiteSetting.content_localization_supported_locales = "es|en" + end + + context "for locale enum" do + it "allows selection of a different locale" do + settings_page.visit + + settings_page.type_in_search("default locale") + expect(settings_page.find_setting("default_locale")).to have_content("Spanish (Español)") + + settings_page.select_enum_value("default_locale", "en") + settings_page.save_setting("default_locale") + + settings_page.type_in_search("default locale") + expect(settings_page.find_setting("default_locale")).to have_content("English (US)") + end + end + + context "for locale lists" do + it "allows adding and removing locales" do + SiteSetting.content_localization_supported_locales = "ja" + sign_in(admin) + + settings_page.visit("content_localization_supported_locales") + expect(settings_page.find_setting("content_localization_supported_locales")).to have_content( + "Japanese (日本語)", + ) + + settings_page.select_list_values("content_localization_supported_locales", %w[en]) + settings_page.save_setting("content_localization_supported_locales") + expect(settings_page.find_setting("content_localization_supported_locales")).to have_content( + "Japanese (日本語), English (US)", + ) + + # confirm persist on reload + settings_page.visit("content_localization_supported_locales") + expect(settings_page.find_setting("content_localization_supported_locales")).to have_content( + "Japanese (日本語), English (US)", + ) + end + end +end diff --git a/spec/system/anon_language_switcher_spec.rb b/spec/system/anon_language_switcher_spec.rb index 919971621a1..b4e28e1cbb1 100644 --- a/spec/system/anon_language_switcher_spec.rb +++ b/spec/system/anon_language_switcher_spec.rb @@ -45,8 +45,8 @@ RSpec.describe "Anonymous user language switcher", type: :system do switcher.expand expect(switcher).to have_content("English (US)") - expect(switcher).to have_content("日本語") - expect(switcher).to have_content("Español") + expect(switcher).to have_content("Japanese (日本語)") + expect(switcher).to have_content("Spanish (Español)") SiteSetting.content_localization_supported_locales = "ja" visit("/") diff --git a/spec/system/content_localization_spec.rb b/spec/system/content_localization_spec.rb index 456ed0f4426..7efc1d42ebe 100644 --- a/spec/system/content_localization_spec.rb +++ b/spec/system/content_localization_spec.rb @@ -191,11 +191,16 @@ describe "Content Localization" do let(:banner) { PageObjects::Components::AdminChangesBanner.new } it "does not allow more than the maximum number of locales" do + SiteSetting.content_localization_supported_locales = "en|ja" SiteSetting.content_localization_max_locales = 2 sign_in(admin) settings_page.visit("content_localization_supported_locales") - settings_page.select_list_values("content_localization_supported_locales", %w[en ja es]) + expect(settings_page.find_setting("content_localization_supported_locales")).to have_content( + "English (US), Japanese", + ) + + settings_page.select_list_values("content_localization_supported_locales", %w[es]) settings_page.save_setting("content_localization_supported_locales") expect(settings_page.error_message("content_localization_supported_locales")).to have_content( I18n.t( diff --git a/spec/system/page_objects/pages/admin_site_settings.rb b/spec/system/page_objects/pages/admin_site_settings.rb index ff306c74976..92865892497 100644 --- a/spec/system/page_objects/pages/admin_site_settings.rb +++ b/spec/system/page_objects/pages/admin_site_settings.rb @@ -41,6 +41,16 @@ module PageObjects self end + def select_enum_value(setting_name, value) + setting = + PageObjects::Components::SelectKit.new( + ".row.setting[data-setting='#{setting_name}'] .single-select", + ) + setting.expand + setting.select_row_by_value(value) + self + end + def has_setting?(setting_name) has_css?(".row.setting[data-setting=\"#{setting_name}\"]") end diff --git a/spec/system/post_translation_spec.rb b/spec/system/post_translation_spec.rb index 58aed5e5fde..29dcb069c9c 100644 --- a/spec/system/post_translation_spec.rb +++ b/spec/system/post_translation_spec.rb @@ -28,6 +28,8 @@ describe "Post translations", type: :system do context "when a post does not have translations" do it "should only show the languages listed in the site setting" do + post.update!(locale: "en") + topic_page.visit_topic(topic) find("#post_#{post.post_number} .post-action-menu__add-translation").click translation_selector.expand @@ -156,9 +158,9 @@ describe "Post translations", type: :system do page.find("#create-topic").click post_language_selector.expand expect(post_language_selector).to have_content("English (US)") # default locale - expect(post_language_selector).to have_content("Français") - expect(post_language_selector).to have_content("Español") - expect(post_language_selector).to have_content("Português (BR)") + expect(post_language_selector).to have_content("French (Français)") + expect(post_language_selector).to have_content("Spanish (Español)") + expect(post_language_selector).to have_content("Portuguese (Português (BR))") expect(post_language_selector).to have_content( I18n.t("js.post.localizations.post_language_selector.none"), )