From eb52c5469e04cb2ea91ce67d442f2e54cf7a4a01 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 25 Jan 2018 12:09:18 +0100 Subject: [PATCH] FEATURE: Allow plugins to register a new locale --- .eslintignore | 6 +- app/assets/javascripts/locales/i18n.js | 5 + app/models/locale_site_setting.rb | 55 ++++----- app/models/translation_override.rb | 3 +- config/locales/names.yml | 3 - lib/discourse_plugin_registry.rb | 15 +++ lib/freedom_patches/translate_accelerator.rb | 7 ++ lib/i18n/backend/fallback_locale_list.rb | 3 +- lib/js_locale_helper.rb | 79 +++++++++---- lib/plugin/instance.rb | 52 ++++++++- spec/components/fallback_locale_list_spec.rb | 48 ++++++++ .../translate_accelerator_spec.rb | 38 ++++++ spec/components/js_locale_helper_spec.rb | 11 +- spec/components/plugin/instance_spec.rb | 108 +++++++++++++++++- .../assets/locales/es_MX.js.erb | 2 + .../custom_locales/assets/locales/foo.js.erb | 2 + .../custom_locales/assets/locales/tlh.js.erb | 2 + .../config/locales/client.es_MX.yml | 1 + .../config/locales/client.foo.yml | 1 + .../config/locales/client.tlh.yml | 1 + .../config/locales/server.es_MX.yml | 1 + .../config/locales/server.foo.yml | 1 + .../config/locales/server.tlh.yml | 1 + .../javascripts/locale/message_format/foo.js | 1 + .../javascripts/locale/message_format/tlh.js | 1 + .../lib/javascripts/locale/moment_js/foo.js | 1 + .../fixtures/plugins/custom_locales/plugin.rb | 4 + spec/models/locale_site_setting_spec.rb | 72 +++++++++++- test/javascripts/lib/i18n-test.js.es6 | 27 ++++- 29 files changed, 480 insertions(+), 71 deletions(-) create mode 100644 spec/components/fallback_locale_list_spec.rb create mode 100644 spec/fixtures/plugins/custom_locales/assets/locales/es_MX.js.erb create mode 100644 spec/fixtures/plugins/custom_locales/assets/locales/foo.js.erb create mode 100644 spec/fixtures/plugins/custom_locales/assets/locales/tlh.js.erb create mode 100644 spec/fixtures/plugins/custom_locales/config/locales/client.es_MX.yml create mode 100644 spec/fixtures/plugins/custom_locales/config/locales/client.foo.yml create mode 100644 spec/fixtures/plugins/custom_locales/config/locales/client.tlh.yml create mode 100644 spec/fixtures/plugins/custom_locales/config/locales/server.es_MX.yml create mode 100644 spec/fixtures/plugins/custom_locales/config/locales/server.foo.yml create mode 100644 spec/fixtures/plugins/custom_locales/config/locales/server.tlh.yml create mode 100644 spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/foo.js create mode 100644 spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/tlh.js create mode 100644 spec/fixtures/plugins/custom_locales/lib/javascripts/locale/moment_js/foo.js create mode 100644 spec/fixtures/plugins/custom_locales/plugin.rb diff --git a/.eslintignore b/.eslintignore index cc014c8b92f..5e8b92a1c90 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,5 @@ app/assets/javascripts/env.js -app/assets/javascripts/main_include.js app/assets/javascripts/main_include_admin.js -app/assets/javascripts/pagedown_custom.js app/assets/javascripts/vendor.js app/assets/javascripts/locales/i18n.js app/assets/javascripts/ember-addons/ @@ -11,11 +9,9 @@ lib/javascripts/messageformat.js lib/javascripts/moment.js lib/javascripts/moment_locale/ lib/highlight_js/ +plugins/**/lib/javascripts/locale public/javascripts/ -spec/phantom_js/smoke_test.js vendor/ test/javascripts/test_helper.js -test/javascripts/test_helper.js test/javascripts/fixtures test/javascripts/helpers/assertions.js -app/assets/javascripts/ember-addons/ diff --git a/app/assets/javascripts/locales/i18n.js b/app/assets/javascripts/locales/i18n.js index ba56bc3a80c..b1efc2e1124 100644 --- a/app/assets/javascripts/locales/i18n.js +++ b/app/assets/javascripts/locales/i18n.js @@ -15,6 +15,7 @@ I18n.pluralizationRules = { // Set current locale to null I18n.locale = null; +I18n.fallbackLocale = null; // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`. I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm; @@ -143,6 +144,10 @@ I18n.translate = function(scope, options) { var translation = this.lookup(scope, options); if (!this.noFallbacks) { + if (!translation && this.fallbackLocale) { + options.locale = this.fallbackLocale; + translation = this.lookup(scope, options); + } if (!translation && this.currentLocale() !== this.defaultLocale) { options.locale = this.defaultLocale; translation = this.lookup(scope, options); diff --git a/app/models/locale_site_setting.rb b/app/models/locale_site_setting.rb index b38b5f85ac1..ae3c02d3ccd 100644 --- a/app/models/locale_site_setting.rb +++ b/app/models/locale_site_setting.rb @@ -7,9 +7,12 @@ class LocaleSiteSetting < EnumSiteSetting end def self.values - supported_locales.map do |l| - lang = language_names[l] || language_names[l[0..1]] - { name: lang ? lang['nativeName'] : l, value: l } + @values ||= supported_locales.map do |locale| + lang = language_names[locale] || language_names[locale.split("_")[0]] + { + name: lang ? lang['nativeName'] : locale, + value: locale + } end end @@ -19,43 +22,41 @@ class LocaleSiteSetting < EnumSiteSetting return @language_names if @language_names @lock.synchronize do - @language_names ||= YAML.load(File.read(File.join(Rails.root, 'config', 'locales', 'names.yml'))) + @language_names ||= begin + names = YAML.load(File.read(File.join(Rails.root, 'config', 'locales', 'names.yml'))) + + DiscoursePluginRegistry.locales.each do |locale, options| + if !names.key?(locale) && options[:name] && options[:nativeName] + names[locale] = { "name" => options[:name], "nativeName" => options[:nativeName] } + end + end + + names + end end end def self.supported_locales @lock.synchronize do @supported_locales ||= begin - app_client_files = Dir.glob( + locales = Dir.glob( File.join(Rails.root, 'config', 'locales', 'client.*.yml') - ) + ).map { |x| x.split('.')[-2] } - unless ignore_plugins? - app_client_files += Dir.glob( - File.join(Rails.root, 'plugins', '*', 'config', 'locales', 'client.*.yml') - ) - end - - app_client_files.map { |x| x.split('.')[-2] } - .uniq - .select { |locale| valid_locale?(locale) } - .sort + locales += DiscoursePluginRegistry.locales.keys + locales.uniq.sort end end end - def self.valid_locale?(locale) - assets = Rails.configuration.assets - - assets.precompile.grep(/locales\/#{locale}(?:\.js)?/).present? && - (Dir.glob(File.join(Rails.root, 'app', 'assets', 'javascripts', 'locales', "#{locale}.js.erb")).present? || - Dir.glob(File.join(Rails.root, 'plugins', '*', 'assets', 'locales', "#{locale}.js.erb")).present?) + def self.reset! + @lock.synchronize do + @values = @language_names = @supported_locales = nil + end end - def self.ignore_plugins? - Rails.env.test? && ENV['LOAD_PLUGINS'] != "1" + def self.fallback_locale(locale) + plugin_locale = DiscoursePluginRegistry.locales[locale.to_s] + plugin_locale ? plugin_locale[:fallbackLocale]&.to_sym : nil end - - private_class_method :valid_locale? - private_class_method :ignore_plugins? end diff --git a/app/models/translation_override.rb b/app/models/translation_override.rb index 4c2ac9b82ca..ea3859930c5 100644 --- a/app/models/translation_override.rb +++ b/app/models/translation_override.rb @@ -21,7 +21,8 @@ class TranslationOverride < ActiveRecord::Base data = { value: value } if key.end_with?('_MF') - data[:compiled_js] = JsLocaleHelper.compile_message_format(locale, value) + _, filename = JsLocaleHelper.find_message_format_locale(['en'], false) + data[:compiled_js] = JsLocaleHelper.compile_message_format(filename, locale, value) end translation_override = find_or_initialize_by(params) diff --git a/config/locales/names.yml b/config/locales/names.yml index 6f15ab632d6..60e9925014a 100644 --- a/config/locales/names.yml +++ b/config/locales/names.yml @@ -119,9 +119,6 @@ eo: es: name: Spanish nativeName: Español -es_MX: - name: Spanish - nativeName: Español (MX) et: name: Estonian nativeName: eesti diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index afe8009f8c5..e46cf891fd8 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -14,6 +14,7 @@ class DiscoursePluginRegistry attr_writer :handlebars attr_writer :serialized_current_user_fields attr_writer :seed_data + attr_writer :locales attr_accessor :custom_html def plugins @@ -65,6 +66,10 @@ class DiscoursePluginRegistry @seed_data ||= HashWithIndifferentAccess.new({}) end + def locales + @locales ||= HashWithIndifferentAccess.new({}) + end + def html_builders @html_builders ||= {} end @@ -92,6 +97,10 @@ class DiscoursePluginRegistry self.class.stylesheets << filename end + def self.register_locale(locale, options = {}) + self.locales[locale] = options + end + def register_archetype(name, options = {}) Archetype.register(name, options) end @@ -171,6 +180,10 @@ class DiscoursePluginRegistry result.uniq end + def locales + self.class.locales + end + def javascripts self.class.javascripts end @@ -207,6 +220,7 @@ class DiscoursePluginRegistry self.desktop_stylesheets = nil self.sass_variables = nil self.handlebars = nil + self.locales = nil end def self.reset! @@ -222,6 +236,7 @@ class DiscoursePluginRegistry html_builders.clear vendored_pretty_text.clear seed_path_builders.clear + locales.clear end def self.setup(plugin_class) diff --git a/lib/freedom_patches/translate_accelerator.rb b/lib/freedom_patches/translate_accelerator.rb index 92f0b80b311..fe737f52578 100644 --- a/lib/freedom_patches/translate_accelerator.rb +++ b/lib/freedom_patches/translate_accelerator.rb @@ -39,6 +39,13 @@ module I18n if @loaded_locales.empty? # load all rb files I18n.backend.load_translations(I18n.load_path.grep(/\.rb$/)) + + # load plural rules from plugins + DiscoursePluginRegistry.locales.each do |locale, options| + if options[:plural] + I18n.backend.store_translations(locale, i18n: { plural: options[:plural] }) + end + end end # load it diff --git a/lib/i18n/backend/fallback_locale_list.rb b/lib/i18n/backend/fallback_locale_list.rb index 29d9700dbaa..6b4faf1bf1b 100644 --- a/lib/i18n/backend/fallback_locale_list.rb +++ b/lib/i18n/backend/fallback_locale_list.rb @@ -3,7 +3,8 @@ module I18n # Configure custom fallback order class FallbackLocaleList < Hash def [](locale) - [locale, SiteSetting.default_locale.to_sym, :en].uniq.compact + fallback_locale = LocaleSiteSetting.fallback_locale(locale) + [locale, fallback_locale, SiteSetting.default_locale.to_sym, :en].uniq.compact end end end diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 59a9c27c3cb..af8d5fbfacc 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -85,6 +85,7 @@ module JsLocaleHelper end def self.load_translations_merged(*locales) + locales = locales.compact @loaded_merges ||= {} @loaded_merges[locales.join('-')] ||= begin all_translations = {} @@ -101,9 +102,10 @@ module JsLocaleHelper end def self.translations_for(locale_str) - current_locale = I18n.locale - locale_sym = locale_str.to_sym - site_locale = SiteSetting.default_locale.to_sym + current_locale = I18n.locale + locale_sym = locale_str.to_sym + site_locale = SiteSetting.default_locale.to_sym + fallback_locale = LocaleSiteSetting.fallback_locale(locale_str) I18n.locale = locale_sym @@ -113,9 +115,9 @@ module JsLocaleHelper elsif locale_sym == :en load_translations(locale_sym) elsif locale_sym == site_locale || site_locale == :en - load_translations_merged(locale_sym, :en) + load_translations_merged(locale_sym, fallback_locale, :en) else - load_translations_merged(locale_sym, site_locale, :en) + load_translations_merged(locale_sym, fallback_locale, site_locale, :en) end I18n.locale = current_locale @@ -125,11 +127,13 @@ module JsLocaleHelper def self.output_locale(locale) locale_str = locale.to_s + fallback_locale_str = LocaleSiteSetting.fallback_locale(locale_str)&.to_s translations = Marshal.load(Marshal.dump(translations_for(locale_str))) message_formats = strip_out_message_formats!(translations[locale_str]['js']) message_formats.merge!(strip_out_message_formats!(translations[locale_str]['admin_js'])) - result = generate_message_format(message_formats, locale_str) + mf_locale, mf_filename = find_message_format_locale([locale_str], true) + result = generate_message_format(message_formats, mf_locale, mf_filename) translations.keys.each do |l| translations[l].keys.each do |k| @@ -140,7 +144,8 @@ module JsLocaleHelper # I18n result << "I18n.translations = #{translations.to_json};\n" result << "I18n.locale = '#{locale_str}';\n" - result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{locale_str};\n" if locale_str != "en" + result << "I18n.fallbackLocale = '#{fallback_locale_str}';\n" if fallback_locale_str && fallback_locale_str != "en" + result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{mf_locale};\n" if mf_locale != "en" # moment result << File.read("#{Rails.root}/lib/javascripts/moment.js") @@ -150,6 +155,41 @@ module JsLocaleHelper result end + def self.find_moment_locale(locale_chain) + path = "#{Rails.root}/lib/javascripts/moment_locale" + + # moment.js uses a different naming scheme for locale files + locale_chain = locale_chain.map { |l| l.tr('_', '-').downcase } + + find_locale(locale_chain, path, :moment_js, false) + end + + def self.find_message_format_locale(locale_chain, fallback_to_english) + path = "#{Rails.root}/lib/javascripts/locale" + find_locale(locale_chain, path, :message_format, fallback_to_english) + end + + def self.find_locale(locale_chain, path, type, fallback_to_english) + locale_chain.each do |locale| + plugin_locale = DiscoursePluginRegistry.locales[locale] + return plugin_locale[type] if plugin_locale&.has_key?(type) + + filename = File.join(path, "#{locale}.js") + return [locale, filename] if File.exist?(filename) + end + + # try again, but this time only with the language itself + locale_chain = locale_chain.map { |l| l.split(/[-_]/)[0] } + .uniq.reject { |l| locale_chain.include?(l) } + unless locale_chain.empty? + locale_data = find_locale(locale_chain, path, type, false) + return locale_data if locale_data + end + + # English should alyways work + ["en", File.join(path, "en.js")] if fallback_to_english + end + def self.moment_formats result = "" result << moment_format_function('short_date_no_year') @@ -163,23 +203,13 @@ module JsLocaleHelper "moment.fn.#{name.camelize(:lower)} = function(){ return this.format('#{format}'); };\n" end - def self.moment_locale(locale_str) - # moment.js uses a different naming scheme for locale files - locale_str = locale_str.tr('_', '-').downcase - filename = "#{Rails.root}/lib/javascripts/moment_locale/#{locale_str}.js" - - # try the language without the territory - locale_str = locale_str.split("-")[0] - filename = "#{Rails.root}/lib/javascripts/moment_locale/#{locale_str}.js" unless File.exists?(filename) - - File.exists?(filename) ? File.read(filename) << "\n" : "" + def self.moment_locale(locale) + _, filename = find_moment_locale([locale]) + filename && File.exist?(filename) ? File.read(filename) << "\n" : "" end - def self.generate_message_format(message_formats, locale_str) - formats = message_formats.map { |k, v| k.inspect << " : " << compile_message_format(locale_str, v) }.join(", ") - - filename = "#{Rails.root}/lib/javascripts/locale/#{locale_str}.js" - filename = "#{Rails.root}/lib/javascripts/locale/en.js" unless File.exists?(filename) + def self.generate_message_format(message_formats, locale, filename) + formats = message_formats.map { |k, v| k.inspect << " : " << compile_message_format(filename, locale, v) }.join(", ") result = "MessageFormat = {locale: {}};\n" result << "I18n._compiledMFs = {#{formats}};\n" @@ -203,10 +233,9 @@ module JsLocaleHelper end end - def self.compile_message_format(locale, format) + def self.compile_message_format(path, locale, format) with_context do |ctx| - path = "#{Rails.root}/lib/javascripts/locale/#{locale}.js" - ctx.load(path) if File.exists?(path) + ctx.load(path) if File.exist?(path) ctx.eval("mf = new MessageFormat('#{locale}');") ctx.eval("mf.precompile(mf.parse(#{format.inspect}))") end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 197606da79b..91953a10747 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -29,6 +29,7 @@ class Plugin::Instance :color_schemes, :initializers, :javascripts, + :locales, :service_workers, :styles, :themes].each do |att| @@ -319,6 +320,14 @@ class Plugin::Instance javascripts << js end + # @option opts [String] :name + # @option opts [String] :nativeName + # @option opts [String] :fallbackLocale + # @option opts [Hash] :plural + def register_locale(locale, opts = {}) + locales << [locale, opts] + end + def register_custom_html(hash) DiscoursePluginRegistry.custom_html ||= {} DiscoursePluginRegistry.custom_html.merge!(hash) @@ -427,7 +436,7 @@ JS end register_assets! unless assets.blank? - + register_locales! register_service_workers! seed_data.each do |key, value| @@ -532,6 +541,33 @@ JS end end + def register_locales! + root_path = File.dirname(@path) + + locales.each do |locale, opts| + opts = opts.dup + opts[:client_locale_file] = File.join(root_path, "config/locales/client.#{locale}.yml") + opts[:server_locale_file] = File.join(root_path, "config/locales/server.#{locale}.yml") + opts[:js_locale_file] = File.join(root_path, "assets/locales/#{locale}.js.erb") + + locale_chain = opts[:fallbackLocale] ? [locale, opts[:fallbackLocale]] : [locale] + lib_locale_path = File.join(root_path, "lib/javascripts/locale") + + path = File.join(lib_locale_path, "message_format") + opts[:message_format] = find_locale_file(locale_chain, path) + opts[:message_format] = JsLocaleHelper.find_message_format_locale(locale_chain, false) unless opts[:message_format] + + path = File.join(lib_locale_path, "moment_js") + opts[:moment_js] = find_locale_file(locale_chain, path) + opts[:moment_js] = JsLocaleHelper.find_moment_locale(locale_chain) unless opts[:moment_js] + + if valid_locale?(opts) + DiscoursePluginRegistry.register_locale(locale, opts) + Rails.configuration.assets.precompile << "locales/#{locale}.js" + end + end + end + private def write_asset(path, contents) @@ -553,4 +589,18 @@ JS yield plugin end + def valid_locale?(custom_locale) + File.exist?(custom_locale[:client_locale_file]) && + File.exist?(custom_locale[:server_locale_file]) && + File.exist?(custom_locale[:js_locale_file]) && + custom_locale[:message_format] && custom_locale[:moment_js] + end + + def find_locale_file(locale_chain, path) + locale_chain.each do |locale| + filename = File.join(path, "#{locale}.js") + return [locale, filename] if File.exist?(filename) + end + nil + end end diff --git a/spec/components/fallback_locale_list_spec.rb b/spec/components/fallback_locale_list_spec.rb new file mode 100644 index 00000000000..2786ff1cd6f --- /dev/null +++ b/spec/components/fallback_locale_list_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' +require 'i18n/backend/fallback_locale_list' + +describe I18n::Backend::FallbackLocaleList do + let(:list) { I18n::Backend::FallbackLocaleList.new } + + it "works when default_locale is English" do + SiteSetting.default_locale = :en + + expect(list[:ru]).to eq([:ru, :en]) + expect(list[:en]).to eq([:en]) + end + + it "works when default_locale is not English" do + SiteSetting.default_locale = :de + + expect(list[:ru]).to eq([:ru, :de, :en]) + expect(list[:de]).to eq([:de, :en]) + expect(list[:en]).to eq([:en, :de]) + end + + context "when plugin registered fallback locale" do + before do + DiscoursePluginRegistry.register_locale("es_MX", fallbackLocale: "es") + DiscoursePluginRegistry.register_locale("de_AT", fallbackLocale: "de") + end + + after do + DiscoursePluginRegistry.reset! + end + + it "works when default_locale is English" do + SiteSetting.default_locale = :en + + expect(list[:de_AT]).to eq([:de_AT, :de, :en]) + expect(list[:de]).to eq([:de, :en]) + expect(list[:en]).to eq([:en]) + end + + it "works when default_locale is not English" do + SiteSetting.default_locale = :de + + expect(list[:es_MX]).to eq([:es_MX, :es, :de, :en]) + expect(list[:es]).to eq([:es, :de, :en]) + expect(list[:en]).to eq([:en, :de]) + end + end +end diff --git a/spec/components/freedom_patches/translate_accelerator_spec.rb b/spec/components/freedom_patches/translate_accelerator_spec.rb index 883edd6f1c4..941b68e52a1 100644 --- a/spec/components/freedom_patches/translate_accelerator_spec.rb +++ b/spec/components/freedom_patches/translate_accelerator_spec.rb @@ -1,6 +1,9 @@ require "rails_helper" describe "translate accelerator" do + after do + I18n.reload! + end it "overrides for both string and symbol keys" do key = "user.email.not_allowed" @@ -32,4 +35,39 @@ describe "translate accelerator" do end end + context "plugins" do + before do + DiscoursePluginRegistry.register_locale( + "foo", + name: "Foo", + nativeName: "Foo Bar", + plural: { + keys: [:one, :few, :other], + rule: lambda do |n| + return :one if n == 1 + return :few if n < 10 + :other + end + } + ) + + LocaleSiteSetting.reset! + I18n.reload! + end + + after do + DiscoursePluginRegistry.reset! + LocaleSiteSetting.reset! + end + + it "loads plural rules from plugins" do + I18n.backend.store_translations(:foo, items: { one: 'one item', few: 'some items', other: "%{count} items" }) + I18n.locale = :foo + + expect(I18n.t('i18n.plural.keys')).to eq([:one, :few, :other]) + expect(I18n.t('items', count: 1)).to eq('one item') + expect(I18n.t('items', count: 3)).to eq('some items') + expect(I18n.t('items', count: 20)).to eq('20 items') + end + end end diff --git a/spec/components/js_locale_helper_spec.rb b/spec/components/js_locale_helper_spec.rb index 120b6b029bd..430a372504c 100644 --- a/spec/components/js_locale_helper_spec.rb +++ b/spec/components/js_locale_helper_spec.rb @@ -34,12 +34,17 @@ describe JsLocaleHelper do end context "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(Rails.root + 'lib/javascripts/locale/en.js') - compiled = JsLocaleHelper.compile_message_format('en', format) + @ctx.load(filename) @ctx.eval("var test = #{compiled}") end @@ -110,7 +115,7 @@ describe JsLocaleHelper do end it 'load pluralizations rules before precompile' do - message = JsLocaleHelper.compile_message_format('ru', 'format') + message = JsLocaleHelper.compile_message_format(message_format_filename('ru'), 'ru', 'format') expect(message).not_to match 'Plural Function not found' end end diff --git a/spec/components/plugin/instance_spec.rb b/spec/components/plugin/instance_spec.rb index 7c1f37f6a50..82e9cb3a2bd 100644 --- a/spec/components/plugin/instance_spec.rb +++ b/spec/components/plugin/instance_spec.rb @@ -10,8 +10,8 @@ describe Plugin::Instance do context "find_all" do it "can find plugins correctly" do plugins = Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins") - expect(plugins.count).to eq(1) - plugin = plugins[0] + expect(plugins.count).to eq(2) + plugin = plugins[1] expect(plugin.name).to eq("plugin-name") expect(plugin.path).to eq("#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb") @@ -268,4 +268,108 @@ describe Plugin::Instance do expect(called).to eq(1) end end + + context "locales" do + let(:plugin_path) { "#{Rails.root}/spec/fixtures/plugins/custom_locales" } + let!(:plugin) { Plugin::Instance.new(nil, "#{plugin_path}/plugin.rb") } + let(:plural) do + { + keys: [:one, :few, :other], + rule: lambda do |n| + return :one if n == 1 + return :few if n < 10 + :other + end + } + end + + def register_locale(locale, opts) + plugin.register_locale(locale, opts) + plugin.activate! + + DiscoursePluginRegistry.locales[locale] + end + + it "enables the registered locales only on activate" do + plugin.register_locale("foo", name: "Foo", nativeName: "Foo Bar", plural: plural) + plugin.register_locale("es_MX", name: "Spanish (Mexico)", nativeName: "Español (México)", fallbackLocale: "es") + expect(DiscoursePluginRegistry.locales.count).to eq(0) + + plugin.activate! + expect(DiscoursePluginRegistry.locales.count).to eq(2) + end + + it "allows finding the locale by string and symbol" do + register_locale("foo", name: "Foo", nativeName: "Foo Bar", plural: plural) + + expect(DiscoursePluginRegistry.locales).to have_key(:foo) + expect(DiscoursePluginRegistry.locales).to have_key('foo') + end + + it "correctly registers a new locale" do + locale = register_locale("foo", name: "Foo", nativeName: "Foo Bar", plural: plural) + + expect(DiscoursePluginRegistry.locales.count).to eq(1) + expect(DiscoursePluginRegistry.locales).to have_key(:foo) + + expect(locale[:fallbackLocale]).to be_nil + expect(locale[:message_format]).to eq(["foo", "#{plugin_path}/lib/javascripts/locale/message_format/foo.js"]) + expect(locale[:moment_js]).to eq(["foo", "#{plugin_path}/lib/javascripts/locale/moment_js/foo.js"]) + expect(locale[:plural]).to eq(plural.with_indifferent_access) + + expect(Rails.configuration.assets.precompile).to include("locales/foo.js") + end + + it "correctly registers a new locale using a fallback locale" do + locale = register_locale("es_MX", name: "Spanish (Mexico)", nativeName: "Español (México)", fallbackLocale: "es") + + expect(DiscoursePluginRegistry.locales.count).to eq(1) + expect(DiscoursePluginRegistry.locales).to have_key(:es_MX) + + expect(locale[:fallbackLocale]).to eq("es") + expect(locale[:message_format]).to eq(["es", "#{Rails.root}/lib/javascripts/locale/es.js"]) + expect(locale[:moment_js]).to eq(["es", "#{Rails.root}/lib/javascripts/moment_locale/es.js"]) + expect(locale[:plural]).to be_nil + + expect(Rails.configuration.assets.precompile).to include("locales/es_MX.js") + end + + it "correctly registers a new locale when some files exist in core" do + locale = register_locale("tlh", name: "Klingon", nativeName: "tlhIngan Hol", plural: plural) + + expect(DiscoursePluginRegistry.locales.count).to eq(1) + expect(DiscoursePluginRegistry.locales).to have_key(:tlh) + + expect(locale[:fallbackLocale]).to be_nil + expect(locale[:message_format]).to eq(["tlh", "#{plugin_path}/lib/javascripts/locale/message_format/tlh.js"]) + expect(locale[:moment_js]).to eq(["tlh", "#{Rails.root}/lib/javascripts/moment_locale/tlh.js"]) + expect(locale[:plural]).to eq(plural.with_indifferent_access) + + expect(Rails.configuration.assets.precompile).to include("locales/tlh.js") + end + + it "does not register a new locale when the fallback locale does not exist" do + register_locale("bar", name: "Bar", nativeName: "Bar", fallbackLocale: "foo") + expect(DiscoursePluginRegistry.locales.count).to eq(0) + end + + [ + "config/locales/client.foo.yml", + "config/locales/server.foo.yml", + "lib/javascripts/locale/message_format/foo.js", + "lib/javascripts/locale/moment_js/foo.js", + "assets/locales/foo.js.erb" + ].each do |path| + it "does not register a new locale when #{path} is missing" do + path = "#{plugin_path}/#{path}" + File.stubs('exist?').returns(false) + File.stubs('exist?').with(regexp_matches(/#{Regexp.quote(plugin_path)}.*/)).returns(true) + File.stubs('exist?').with(path).returns(false) + + register_locale("foo", name: "Foo", nativeName: "Foo Bar", plural: plural) + expect(DiscoursePluginRegistry.locales.count).to eq(0) + end + end + end + end diff --git a/spec/fixtures/plugins/custom_locales/assets/locales/es_MX.js.erb b/spec/fixtures/plugins/custom_locales/assets/locales/es_MX.js.erb new file mode 100644 index 00000000000..d07d575a09e --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/assets/locales/es_MX.js.erb @@ -0,0 +1,2 @@ +//= require locales/i18n +<%= JsLocaleHelper.output_locale(:es_MX) %> diff --git a/spec/fixtures/plugins/custom_locales/assets/locales/foo.js.erb b/spec/fixtures/plugins/custom_locales/assets/locales/foo.js.erb new file mode 100644 index 00000000000..601dadb7774 --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/assets/locales/foo.js.erb @@ -0,0 +1,2 @@ +//= require locales/i18n +<%= JsLocaleHelper.output_locale(:foo) %> diff --git a/spec/fixtures/plugins/custom_locales/assets/locales/tlh.js.erb b/spec/fixtures/plugins/custom_locales/assets/locales/tlh.js.erb new file mode 100644 index 00000000000..2e3284dc996 --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/assets/locales/tlh.js.erb @@ -0,0 +1,2 @@ +//= require locales/i18n +<%= JsLocaleHelper.output_locale(:tlh) %> diff --git a/spec/fixtures/plugins/custom_locales/config/locales/client.es_MX.yml b/spec/fixtures/plugins/custom_locales/config/locales/client.es_MX.yml new file mode 100644 index 00000000000..c9cb23e960b --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/config/locales/client.es_MX.yml @@ -0,0 +1 @@ +es_MX: diff --git a/spec/fixtures/plugins/custom_locales/config/locales/client.foo.yml b/spec/fixtures/plugins/custom_locales/config/locales/client.foo.yml new file mode 100644 index 00000000000..0152a79652d --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/config/locales/client.foo.yml @@ -0,0 +1 @@ +foo: diff --git a/spec/fixtures/plugins/custom_locales/config/locales/client.tlh.yml b/spec/fixtures/plugins/custom_locales/config/locales/client.tlh.yml new file mode 100644 index 00000000000..884714fb718 --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/config/locales/client.tlh.yml @@ -0,0 +1 @@ +tlh: diff --git a/spec/fixtures/plugins/custom_locales/config/locales/server.es_MX.yml b/spec/fixtures/plugins/custom_locales/config/locales/server.es_MX.yml new file mode 100644 index 00000000000..c9cb23e960b --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/config/locales/server.es_MX.yml @@ -0,0 +1 @@ +es_MX: diff --git a/spec/fixtures/plugins/custom_locales/config/locales/server.foo.yml b/spec/fixtures/plugins/custom_locales/config/locales/server.foo.yml new file mode 100644 index 00000000000..0152a79652d --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/config/locales/server.foo.yml @@ -0,0 +1 @@ +foo: diff --git a/spec/fixtures/plugins/custom_locales/config/locales/server.tlh.yml b/spec/fixtures/plugins/custom_locales/config/locales/server.tlh.yml new file mode 100644 index 00000000000..884714fb718 --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/config/locales/server.tlh.yml @@ -0,0 +1 @@ +tlh: diff --git a/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/foo.js b/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/foo.js new file mode 100644 index 00000000000..79b527258a0 --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/foo.js @@ -0,0 +1 @@ +// this file should contain plural rules diff --git a/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/tlh.js b/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/tlh.js new file mode 100644 index 00000000000..79b527258a0 --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/message_format/tlh.js @@ -0,0 +1 @@ +// this file should contain plural rules diff --git a/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/moment_js/foo.js b/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/moment_js/foo.js new file mode 100644 index 00000000000..971b3b1a1ca --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/lib/javascripts/locale/moment_js/foo.js @@ -0,0 +1 @@ +// this file should contain the locale configuration for moment.js diff --git a/spec/fixtures/plugins/custom_locales/plugin.rb b/spec/fixtures/plugins/custom_locales/plugin.rb new file mode 100644 index 00000000000..700c68cb5f8 --- /dev/null +++ b/spec/fixtures/plugins/custom_locales/plugin.rb @@ -0,0 +1,4 @@ +# name: custom-locales +# about: Fixtures for plugin that adds new locales +# version: 1.0 +# authors: Gerhard Schlager diff --git a/spec/models/locale_site_setting_spec.rb b/spec/models/locale_site_setting_spec.rb index ef81765dc07..06406385eb1 100644 --- a/spec/models/locale_site_setting_spec.rb +++ b/spec/models/locale_site_setting_spec.rb @@ -1,6 +1,15 @@ require 'rails_helper' describe LocaleSiteSetting do + def core_locales + pattern = File.join(Rails.root, 'config', 'locales', 'client.*.yml') + Dir.glob(pattern).map { |x| x.split('.')[-2] } + end + + def native_locale_name(locale) + value = LocaleSiteSetting.values.find { |v| v[:value] == locale } + value[:name] + end describe 'valid_value?' do it 'returns true for a locale that we have translations for' do @@ -14,8 +23,69 @@ describe LocaleSiteSetting do describe 'values' do it 'returns all the locales that we have translations for' do - expect(LocaleSiteSetting.values.map { |x| x[:value] }).to include(*Dir.glob(File.join(Rails.root, 'config', 'locales', 'client.*.yml')).map { |x| x.split('.')[-2] }) + expect(LocaleSiteSetting.values.map { |x| x[:value] }).to include(*core_locales) + end + + it 'returns native names' do + expect(native_locale_name('de')).to eq('Deutsch') + expect(native_locale_name('zh_CN')).to eq('中文') + expect(native_locale_name('zh_TW')).to eq('中文 (TW)') end end + context 'with locales from plugin' do + before do + DiscoursePluginRegistry.register_locale("foo", name: "Foo", nativeName: "Native Foo") + DiscoursePluginRegistry.register_locale("bar", name: "Bar", nativeName: "Native Bar") + DiscoursePluginRegistry.register_locale("de", name: "Renamed German", nativeName: "Native renamed German") + DiscoursePluginRegistry.register_locale("de_AT", name: "German (Austria)", nativeName: "Österreichisch", fallbackLocale: "de") + DiscoursePluginRegistry.register_locale("tlh") + + # Plugins normally register a locale before LocaleSiteSetting is initialized. + # That's not happening in tests, so we need to call reset! + LocaleSiteSetting.reset! + end + + after do + DiscoursePluginRegistry.reset! + end + + describe 'valid_value?' 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) + end + + it 'returns true for locales added by plugins' do + expect(LocaleSiteSetting.valid_value?('foo')).to eq(true) + expect(LocaleSiteSetting.valid_value?('bar')).to eq(true) + end + end + + describe 'values' do + it 'returns native names added by plugin' do + expect(native_locale_name('foo')).to eq('Native Foo') + expect(native_locale_name('bar')).to eq('Native Bar') + end + + it 'does not allow plugins to override native names that exist in core' 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') + end + end + + describe 'fallback_locale' do + it 'returns the fallback locale registered by plugin' do + expect(LocaleSiteSetting.fallback_locale('de_AT')).to eq(:de) + expect(LocaleSiteSetting.fallback_locale(:de_AT)).to eq(:de) + end + + it 'returns nothing when no fallback locale was registered' do + expect(LocaleSiteSetting.fallback_locale('foo')).to be_nil + end + end + end end diff --git a/test/javascripts/lib/i18n-test.js.es6 b/test/javascripts/lib/i18n-test.js.es6 index 314c4cc6938..e653a8d0ca3 100644 --- a/test/javascripts/lib/i18n-test.js.es6 +++ b/test/javascripts/lib/i18n-test.js.es6 @@ -1,17 +1,30 @@ QUnit.module("lib:i18n", { _locale: I18n.locale, + _fallbackLocale: I18n.fallbackLocale, _translations: I18n.translations, beforeEach() { I18n.locale = "fr"; I18n.translations = { + "fr_FOO": { + "js": { + "topic": { + "reply": { + "title": "Foo" + } + }, + } + }, "fr": { "js": { "hello": "Bonjour", "topic": { "reply": { - "title": "Répondre", + "title": "Répondre" + }, + "share": { + "title": "Partager" } }, "character_count": { @@ -56,6 +69,7 @@ QUnit.module("lib:i18n", { afterEach() { I18n.locale = this._locale; + I18n.fallbackLocale = this._fallbackLocale; I18n.translations = this._translations; } }); @@ -92,4 +106,13 @@ QUnit.test("pluralizations", assert => { assert.equal(I18n.t("word_count", { count: 3 }), "3 words"); assert.equal(I18n.t("word_count", { count: 10 }), "10 words"); assert.equal(I18n.t("word_count", { count: 100 }), "100 words"); -}); \ No newline at end of file +}); + +QUnit.test("fallback", assert => { + I18n.locale = "fr_FOO"; + I18n.fallbackLocale = "fr"; + + assert.equal(I18n.t("topic.reply.title"), "Foo", "uses locale translations when they exist"); + assert.equal(I18n.t("topic.share.title"), "Partager", "falls back to fallbackLocale translations when they exist"); + assert.equal(I18n.t("topic.reply.help"), "begin composing a reply to this topic", "falls back to English translations"); +});