FEATURE: Allow plugins to register a new locale

This commit is contained in:
Gerhard Schlager 2018-01-25 12:09:18 +01:00
parent ba6cd83e3a
commit eb52c5469e
29 changed files with 480 additions and 71 deletions

View File

@ -1,7 +1,5 @@
app/assets/javascripts/env.js app/assets/javascripts/env.js
app/assets/javascripts/main_include.js
app/assets/javascripts/main_include_admin.js app/assets/javascripts/main_include_admin.js
app/assets/javascripts/pagedown_custom.js
app/assets/javascripts/vendor.js app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js app/assets/javascripts/locales/i18n.js
app/assets/javascripts/ember-addons/ app/assets/javascripts/ember-addons/
@ -11,11 +9,9 @@ lib/javascripts/messageformat.js
lib/javascripts/moment.js lib/javascripts/moment.js
lib/javascripts/moment_locale/ lib/javascripts/moment_locale/
lib/highlight_js/ lib/highlight_js/
plugins/**/lib/javascripts/locale
public/javascripts/ public/javascripts/
spec/phantom_js/smoke_test.js
vendor/ vendor/
test/javascripts/test_helper.js test/javascripts/test_helper.js
test/javascripts/test_helper.js
test/javascripts/fixtures test/javascripts/fixtures
test/javascripts/helpers/assertions.js test/javascripts/helpers/assertions.js
app/assets/javascripts/ember-addons/

View File

