mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
Merge pull request #3609 from riking/patch-7
FEATURE: Localization fallbacks
This commit is contained in:
commit
9911e92e24
11 changed files with 234 additions and 49 deletions
|
@ -52,6 +52,8 @@ I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
|
|||
|
||||
I18n.fallbackRules = {};
|
||||
|
||||
I18n.noFallbacks = false;
|
||||
|
||||
I18n.pluralizationRules = {
|
||||
en: function(n) {
|
||||
return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other";
|
||||
|
@ -192,6 +194,15 @@ I18n.interpolate = function(message, options) {
|
|||
I18n.translate = function(scope, options) {
|
||||
options = this.prepareOptions(options);
|
||||
var translation = this.lookup(scope, options);
|
||||
// Fallback to the default locale
|
||||
if (!translation && this.currentLocale() !== this.defaultLocale && !this.noFallbacks) {
|
||||
options.locale = this.defaultLocale;
|
||||
translation = this.lookup(scope, options);
|
||||
}
|
||||
if (!translation && this.currentLocale() !== 'en' && !this.noFallbacks) {
|
||||
options.locale = 'en';
|
||||
translation = this.lookup(scope, options);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof translation === "object") {
|
||||
|
@ -513,6 +524,7 @@ I18n.enable_verbose_localization = function(){
|
|||
var keys = {};
|
||||
var t = I18n.t;
|
||||
|
||||
I18n.noFallbacks = true;
|
||||
|
||||
I18n.t = I18n.translate = function(scope, value){
|
||||
var current = keys[scope];
|
||||
|
|
|
@ -155,6 +155,8 @@ class ApplicationController < ActionController::Base
|
|||
else
|
||||
SiteSetting.default_locale
|
||||
end
|
||||
|
||||
I18n.fallbacks.ensure_loaded!
|
||||
end
|
||||
|
||||
def store_preloaded(key, json)
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
Discourse.Environment = '<%= Rails.env %>';
|
||||
Discourse.SiteSettings = PreloadStore.get('siteSettings');
|
||||
Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>';
|
||||
I18n.defaultLocale = '<%= SiteSetting.default_locale %>';
|
||||
PreloadStore.get("customEmoji").forEach(function(emoji) {
|
||||
Discourse.Dialect.registerEmoji(emoji.name, emoji.url);
|
||||
});
|
||||
|
|
|
@ -23,11 +23,6 @@ Discourse::Application.configure do
|
|||
# Specifies the header that your server uses for sending files
|
||||
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation can not be found)
|
||||
config.i18n.fallbacks = true
|
||||
|
||||
|
||||
# you may use other configuration here for mail eg: sendgrid
|
||||
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
|
|
|
@ -24,10 +24,6 @@ Discourse::Application.configure do
|
|||
|
||||
config.log_level = :info
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation can not be found)
|
||||
config.i18n.fallbacks = true
|
||||
|
||||
if GlobalSetting.smtp_address
|
||||
settings = {
|
||||
address: GlobalSetting.smtp_address,
|
||||
|
|
|
@ -27,10 +27,6 @@ Discourse::Application.configure do
|
|||
# Specifies the header that your server uses for sending files
|
||||
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation can not be found)
|
||||
config.i18n.fallbacks = true
|
||||
|
||||
# we recommend you use mailcatcher https://github.com/sj26/mailcatcher
|
||||
config.action_mailer.smtp_settings = { address: "localhost", port: 1025 }
|
||||
|
||||
|
|
35
config/initializers/i18n.rb
Normal file
35
config/initializers/i18n.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# order: after 02-freedom_patches.rb
|
||||
|
||||
# Include pluralization module
|
||||
require 'i18n/backend/pluralization'
|
||||
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
|
||||
|
||||
# Include fallbacks module
|
||||
require 'i18n/backend/fallbacks'
|
||||
I18n.backend.class.send(:include, I18n::Backend::Fallbacks)
|
||||
|
||||
# Configure custom fallback order
|
||||
class FallbackLocaleList < Hash
|
||||
def [](locale)
|
||||
# user locale, site locale, english
|
||||
# TODO - this can be extended to be per-language for a better user experience
|
||||
# (e.g. fallback zh_TW to zh_CN / vice versa)
|
||||
[locale, SiteSetting.default_locale.to_sym, :en].uniq.compact
|
||||
end
|
||||
|
||||
def ensure_loaded!
|
||||
self[I18n.locale].each { |l| I18n.ensure_loaded! l }
|
||||
end
|
||||
end
|
||||
|
||||
class NoFallbackLocaleList < FallbackLocaleList
|
||||
def [](locale)
|
||||
[locale]
|
||||
end
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
I18n.fallbacks = FallbackLocaleList.new
|
||||
else
|
||||
I18n.fallbacks = NoFallbackLocaleList.new
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
require "i18n/backend/pluralization"
|
||||
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
|
|
@ -59,6 +59,11 @@ module I18n
|
|||
end
|
||||
end
|
||||
|
||||
def ensure_loaded!(locale)
|
||||
@loaded_locales ||= []
|
||||
load_locale locale unless @loaded_locales.include?(locale)
|
||||
end
|
||||
|
||||
def translate(key, *args)
|
||||
load_locale(config.locale) unless @loaded_locales.include?(config.locale)
|
||||
return translate_no_cache(key, *args) if args.length > 0
|
||||
|
|
|
@ -1,30 +1,94 @@
|
|||
module JsLocaleHelper
|
||||
|
||||
def self.output_locale(locale, translations = nil)
|
||||
current_locale = I18n.locale
|
||||
I18n.locale = locale.to_sym
|
||||
def self.load_translations(locale)
|
||||
@loaded_translations ||= HashWithIndifferentAccess.new
|
||||
@loaded_translations[locale] ||= begin
|
||||
locale_str = locale.to_s
|
||||
|
||||
# load default translations
|
||||
translations = YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml"))
|
||||
# load plugins translations
|
||||
plugin_translations = {}
|
||||
Dir["#{Rails.root}/plugins/*/config/locales/client.#{locale_str}.yml"].each do |file|
|
||||
plugin_translations.deep_merge! YAML::load(File.open(file))
|
||||
end
|
||||
|
||||
# merge translations (plugin translations overwrite default translations)
|
||||
translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['js']
|
||||
|
||||
# We used to split the admin versus the client side, but it's much simpler to just
|
||||
# include both for now due to the small size of the admin section.
|
||||
#
|
||||
# For now, let's leave it split out in the translation file in case we want to split
|
||||
# it again later, so we'll merge the JSON ourselves.
|
||||
admin_contents = translations[locale_str].delete('admin_js')
|
||||
translations[locale_str]['js'].deep_merge!(admin_contents) if admin_contents.present?
|
||||
translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['admin_js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['admin_js']
|
||||
|
||||
translations
|
||||
end
|
||||
end
|
||||
|
||||
# purpose-built recursive algorithm ahoy!
|
||||
def self.deep_delete_matches(deleting_from, *checking_hashes)
|
||||
checking_hashes.compact!
|
||||
|
||||
new_hash = deleting_from.dup
|
||||
deleting_from.each do |key, value|
|
||||
if value.is_a? Hash
|
||||
# Recurse
|
||||
new_at_key = deep_delete_matches(deleting_from[key], *(checking_hashes.map {|h| h[key]}))
|
||||
if new_at_key.empty?
|
||||
new_hash.delete key
|
||||
else
|
||||
new_hash[key] = new_at_key
|
||||
end
|
||||
else
|
||||
if checking_hashes.any? {|h| h.include? key}
|
||||
new_hash.delete key
|
||||
end
|
||||
end
|
||||
end
|
||||
new_hash
|
||||
end
|
||||
|
||||
def self.load_translations_merged(*locales)
|
||||
@loaded_merges ||= {}
|
||||
@loaded_merges[locales.join('-')] ||= begin
|
||||
all_translations = {}
|
||||
merged_translations = {}
|
||||
loaded_locales = []
|
||||
|
||||
locales.map(&:to_s).each do |locale|
|
||||
all_translations[locale] = JsLocaleHelper.load_translations locale
|
||||
merged_translations[locale] = deep_delete_matches(all_translations[locale][locale], *loaded_locales.map { |l| merged_translations[l] })
|
||||
loaded_locales << locale
|
||||
end
|
||||
merged_translations
|
||||
end
|
||||
end
|
||||
|
||||
def self.output_locale(locale)
|
||||
locale_sym = locale.to_sym
|
||||
locale_str = locale.to_s
|
||||
|
||||
# load default translations
|
||||
translations ||= YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml"))
|
||||
# load plugins translations
|
||||
plugin_translations = {}
|
||||
Dir["#{Rails.root}/plugins/*/config/locales/client.#{locale_str}.yml"].each do |file|
|
||||
plugin_translations.deep_merge! YAML::load(File.open(file))
|
||||
current_locale = I18n.locale
|
||||
I18n.locale = locale_sym
|
||||
|
||||
site_locale = SiteSetting.default_locale.to_sym
|
||||
|
||||
if Rails.env.development?
|
||||
translations = load_translations(locale_sym)
|
||||
else
|
||||
if locale_sym == :en
|
||||
translations = load_translations(locale_sym)
|
||||
elsif locale_sym == site_locale || site_locale == :en
|
||||
translations = load_translations_merged(locale_sym, :en)
|
||||
else
|
||||
translations = load_translations_merged(locale_sym, site_locale, :en)
|
||||
end
|
||||
end
|
||||
|
||||
# merge translations (plugin translations overwrite default translations)
|
||||
translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['js']
|
||||
|
||||
# We used to split the admin versus the client side, but it's much simpler to just
|
||||
# include both for now due to the small size of the admin section.
|
||||
#
|
||||
# For now, let's leave it split out in the translation file in case we want to split
|
||||
# it again later, so we'll merge the JSON ourselves.
|
||||
admin_contents = translations[locale_str].delete('admin_js')
|
||||
translations[locale_str]['js'].deep_merge!(admin_contents) if admin_contents.present?
|
||||
translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['admin_js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['admin_js']
|
||||
message_formats = strip_out_message_formats!(translations[locale_str]['js'])
|
||||
|
||||
result = generate_message_format(message_formats, locale_str)
|
||||
|
|
|
@ -2,6 +2,24 @@ require 'spec_helper'
|
|||
require_dependency 'js_locale_helper'
|
||||
|
||||
describe JsLocaleHelper do
|
||||
|
||||
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
|
||||
|
||||
after do
|
||||
JsLocaleHelper.clear_cache!
|
||||
end
|
||||
|
||||
it 'should be able to generate translations' do
|
||||
expect(JsLocaleHelper.output_locale('en').length).to be > 0
|
||||
end
|
||||
|
@ -57,21 +75,23 @@ describe JsLocaleHelper do
|
|||
it 'handles message format special keys' do
|
||||
ctx = V8::Context.new
|
||||
ctx.eval("I18n = {};")
|
||||
ctx.eval(JsLocaleHelper.output_locale('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 {#}}"
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
expect(ctx.eval('I18n.translations')["en"]["js"]["hello"]).to eq("world")
|
||||
expect(ctx.eval('I18n.translations')["en"]["js"]["test_MF"]).to eq(nil)
|
||||
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 {#}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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/)
|
||||
|
@ -84,6 +104,67 @@ describe JsLocaleHelper do
|
|||
expect(message).not_to match 'Plural Function not found'
|
||||
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" => "2-ru",
|
||||
"english_and_site" => "3-ru",
|
||||
"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
|
||||
|
||||
ctx = V8::Context.new
|
||||
ctx.eval('var window = this;')
|
||||
ctx.load(Rails.root + 'app/assets/javascripts/locales/i18n.js')
|
||||
ctx.eval(JsLocaleHelper.output_locale(I18n.locale))
|
||||
ctx.eval('I18n.defaultLocale = "ru";')
|
||||
|
||||
# Test - unneeded translations are not emitted
|
||||
expect(ctx.eval('I18n.translations.en.js').keys).to eq(["only_english"])
|
||||
expect(ctx.eval('I18n.translations.ru.js').keys).to eq(["only_site", "english_and_site"])
|
||||
|
||||
expected.each do |key, expect|
|
||||
expect(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])
|
||||
|
|
Loading…
Reference in a new issue