# frozen_string_literal: true require "mini_racer" RSpec.describe JsLocaleHelper do let(:v8_ctx) do node_modules = "#{Rails.root}/node_modules/" transpiler = DiscourseJsProcessor::Transpiler.new discourse_i18n = transpiler.perform( File.read("#{Rails.root}/app/assets/javascripts/discourse-i18n/src/index.js"), "app/assets/javascripts/discourse", "discourse-i18n", ) ctx = MiniRacer::Context.new ctx.load("#{node_modules}/loader.js/dist/loader/loader.js") ctx.eval("var window = globalThis;") ctx.eval(discourse_i18n) ctx.eval <<~JS define("discourse/loader-shims", () => {}) JS ctx.load("#{Rails.root}/app/assets/javascripts/locales/i18n.js") 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 describe "message format" do def message_format_filename(locale) Rails.root + "lib/javascripts/locale/#{locale}.js" end def setup_message_format(format) filename = message_format_filename("en") compiled = JsLocaleHelper.compile_message_format(filename, "en", format) @ctx = MiniRacer::Context.new @ctx.eval("MessageFormat = {locale: {}};") @ctx.load(filename) @ctx.eval("var test = #{compiled}") end def localize(opts) @ctx.eval("test(#{opts.to_json})") end it "handles plurals" do setup_message_format( "{NUM_RESULTS, plural, one {1 result} other {# results} }", ) expect(localize(NUM_RESULTS: 1)).to eq("1 result") expect(localize(NUM_RESULTS: 2)).to eq("2 results") end it "handles double plurals" do setup_message_format( "{NUM_RESULTS, plural, one {1 result} other {# results} } and {NUM_APPLES, plural, one {1 apple} other {# apples} }", ) expect(localize(NUM_RESULTS: 1, NUM_APPLES: 2)).to eq("1 result and 2 apples") expect(localize(NUM_RESULTS: 2, NUM_APPLES: 1)).to eq("2 results and 1 apple") end it "handles select" do setup_message_format("{GENDER, select, male {He} female {She} other {They}} read a book") expect(localize(GENDER: "male")).to eq("He read a book") expect(localize(GENDER: "female")).to eq("She read a book") expect(localize(GENDER: "none")).to eq("They read a book") end it "can strip out message formats" do hash = { "a" => "b", "c" => { "d" => { "f_MF" => "bob" } } } expect(JsLocaleHelper.strip_out_message_formats!(hash)).to eq("c.d.f_MF" => "bob") expect(hash["c"]["d"]).to eq({}) end it "handles message format special keys" do JsLocaleHelper.set_translations( "en", "en" => { "js" => { "hello" => "world", "test_MF" => "{HELLO} {COUNT, plural, one {1 duck} other {# ducks}}", "error_MF" => "{{BLA}", "simple_MF" => "{COUNT, plural, one {1} other {#}}", }, "admin_js" => { "foo_MF" => "{HELLO} {COUNT, plural, one {1 duck} other {# ducks}}", }, }, ) ctx = MiniRacer::Context.new ctx.eval("I18n = { pluralizationRules: {} };") ctx.eval(JsLocaleHelper.output_locale("en")) expect(ctx.eval('I18n.translations["en"]["js"]["hello"]')).to eq("world") expect(ctx.eval('I18n.translations["en"]["js"]["test_MF"]')).to eq(nil) expect(ctx.eval('I18n.messageFormat("test_MF", { HELLO: "hi", COUNT: 3 })')).to eq( "hi 3 ducks", ) expect(ctx.eval('I18n.messageFormat("error_MF", { HELLO: "hi", COUNT: 3 })')).to match( /Invalid Format/, ) expect(ctx.eval('I18n.messageFormat("missing", {})')).to match(/missing/) expect(ctx.eval('I18n.messageFormat("simple_MF", {})')).to match(/COUNT/) # error expect(ctx.eval('I18n.messageFormat("foo_MF", { HELLO: "hi", COUNT: 4 })')).to eq( "hi 4 ducks", ) end it "load pluralization rules before precompile" do message = JsLocaleHelper.compile_message_format(message_format_filename("ru"), "ru", "format") expect(message).not_to match "Plural Function not found" end it "uses message formats from fallback locale" do translations = JsLocaleHelper.translations_for(:en_GB) en_gb_message_formats = JsLocaleHelper.remove_message_formats!(translations, :en_GB) expect(en_gb_message_formats).to_not be_empty translations = JsLocaleHelper.translations_for(:en) en_message_formats = JsLocaleHelper.remove_message_formats!(translations, :en) expect(en_gb_message_formats).to eq(en_message_formats) 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 it "correctly evaluates message formats in en fallback" do JsLocaleHelper.set_translations("en", "en" => { "js" => { "something_MF" => "en mf" } }) JsLocaleHelper.set_translations("de", "de" => { "js" => { "something_MF" => "de mf" } }) TranslationOverride.upsert!("en", "js.something_MF", <<~MF.strip) There { UNREAD, plural, =0 {are no} one {is one unread} other {are # unread} } MF v8_ctx.eval(JsLocaleHelper.output_locale("de")) v8_ctx.eval(JsLocaleHelper.output_client_overrides("de")) v8_ctx.eval(<<~JS) for (let [key, value] of Object.entries(I18n._mfOverrides || {})) { key = key.replace(/^[a-z_]*js\./, ""); I18n._compiledMFs[key] = value; } JS expect(v8_ctx.eval("I18n.messageFormat('something_MF', { UNREAD: 1 })")).to eq( "There is one unread", ) 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 end describe ".find_message_format_locale" do it "finds locale's message format rules" do locale, filename = JsLocaleHelper.find_message_format_locale([:de], fallback_to_english: false) expect(locale).to eq("de") expect(filename).to end_with("/de.js") end it "finds locale for en_GB" do locale, filename = JsLocaleHelper.find_message_format_locale([:en_GB], fallback_to_english: false) expect(locale).to eq("en") expect(filename).to end_with("/en.js") locale, filename = JsLocaleHelper.find_message_format_locale(["en_GB"], fallback_to_english: false) expect(locale).to eq("en") expect(filename).to end_with("/en.js") end it "falls back to en when locale doesn't have own message format rules" do locale, filename = JsLocaleHelper.find_message_format_locale([:nonexistent], fallback_to_english: true) expect(locale).to eq("en") expect(filename).to end_with("/en.js") end end end