@ -15,6 +15,7 @@ I18n.pluralizationRules = {
// Set current locale to null // Set current locale to null
I18n.locale = null; I18n.locale = null;
I18n.fallbackLocale = null;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`. // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm; I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
@ -143,6 +144,10 @@ I18n.translate = function(scope, options) {
var translation = this.lookup(scope, options); var translation = this.lookup(scope, options);
if (!this.noFallbacks) { if (!this.noFallbacks) {
if (!translation && this.fallbackLocale) {
options.locale = this.fallbackLocale;
translation = this.lookup(scope, options);
}
if (!translation && this.currentLocale() !== this.defaultLocale) { if (!translation && this.currentLocale() !== this.defaultLocale) {
options.locale = this.defaultLocale; options.locale = this.defaultLocale;
translation = this.lookup(scope, options); translation = this.lookup(scope, options);

View File

@ -7,9 +7,12 @@ class LocaleSiteSetting < EnumSiteSetting
end end
def self.values def self.values
supported_locales.map do |l| @values ||= supported_locales.map do |locale|
lang = language_names[l] || language_names[l[0..1]] lang = language_names[locale] || language_names[locale.split("_")[0]]
{ name: lang ? lang['nativeName'] : l, value: l } {
name: lang ? lang['nativeName'] : locale,
value: locale
}
end end
end end
@ -19,43 +22,41 @@ class LocaleSiteSetting < EnumSiteSetting
return @language_names if @language_names return @language_names if @language_names
@lock.synchronize do @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
end end
def self.supported_locales def self.supported_locales
@lock.synchronize do @lock.synchronize do
@supported_locales ||= begin @supported_locales ||= begin
app_client_files = Dir.glob( locales = Dir.glob(
File.join(Rails.root, 'config', 'locales', 'client.*.yml') File.join(Rails.root, 'config', 'locales', 'client.*.yml')
) ).map { |x| x.split('.')[-2] }
unless ignore_plugins? locales += DiscoursePluginRegistry.locales.keys
app_client_files += Dir.glob( locales.uniq.sort
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
end end
end end
end end
def self.valid_locale?(locale) def self.reset!
assets = Rails.configuration.assets @lock.synchronize do
@values = @language_names = @supported_locales = nil
assets.precompile.grep(/locales\/#{locale}(?:\.js)?/).present? && end
(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?)
end end
def self.ignore_plugins? def self.fallback_locale(locale)
Rails.env.test? && ENV['LOAD_PLUGINS'] != "1" plugin_locale = DiscoursePluginRegistry.locales[locale.to_s]
plugin_locale ? plugin_locale[:fallbackLocale]&.to_sym : nil
end end
private_class_method :valid_locale?
private_class_method :ignore_plugins?
end end

View File

@ -21,7 +21,8 @@ class TranslationOverride < ActiveRecord::Base
data = { value: value } data = { value: value }
if key.end_with?('_MF') 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 end
translation_override = find_or_initialize_by(params) translation_override = find_or_initialize_by(params)

View File

@ -119,9 +119,6 @@ eo:
es: es:
name: Spanish name: Spanish
nativeName: Español nativeName: Español
es_MX:
name: Spanish
nativeName: Español (MX)
et: et:
name: Estonian name: Estonian
nativeName: eesti nativeName: eesti

View File

@ -14,6 +14,7 @@ class DiscoursePluginRegistry
attr_writer :handlebars attr_writer :handlebars
attr_writer :serialized_current_user_fields attr_writer :serialized_current_user_fields
attr_writer :seed_data attr_writer :seed_data
attr_writer :locales
attr_accessor :custom_html attr_accessor :custom_html
def plugins def plugins
@ -65,6 +66,10 @@ class DiscoursePluginRegistry
@seed_data ||= HashWithIndifferentAccess.new({}) @seed_data ||= HashWithIndifferentAccess.new({})
end end
def locales
@locales ||= HashWithIndifferentAccess.new({})
end
def html_builders def html_builders
@html_builders ||= {} @html_builders ||= {}
end end
@ -92,6 +97,10 @@ class DiscoursePluginRegistry
self.class.stylesheets << filename self.class.stylesheets << filename
end end
def self.register_locale(locale, options = {})
self.locales[locale] = options
end
def register_archetype(name, options = {}) def register_archetype(name, options = {})
Archetype.register(name, options) Archetype.register(name, options)
end end
@ -171,6 +180,10 @@ class DiscoursePluginRegistry
result.uniq result.uniq
end end
def locales
self.class.locales
end
def javascripts def javascripts
self.class.javascripts self.class.javascripts
end end
@ -207,6 +220,7 @@ class DiscoursePluginRegistry
self.desktop_stylesheets = nil self.desktop_stylesheets = nil
self.sass_variables = nil self.sass_variables = nil
self.handlebars = nil self.handlebars = nil
self.locales = nil
end end
def self.reset! def self.reset!
@ -222,6 +236,7 @@ class DiscoursePluginRegistry
html_builders.clear html_builders.clear
vendored_pretty_text.clear vendored_pretty_text.clear
seed_path_builders.clear seed_path_builders.clear
locales.clear
end end
def self.setup(plugin_class) def self.setup(plugin_class)

View File

@ -39,6 +39,13 @@ module I18n
if @loaded_locales.empty? if @loaded_locales.empty?
# load all rb files # load all rb files
I18n.backend.load_translations(I18n.load_path.grep(/\.rb$/)) 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 end
# load it # load it

View File

@ -3,7 +3,8 @@ module I18n
# Configure custom fallback order # Configure custom fallback order
class FallbackLocaleList < Hash class FallbackLocaleList < Hash
def [](locale) 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 end
end end

View File

@ -85,6 +85,7 @@ module JsLocaleHelper
end end
def self.load_translations_merged(*locales) def self.load_translations_merged(*locales)
locales = locales.compact
@loaded_merges ||= {} @loaded_merges ||= {}
@loaded_merges[locales.join('-')] ||= begin @loaded_merges[locales.join('-')] ||= begin
all_translations = {} all_translations = {}
@ -104,6 +105,7 @@ module JsLocaleHelper
current_locale = I18n.locale current_locale = I18n.locale
locale_sym = locale_str.to_sym locale_sym = locale_str.to_sym
site_locale = SiteSetting.default_locale.to_sym site_locale = SiteSetting.default_locale.to_sym
fallback_locale = LocaleSiteSetting.fallback_locale(locale_str)
I18n.locale = locale_sym I18n.locale = locale_sym
@ -113,9 +115,9 @@ module JsLocaleHelper
elsif locale_sym == :en elsif locale_sym == :en
load_translations(locale_sym) load_translations(locale_sym)
elsif locale_sym == site_locale || site_locale == :en elsif locale_sym == site_locale || site_locale == :en
load_translations_merged(locale_sym, :en) load_translations_merged(locale_sym, fallback_locale, :en)
else else
load_translations_merged(locale_sym, site_locale, :en) load_translations_merged(locale_sym, fallback_locale, site_locale, :en)
end end
I18n.locale = current_locale I18n.locale = current_locale
@ -125,11 +127,13 @@ module JsLocaleHelper
def self.output_locale(locale) def self.output_locale(locale)
locale_str = locale.to_s locale_str = locale.to_s
fallback_locale_str = LocaleSiteSetting.fallback_locale(locale_str)&.to_s
translations = Marshal.load(Marshal.dump(translations_for(locale_str))) translations = Marshal.load(Marshal.dump(translations_for(locale_str)))
message_formats = strip_out_message_formats!(translations[locale_str]['js']) message_formats = strip_out_message_formats!(translations[locale_str]['js'])
message_formats.merge!(strip_out_message_formats!(translations[locale_str]['admin_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.keys.each do |l|
translations[l].keys.each do |k| translations[l].keys.each do |k|
@ -140,7 +144,8 @@ module JsLocaleHelper
# I18n # I18n
result << "I18n.translations = #{translations.to_json};\n" result << "I18n.translations = #{translations.to_json};\n"
result << "I18n.locale = '#{locale_str}';\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 # moment
result << File.read("#{Rails.root}/lib/javascripts/moment.js") result << File.read("#{Rails.root}/lib/javascripts/moment.js")
@ -150,6 +155,41 @@ module JsLocaleHelper
result result
end 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 def self.moment_formats
result = "" result = ""
result << moment_format_function('short_date_no_year') 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" "moment.fn.#{name.camelize(:lower)} = function(){ return this.format('#{format}'); };\n"
end end
def self.moment_locale(locale_str) def self.moment_locale(locale)
# moment.js uses a different naming scheme for locale files _, filename = find_moment_locale([locale])
locale_str = locale_str.tr('_', '-').downcase filename && File.exist?(filename) ? File.read(filename) << "\n" : ""
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" : ""
end end
def self.generate_message_format(message_formats, locale_str) def self.generate_message_format(message_formats, locale, filename)
formats = message_formats.map { |k, v| k.inspect << " : " << compile_message_format(locale_str, v) }.join(", ") formats = message_formats.map { |k, v| k.inspect << " : " << compile_message_format(filename, locale, v) }.join(", ")
filename = "#{Rails.root}/lib/javascripts/locale/#{locale_str}.js"
filename = "#{Rails.root}/lib/javascripts/locale/en.js" unless File.exists?(filename)
result = "MessageFormat = {locale: {}};\n" result = "MessageFormat = {locale: {}};\n"
result << "I18n._compiledMFs = {#{formats}};\n" result << "I18n._compiledMFs = {#{formats}};\n"
@ -203,10 +233,9 @@ module JsLocaleHelper
end end
end end
def self.compile_message_format(locale, format) def self.compile_message_format(path, locale, format)
with_context do |ctx| with_context do |ctx|
path = "#{Rails.root}/lib/javascripts/locale/#{locale}.js" ctx.load(path) if File.exist?(path)
ctx.load(path) if File.exists?(path)
ctx.eval("mf = new MessageFormat('#{locale}');") ctx.eval("mf = new MessageFormat('#{locale}');")
ctx.eval("mf.precompile(mf.parse(#{format.inspect}))") ctx.eval("mf.precompile(mf.parse(#{format.inspect}))")
end end

View File

@ -29,6 +29,7 @@ class Plugin::Instance
:color_schemes, :color_schemes,
:initializers, :initializers,
:javascripts, :javascripts,
:locales,
:service_workers, :service_workers,
:styles, :styles,
:themes].each do |att| :themes].each do |att|
@ -319,6 +320,14 @@ class Plugin::Instance
javascripts << js javascripts << js
end 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) def register_custom_html(hash)
DiscoursePluginRegistry.custom_html ||= {} DiscoursePluginRegistry.custom_html ||= {}
DiscoursePluginRegistry.custom_html.merge!(hash) DiscoursePluginRegistry.custom_html.merge!(hash)
@ -427,7 +436,7 @@ JS
end end
register_assets! unless assets.blank? register_assets! unless assets.blank?
register_locales!
register_service_workers! register_service_workers!
seed_data.each do |key, value| seed_data.each do |key, value|
@ -532,6 +541,33 @@ JS
end end
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 private
def write_asset(path, contents) def write_asset(path, contents)
@ -553,4 +589,18 @@ JS
yield plugin yield plugin
end 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 end

View File

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

View File

@ -1,6 +1,9 @@
require "rails_helper" require "rails_helper"
describe "translate accelerator" do describe "translate accelerator" do
after do
I18n.reload!
end
it "overrides for both string and symbol keys" do it "overrides for both string and symbol keys" do
key = "user.email.not_allowed" key = "user.email.not_allowed"
@ -32,4 +35,39 @@ describe "translate accelerator" do
end end
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 end

View File

@ -34,12 +34,17 @@ describe JsLocaleHelper do
end end
context "message format" do context "message format" do
def message_format_filename(locale)
Rails.root + "lib/javascripts/locale/#{locale}.js"
end
def setup_message_format(format) def setup_message_format(format)
filename = message_format_filename('en')
compiled = JsLocaleHelper.compile_message_format(filename, 'en', format)
@ctx = MiniRacer::Context.new @ctx = MiniRacer::Context.new
@ctx.eval('MessageFormat = {locale: {}};') @ctx.eval('MessageFormat = {locale: {}};')
@ctx.load(Rails.root + 'lib/javascripts/locale/en.js') @ctx.load(filename)
compiled = JsLocaleHelper.compile_message_format('en', format)
@ctx.eval("var test = #{compiled}") @ctx.eval("var test = #{compiled}")
end end
@ -110,7 +115,7 @@ describe JsLocaleHelper do
end end
it 'load pluralizations rules before precompile' do 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' expect(message).not_to match 'Plural Function not found'
end end
end end

View File

@ -10,8 +10,8 @@ describe Plugin::Instance do
context "find_all" do context "find_all" do
it "can find plugins correctly" do it "can find plugins correctly" do
plugins = Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins") plugins = Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins")
expect(plugins.count).to eq(1) expect(plugins.count).to eq(2)
plugin = plugins[0] plugin = plugins[1]
expect(plugin.name).to eq("plugin-name") expect(plugin.name).to eq("plugin-name")
expect(plugin.path).to eq("#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb") 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) expect(called).to eq(1)
end end
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 end

View File

@ -0,0 +1,2 @@
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:es_MX) %>

View File

@ -0,0 +1,2 @@
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:foo) %>

View File

@ -0,0 +1,2 @@
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:tlh) %>

View File

@ -0,0 +1 @@
es_MX:

View File

@ -0,0 +1 @@
foo:

View File

@ -0,0 +1 @@
tlh:

View File

@ -0,0 +1 @@
es_MX:

View File

@ -0,0 +1 @@
foo:

View File

@ -0,0 +1 @@
tlh:

View File

@ -0,0 +1 @@
// this file should contain plural rules

View File

@ -0,0 +1 @@
// this file should contain plural rules

View File

@ -0,0 +1 @@
// this file should contain the locale configuration for moment.js

View File

@ -0,0 +1,4 @@
# name: custom-locales
# about: Fixtures for plugin that adds new locales
# version: 1.0
# authors: Gerhard Schlager

View File

@ -1,6 +1,15 @@
require 'rails_helper' require 'rails_helper'
describe LocaleSiteSetting do 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 describe 'valid_value?' do
it 'returns true for a locale that we have translations for' do it 'returns true for a locale that we have translations for' do
@ -14,8 +23,69 @@ describe LocaleSiteSetting do
describe 'values' do describe 'values' do
it 'returns all the locales that we have translations for' 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
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 end

View File

@ -1,17 +1,30 @@
QUnit.module("lib:i18n", { QUnit.module("lib:i18n", {
_locale: I18n.locale, _locale: I18n.locale,
_fallbackLocale: I18n.fallbackLocale,
_translations: I18n.translations, _translations: I18n.translations,
beforeEach() { beforeEach() {
I18n.locale = "fr"; I18n.locale = "fr";
I18n.translations = { I18n.translations = {
"fr_FOO": {
"js": {
"topic": {
"reply": {
"title": "Foo"
}
},
}
},
"fr": { "fr": {
"js": { "js": {
"hello": "Bonjour", "hello": "Bonjour",
"topic": { "topic": {
"reply": { "reply": {
"title": "Répondre", "title": "Répondre"
},
"share": {
"title": "Partager"
} }
}, },
"character_count": { "character_count": {
@ -56,6 +69,7 @@ QUnit.module("lib:i18n", {
afterEach() { afterEach() {
I18n.locale = this._locale; I18n.locale = this._locale;
I18n.fallbackLocale = this._fallbackLocale;
I18n.translations = this._translations; I18n.translations = this._translations;
} }
}); });
@ -93,3 +107,12 @@ QUnit.test("pluralizations", assert => {
assert.equal(I18n.t("word_count", { count: 10 }), "10 words"); assert.equal(I18n.t("word_count", { count: 10 }), "10 words");
assert.equal(I18n.t("word_count", { count: 100 }), "100 words"); assert.equal(I18n.t("word_count", { count: 100 }), "100 words");
}); });
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");
});