Skip to content

Commit 728845d

Browse files
committed
FEATURE: Localization fallbacks (client)
This patch sets I18n.defaultLocale in the Discourse.start() script block (it was formerly always 'en') to SiteSetting.default_locale, and patches translate() to perform fallback to defaultLocale followed by english. Additionally, when enable_verbose_localization() is called, no fallbacks will be performed. It also memoizes the file loading operations in JsLocaleHelper and strips out translations from the fallbacks that are also present in a prefered language, to minimize file size.
1 parent 1851c8d commit 728845d

File tree

3 files changed

+91
-19
lines changed

3 files changed

+91
-19
lines changed

app/assets/javascripts/locales/i18n.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
5252

5353
I18n.fallbackRules = {};
5454

55+
I18n.noFallbacks = false;
56+
5557
I18n.pluralizationRules = {
5658
en: function(n) {
5759
return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other";
@@ -192,6 +194,15 @@ I18n.interpolate = function(message, options) {
192194
I18n.translate = function(scope, options) {
193195
options = this.prepareOptions(options);
194196
var translation = this.lookup(scope, options);
197+
// Fallback to the default locale
198+
if (!translation && this.currentLocale() !== this.defaultLocale && !this.noFallbacks) {
199+
options.locale = this.defaultLocale;
200+
translation = this.lookup(scope, options);
201+
}
202+
if (!translation && this.currentLocale() !== 'en' && !this.noFallbacks) {
203+
options.locale = 'en';
204+
translation = this.lookup(scope, options);
205+
}
195206

196207
try {
197208
if (typeof translation === "object") {
@@ -513,6 +524,7 @@ I18n.enable_verbose_localization = function(){
513524
var keys = {};
514525
var t = I18n.t;
515526

527+
I18n.noFallbacks = true;
516528

517529
I18n.t = I18n.translate = function(scope, value){
518530
var current = keys[scope];

app/views/common/_discourse_javascript.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
Discourse.Environment = '<%= Rails.env %>';
3434
Discourse.SiteSettings = PreloadStore.get('siteSettings');
3535
Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>';
36+
I18n.defaultLocale = '<%= SiteSetting.default_locale %>';
3637
PreloadStore.get("customEmoji").forEach(function(emoji) {
3738
Discourse.Dialect.registerEmoji(emoji.name, emoji.url);
3839
});

lib/js_locale_helper.rb

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,89 @@
11
module JsLocaleHelper
22

3-
def self.output_locale(locale, translations = nil)
3+
def self.load_translations(locale)
4+
@loaded_translations ||= {}
5+
@loaded_translations[locale] ||= begin
6+
locale_str = locale.to_s
7+
8+
# load default translations
9+
translations = YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml"))
10+
# load plugins translations
11+
plugin_translations = {}
12+
Dir["#{Rails.root}/plugins/*/config/locales/client.#{locale_str}.yml"].each do |file|
13+
plugin_translations.deep_merge! YAML::load(File.open(file))
14+
end
15+
16+
# merge translations (plugin translations overwrite default translations)
17+
translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['js']
18+
19+
# We used to split the admin versus the client side, but it's much simpler to just
20+
# include both for now due to the small size of the admin section.
21+
#
22+
# For now, let's leave it split out in the translation file in case we want to split
23+
# it again later, so we'll merge the JSON ourselves.
24+
admin_contents = translations[locale_str].delete('admin_js')
25+
translations[locale_str]['js'].deep_merge!(admin_contents) if admin_contents.present?
26+
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']
27+
28+
translations
29+
end
30+
end
31+
32+
# purpose-built recursive algorithm ahoy!
33+
def self.deep_delete_matches(deleting_from, *checking_hashes)
34+
checking_hashes.compact!
35+
36+
new_hash = deleting_from.dup
37+
deleting_from.each do |key, value|
38+
if value.is_a? Hash
39+
# Recurse
40+
new_at_key = deep_delete_matches(deleting_from[key], *(checking_hashes.map {|h| h[key]}))
41+
if new_at_key.empty?
42+
new_hash.delete key
43+
else
44+
new_hash[key] = new_at_key
45+
end
46+
else
47+
if checking_hashes.any? {|h| h.include? key}
48+
new_hash.delete key
49+
end
50+
end
51+
end
52+
new_hash
53+
end
54+
55+
def self.load_translations_merged(*locales)
56+
@loaded_merges ||= {}
57+
@loaded_merges[locales.join('-')] ||= begin
58+
# TODO - this will need to be reworked to support N fallbacks in the future
59+
all_translations = locales.map { |l| JsLocaleHelper.load_translations l }
60+
merged_translations = {}
61+
merged_translations[locales[0].to_s] = all_translations[0][locales[0].to_s]
62+
if locales[1]
63+
merged_translations[locales[1].to_s] = deep_delete_matches(all_translations[1][locales[1].to_s].dup, merged_translations[locales[0].to_s])
64+
end
65+
if locales[2]
66+
merged_translations[locales[2].to_s] = deep_delete_matches(all_translations[2][locales[2].to_s].dup, merged_translations[locales[0].to_s], merged_translations[locales[1].to_s])
67+
end
68+
merged_translations
69+
end
70+
end
71+
72+
def self.output_locale(locale, request=nil)
473
current_locale = I18n.locale
574
I18n.locale = locale.to_sym
675

776
locale_str = locale.to_s
8-
9-
# load default translations
10-
translations ||= YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml"))
11-
# load plugins translations
12-
plugin_translations = {}
13-
Dir["#{Rails.root}/plugins/*/config/locales/client.#{locale_str}.yml"].each do |file|
14-
plugin_translations.deep_merge! YAML::load(File.open(file))
77+
site_locale = SiteSetting.default_locale.to_sym
78+
79+
if locale == :en
80+
translations = load_translations(locale)
81+
elsif locale == site_locale || site_locale == :en
82+
translations = load_translations_merged(locale, :en)
83+
else
84+
translations = load_translations_merged(locale, site_locale, :en)
1585
end
1686

17-
# merge translations (plugin translations overwrite default translations)
18-
translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['js']
19-
20-
# We used to split the admin versus the client side, but it's much simpler to just
21-
# include both for now due to the small size of the admin section.
22-
#
23-
# For now, let's leave it split out in the translation file in case we want to split
24-
# it again later, so we'll merge the JSON ourselves.
25-
admin_contents = translations[locale_str].delete('admin_js')
26-
translations[locale_str]['js'].deep_merge!(admin_contents) if admin_contents.present?
27-
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']
2887
message_formats = strip_out_message_formats!(translations[locale_str]['js'])
2988

3089
result = generate_message_format(message_formats, locale_str)

0 commit comments

Comments
 (0)