# frozen_string_literal: true require "mini_racer" RSpec.describe JsLocaleHelper do let(:v8_ctx) do discourse_node_modules = "#{Rails.root}/app/assets/javascripts/discourse/node_modules" mf_runtime = "#{discourse_node_modules}/@messageformat/runtime" transpiler = DiscourseJsProcessor::Transpiler.new ctx = MiniRacer::Context.new ctx.load("#{discourse_node_modules}/loader.js/dist/loader/loader.js") ctx.eval("var window = globalThis;") { "@messageformat/runtime/messages": "#{mf_runtime}/esm/messages.js", "@messageformat/runtime": "#{mf_runtime}/esm/runtime.js", "@messageformat/runtime/lib/cardinals": "#{mf_runtime}/esm/cardinals.js", "make-plural/cardinals": "#{discourse_node_modules}/make-plural/cardinals.mjs", "discourse-i18n": "#{Rails.root}/app/assets/javascripts/discourse-i18n/src/index.js", }.each do |module_name, path| ctx.eval(transpiler.perform(File.read(path), "", module_name.to_s)) end ctx.eval <<~JS define("discourse/loader-shims", () => {}) JS # As there are circular references in the return value, this raises an # error if we let MiniRacer try to convert the value to JSON. Forcing # returning `null` from `#eval` will prevent that. ctx.eval("#{File.read("#{Rails.root}/app/assets/javascripts/locales/i18n.js")};null") ctx end module StubLoadTranslations def set_translations(locale, translations) @loaded_translations ||= HashWithIndifferentAccess.new @loaded_translations[locale] = translations end def clear_cache! @loaded_translations = nil @loaded_merges = nil end end JsLocaleHelper.extend StubLoadTranslations before { JsLocaleHelper.clear_cache! } after { JsLocaleHelper.clear_cache! } describe "#output_locale" do it "doesn't change the cached translations hash" do I18n.locale = :fr expect(JsLocaleHelper.output_locale("fr").length).to be > 0 expect(JsLocaleHelper.translations_for("fr")["fr"].keys).to contain_exactly( "js", "admin_js", "wizard_js", ) end end it "performs fallbacks to English if a translation is not available" do JsLocaleHelper.set_translations( "en", "en" => { "js" => { "only_english" => "1-en", "english_and_site" => "3-en", "english_and_user" => "5-en", "all_three" => "7-en", }, }, ) JsLocaleHelper.set_translations( "ru", "ru" => { "js" => { "only_site" => "2-ru", "english_and_site" => "3-ru", "site_and_user" => "6-ru", "all_three" => "7-ru", }, }, ) JsLocaleHelper.set_translations( "uk", "uk" => { "js" => { "only_user" => "4-uk", "english_and_user" => "5-uk", "site_and_user" => "6-uk", "all_three" => "7-uk", }, }, ) expected = { "none" => "[uk.js.none]", "only_english" => "1-en", "only_site" => "[uk.js.only_site]", "english_and_site" => "3-en", "only_user" => "4-uk", "english_and_user" => "5-uk", "site_and_user" => "6-uk", "all_three" => "7-uk", } SiteSetting.default_locale = "ru" I18n.locale = :uk v8_ctx.eval(JsLocaleHelper.output_locale(I18n.locale)) v8_ctx.eval('I18n.defaultLocale = "ru";') expect(v8_ctx.eval("I18n.translations").keys).to contain_exactly("uk", "en") expect(v8_ctx.eval("I18n.translations.uk.js").keys).to contain_exactly( "all_three", "english_and_user", "only_user", "site_and_user", ) expect(v8_ctx.eval("I18n.translations.en.js").keys).to contain_exactly( "only_english", "english_and_site", ) expected.each do |key, expect| expect(v8_ctx.eval("I18n.t(#{"js.#{key}".inspect})")).to eq(expect) end end LocaleSiteSetting.values.each do |locale| it "generates valid date helpers for #{locale[:value]} locale" do js = JsLocaleHelper.output_locale(locale[:value]) v8_ctx.eval(js) end it "finds moment.js locale file for #{locale[:value]}" do content = JsLocaleHelper.moment_locale(locale[:value]) if (locale[:value] == SiteSettings::DefaultsProvider::DEFAULT_LOCALE) expect(content).to eq("") else expect(content).to_not eq("") end end it "generates valid MF locales for the '#{locale[:value]}' locale" do expect(described_class.output_MF(locale[:value])).not_to match(/Failed to compile/) end end describe ".output_MF" do fab!(:overriden_translation_en) do Fabricate( :translation_override, translation_key: "admin_js.admin.user.penalty_history_MF", value: "OVERRIDEN", ) end fab!(:overriden_translation_ja) do Fabricate(:translation_override, locale: "ja", translation_key: "js.posts_likes_MF") end fab!(:overriden_translation_he) do Fabricate(:translation_override, locale: "he", translation_key: "js.posts_likes_MF") end let(:output) { described_class.output_MF(locale).gsub(/^import.*$/, "") } let(:generated_locales) { v8_ctx.eval("Object.keys(I18n._mfMessages._data)") } let(:translated_message) do v8_ctx.eval("I18n._mfMessages.get('posts_likes_MF', {count: 3, ratio: 'med'})") end before do overriden_translation_ja.update_columns( value: "{ count, plural, one {返信 # 件、} other {返信 # 件、} }", ) overriden_translation_he.update_columns(value: "{ count, plural, ") v8_ctx.eval(output) end context "when locale is 'en'" do let(:locale) { "en" } it "generates messages for the 'en' locale only" do expect(generated_locales).to eq %w[en] end it "translates messages properly" do expect( translated_message, ).to eq "3 replies, very high like to post ratio, jump to the first or last post…\n" end context "when the translation is overriden" do let(:translated_message) do v8_ctx.eval( "I18n._mfMessages.get('admin.user.penalty_history_MF', { SUSPENDED: 3, SILENCED: 2 })", ) end it "returns the overriden translation" do expect(translated_message).to eq "OVERRIDEN" end end end context "when locale is not 'en'" do let(:locale) { "fr" } it "generates messages for the current locale and uses 'en' as fallback" do expect(generated_locales).to match(%w[fr en]) end it "translates messages properly" do expect( translated_message, ).to eq "3 réponses, avec un taux très élevé de « J'aime » par publication, accéder à la première ou dernière publication...\n" end context "when a translation is missing" do before { v8_ctx.eval("delete I18n._mfMessages._data.fr.posts_likes_MF") } it "returns the fallback translation" do expect( translated_message, ).to eq "3 replies, very high like to post ratio, jump to the first or last post…\n" end context "when the fallback translation is overriden" do let(:translated_message) do v8_ctx.eval( "I18n._mfMessages.get('admin.user.penalty_history_MF', { SUSPENDED: 3, SILENCED: 2 })", ) end before do v8_ctx.eval("delete I18n._mfMessages._data.fr['admin.user.penalty_history_MF']") end it "returns the overriden fallback translation" do expect(translated_message).to eq "OVERRIDEN" end end end end context "when locale contains invalid plural keys" do let(:locale) { "ja" } it "does not raise an error" do expect(generated_locales).to match(%w[ja en]) end end context "when locale contains malformed messages" do let(:locale) { "he" } it "raises an error" do expect(output).to match(/Failed to compile message formats/) end end end describe ".output_client_overrides" do subject(:client_overrides) { described_class.output_client_overrides("en") } before do Fabricate( :translation_override, locale: "en", translation_key: "js.user.preferences.title", value: "SHOULD_SHOW", ) Fabricate( :translation_override, locale: "en", translation_key: "js.user.preferences", value: "SHOULD_NOT_SHOW", status: "deprecated", ) end it "does not output deprecated translation overrides" do expect(client_overrides).to include("SHOULD_SHOW") expect(client_overrides).not_to include("SHOULD_NOT_SHOW") end end end