i18n-js 3.3.0 → 3.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.editorconfig +1 -1
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/tests.yaml +106 -0
- data/Appraisals +16 -0
- data/CHANGELOG.md +153 -4
- data/README.md +505 -333
- data/app/assets/javascripts/i18n/shims.js +35 -3
- data/app/assets/javascripts/i18n.js +37 -33
- data/gemfiles/i18n_1_10.gemfile +7 -0
- data/gemfiles/i18n_1_7.gemfile +7 -0
- data/gemfiles/i18n_1_8.gemfile +7 -0
- data/gemfiles/i18n_1_9.gemfile +7 -0
- data/i18n-js.gemspec +2 -2
- data/i18njs.png +0 -0
- data/lib/i18n/js/dependencies.rb +6 -2
- data/lib/i18n/js/engine.rb +1 -1
- data/lib/i18n/js/formatters/base.rb +3 -1
- data/lib/i18n/js/formatters/js.rb +12 -4
- data/lib/i18n/js/middleware.rb +1 -1
- data/lib/i18n/js/private/config_store.rb +31 -0
- data/lib/i18n/js/segment.rb +9 -3
- data/lib/i18n/js/utils.rb +34 -8
- data/lib/i18n/js/version.rb +1 -1
- data/lib/i18n/js.rb +25 -10
- data/package.json +2 -2
- data/spec/fixtures/js_available_locales_custom.yml +1 -0
- data/spec/fixtures/{js_file_with_namespace_and_pretty_print.yml → js_file_with_namespace_prefix_and_pretty_print.yml} +2 -0
- data/spec/fixtures/locales.yml +38 -0
- data/spec/fixtures/merge_plurals_with_no_overrides.yml +4 -0
- data/spec/fixtures/merge_plurals_with_partial_overrides.yml +4 -0
- data/spec/fixtures/millions.yml +4 -0
- data/spec/js/dates.spec.js +1 -0
- data/spec/js/json_parsable.spec.js +14 -0
- data/spec/js/localization.spec.js +38 -14
- data/spec/js/numbers.spec.js +4 -0
- data/spec/js/pluralization.spec.js +19 -2
- data/spec/js/require.js +4 -4
- data/spec/js/translate.spec.js +68 -48
- data/spec/js/translations.js +27 -2
- data/spec/ruby/i18n/js/segment_spec.rb +75 -8
- data/spec/ruby/i18n/js/utils_spec.rb +32 -0
- data/spec/ruby/i18n/js_spec.rb +169 -35
- data/spec/spec_helper.rb +1 -0
- data/yarn.lock +32 -25
- metadata +26 -9
- data/.travis.yml +0 -37
@@ -33,7 +33,7 @@ if (!Array.prototype.indexOf) {
|
|
33
33
|
}
|
34
34
|
|
35
35
|
// Production steps of ECMA-262, Edition 5, 15.4.4.18
|
36
|
-
// Reference:
|
36
|
+
// Reference: https://es5.github.com/#x15.4.4.18
|
37
37
|
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
|
38
38
|
if ( !Array.prototype.forEach ) {
|
39
39
|
|
@@ -53,7 +53,7 @@ if ( !Array.prototype.forEach ) {
|
|
53
53
|
var len = O.length >>> 0; // Hack to convert O.length to a UInt32
|
54
54
|
|
55
55
|
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
56
|
-
// See:
|
56
|
+
// See: https://es5.github.com/#x9.11
|
57
57
|
if ( {}.toString.call(callback) !== "[object Function]" ) {
|
58
58
|
throw new TypeError( callback + " is not a function" );
|
59
59
|
}
|
@@ -139,7 +139,7 @@ if (!Array.prototype.map) {
|
|
139
139
|
var len = O.length >>> 0;
|
140
140
|
|
141
141
|
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
142
|
-
// See:
|
142
|
+
// See: https://es5.github.com/#x9.11
|
143
143
|
if (typeof callback !== 'function') {
|
144
144
|
throw new TypeError(callback + ' is not a function');
|
145
145
|
}
|
@@ -206,3 +206,35 @@ if (!Array.prototype.map) {
|
|
206
206
|
return A;
|
207
207
|
};
|
208
208
|
}
|
209
|
+
|
210
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind
|
211
|
+
if (!Function.prototype.bind) (function(){
|
212
|
+
var ArrayPrototypeSlice = Array.prototype.slice;
|
213
|
+
Function.prototype.bind = function(otherThis) {
|
214
|
+
if (typeof this !== 'function') {
|
215
|
+
// closest thing possible to the ECMAScript 5
|
216
|
+
// internal IsCallable function
|
217
|
+
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
|
218
|
+
}
|
219
|
+
|
220
|
+
var baseArgs= ArrayPrototypeSlice .call(arguments, 1),
|
221
|
+
baseArgsLength = baseArgs.length,
|
222
|
+
fToBind = this,
|
223
|
+
fNOP = function() {},
|
224
|
+
fBound = function() {
|
225
|
+
baseArgs.length = baseArgsLength; // reset to default base arguments
|
226
|
+
baseArgs.push.apply(baseArgs, arguments);
|
227
|
+
return fToBind.apply(
|
228
|
+
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
|
229
|
+
);
|
230
|
+
};
|
231
|
+
|
232
|
+
if (this.prototype) {
|
233
|
+
// Function.prototype doesn't have a prototype property
|
234
|
+
fNOP.prototype = this.prototype;
|
235
|
+
}
|
236
|
+
fBound.prototype = new fNOP();
|
237
|
+
|
238
|
+
return fBound;
|
239
|
+
};
|
240
|
+
})();
|
@@ -269,23 +269,23 @@
|
|
269
269
|
}
|
270
270
|
|
271
271
|
// Locale code format 1:
|
272
|
-
// According to RFC4646 (
|
272
|
+
// According to RFC4646 (https://www.ietf.org/rfc/rfc4646.txt)
|
273
273
|
// language codes for Traditional Chinese should be `zh-Hant`
|
274
274
|
//
|
275
275
|
// But due to backward compatibility
|
276
276
|
// We use older version of IETF language tag
|
277
|
-
// @see
|
278
|
-
// @see
|
277
|
+
// @see https://www.w3.org/TR/html401/struct/dirlang.html
|
278
|
+
// @see https://en.wikipedia.org/wiki/IETF_language_tag
|
279
279
|
//
|
280
280
|
// Format: `language-code = primary-code ( "-" subcode )*`
|
281
281
|
//
|
282
282
|
// primary-code uses ISO639-1
|
283
|
-
// @see
|
284
|
-
// @see
|
283
|
+
// @see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
284
|
+
// @see https://www.iso.org/iso/home/standards/language_codes.htm
|
285
285
|
//
|
286
286
|
// subcode uses ISO 3166-1 alpha-2
|
287
|
-
// @see
|
288
|
-
// @see
|
287
|
+
// @see https://en.wikipedia.org/wiki/ISO_3166
|
288
|
+
// @see https://www.iso.org/iso/country_codes.htm
|
289
289
|
//
|
290
290
|
// @note
|
291
291
|
// subcode can be in upper case or lower case
|
@@ -402,7 +402,7 @@
|
|
402
402
|
|
403
403
|
while (locales.length) {
|
404
404
|
locale = locales.shift();
|
405
|
-
scopes = fullScope.split(this.defaultSeparator);
|
405
|
+
scopes = fullScope.split(options.separator || this.defaultSeparator);
|
406
406
|
translations = this.translations[locale];
|
407
407
|
|
408
408
|
if (!translations) {
|
@@ -433,7 +433,7 @@
|
|
433
433
|
, pluralizerKey
|
434
434
|
, message;
|
435
435
|
|
436
|
-
if (isObject(translations)) {
|
436
|
+
if (translations && isObject(translations)) {
|
437
437
|
while (pluralizerKeys.length) {
|
438
438
|
pluralizerKey = pluralizerKeys.shift();
|
439
439
|
if (isSet(translations[pluralizerKey])) {
|
@@ -459,7 +459,7 @@
|
|
459
459
|
|
460
460
|
while (locales.length) {
|
461
461
|
locale = locales.shift();
|
462
|
-
scopes = scope.split(this.defaultSeparator);
|
462
|
+
scopes = scope.split(options.separator || this.defaultSeparator);
|
463
463
|
translations = this.translations[locale];
|
464
464
|
|
465
465
|
if (!translations) {
|
@@ -679,13 +679,13 @@
|
|
679
679
|
var s = scope.split('.').slice(-1)[0];
|
680
680
|
//replace underscore with space && camelcase with space and lowercase letter
|
681
681
|
return (this.missingTranslationPrefix.length > 0 ? this.missingTranslationPrefix : '') +
|
682
|
-
s.replace(
|
682
|
+
s.replace(/_/g,' ').replace(/([a-z])([A-Z])/g,
|
683
683
|
function(match, p1, p2) {return p1 + ' ' + p2.toLowerCase()} );
|
684
684
|
}
|
685
685
|
|
686
686
|
var localeForTranslation = (options != null && options.locale != null) ? options.locale : this.currentLocale();
|
687
687
|
var fullScope = this.getFullScope(scope, options);
|
688
|
-
var fullScopeWithLocale = [localeForTranslation, fullScope].join(this.defaultSeparator);
|
688
|
+
var fullScopeWithLocale = [localeForTranslation, fullScope].join(options.separator || this.defaultSeparator);
|
689
689
|
|
690
690
|
return '[missing "' + fullScopeWithLocale + '" translation]';
|
691
691
|
};
|
@@ -779,8 +779,8 @@
|
|
779
779
|
I18n.toCurrency = function(number, options) {
|
780
780
|
options = this.prepareOptions(
|
781
781
|
options
|
782
|
-
, this.lookup("number.currency.format")
|
783
|
-
, this.lookup("number.format")
|
782
|
+
, this.lookup("number.currency.format", options)
|
783
|
+
, this.lookup("number.format", options)
|
784
784
|
, CURRENCY_FORMAT
|
785
785
|
);
|
786
786
|
|
@@ -799,17 +799,17 @@
|
|
799
799
|
|
800
800
|
switch (scope) {
|
801
801
|
case "currency":
|
802
|
-
return this.toCurrency(value);
|
802
|
+
return this.toCurrency(value, options);
|
803
803
|
case "number":
|
804
|
-
scope = this.lookup("number.format");
|
804
|
+
scope = this.lookup("number.format", options);
|
805
805
|
return this.toNumber(value, scope);
|
806
806
|
case "percentage":
|
807
|
-
return this.toPercentage(value);
|
807
|
+
return this.toPercentage(value, options);
|
808
808
|
default:
|
809
809
|
var localizedValue;
|
810
810
|
|
811
811
|
if (scope.match(/^(date|time)/)) {
|
812
|
-
localizedValue = this.toTime(scope, value);
|
812
|
+
localizedValue = this.toTime(scope, value, options);
|
813
813
|
} else {
|
814
814
|
localizedValue = value.toString();
|
815
815
|
}
|
@@ -897,7 +897,7 @@
|
|
897
897
|
// %d - Day of the month (01..31)
|
898
898
|
// %-d - Day of the month (1..31)
|
899
899
|
// %H - Hour of the day, 24-hour clock (00..23)
|
900
|
-
// %-H
|
900
|
+
// %-H/%k - Hour of the day, 24-hour clock (0..23)
|
901
901
|
// %I - Hour of the day, 12-hour clock (01..12)
|
902
902
|
// %-I/%l - Hour of the day, 12-hour clock (1..12)
|
903
903
|
// %m - Month of the year (01..12)
|
@@ -914,8 +914,8 @@
|
|
914
914
|
// %Y - Year with century
|
915
915
|
// %z/%Z - Timezone offset (+0545)
|
916
916
|
//
|
917
|
-
I18n.strftime = function(date, format) {
|
918
|
-
var options = this.lookup("date")
|
917
|
+
I18n.strftime = function(date, format, options) {
|
918
|
+
var options = this.lookup("date", options)
|
919
919
|
, meridianOptions = I18n.meridian()
|
920
920
|
;
|
921
921
|
|
@@ -961,6 +961,7 @@
|
|
961
961
|
format = format.replace("%-d", day);
|
962
962
|
format = format.replace("%H", padding(hour));
|
963
963
|
format = format.replace("%-H", hour);
|
964
|
+
format = format.replace("%k", hour);
|
964
965
|
format = format.replace("%I", padding(hour12));
|
965
966
|
format = format.replace("%-I", hour12);
|
966
967
|
format = format.replace("%l", hour12);
|
@@ -983,9 +984,9 @@
|
|
983
984
|
};
|
984
985
|
|
985
986
|
// Convert the given dateString into a formatted date.
|
986
|
-
I18n.toTime = function(scope, dateString) {
|
987
|
+
I18n.toTime = function(scope, dateString, options) {
|
987
988
|
var date = this.parseDate(dateString)
|
988
|
-
, format = this.lookup(scope)
|
989
|
+
, format = this.lookup(scope, options)
|
989
990
|
;
|
990
991
|
|
991
992
|
// A date input of `null` or `undefined` will be returned as-is
|
@@ -1002,15 +1003,15 @@
|
|
1002
1003
|
return date_string;
|
1003
1004
|
}
|
1004
1005
|
|
1005
|
-
return this.strftime(date, format);
|
1006
|
+
return this.strftime(date, format, options);
|
1006
1007
|
};
|
1007
1008
|
|
1008
1009
|
// Convert a number into a formatted percentage value.
|
1009
1010
|
I18n.toPercentage = function(number, options) {
|
1010
1011
|
options = this.prepareOptions(
|
1011
1012
|
options
|
1012
|
-
, this.lookup("number.percentage.format")
|
1013
|
-
, this.lookup("number.format")
|
1013
|
+
, this.lookup("number.percentage.format", options)
|
1014
|
+
, this.lookup("number.format", options)
|
1014
1015
|
, PERCENTAGE_FORMAT
|
1015
1016
|
);
|
1016
1017
|
|
@@ -1024,6 +1025,7 @@
|
|
1024
1025
|
, iterations = 0
|
1025
1026
|
, unit
|
1026
1027
|
, precision
|
1028
|
+
, fullScope
|
1027
1029
|
;
|
1028
1030
|
|
1029
1031
|
while (size >= kb && iterations < 4) {
|
@@ -1032,10 +1034,12 @@
|
|
1032
1034
|
}
|
1033
1035
|
|
1034
1036
|
if (iterations === 0) {
|
1035
|
-
|
1037
|
+
fullScope = this.getFullScope("number.human.storage_units.units.byte", options);
|
1038
|
+
unit = this.t(fullScope, {count: size});
|
1036
1039
|
precision = 0;
|
1037
1040
|
} else {
|
1038
|
-
|
1041
|
+
fullScope = this.getFullScope("number.human.storage_units.units." + SIZE_UNITS[iterations], options);
|
1042
|
+
unit = this.t(fullScope);
|
1039
1043
|
precision = (size - Math.floor(size) === 0) ? 0 : 1;
|
1040
1044
|
}
|
1041
1045
|
|
@@ -1052,7 +1056,7 @@
|
|
1052
1056
|
|
1053
1057
|
// Deal with the scope as an array.
|
1054
1058
|
if (isArray(scope)) {
|
1055
|
-
scope = scope.join(this.defaultSeparator);
|
1059
|
+
scope = scope.join(options.separator || this.defaultSeparator);
|
1056
1060
|
}
|
1057
1061
|
|
1058
1062
|
// Deal with the scope option provided through the second argument.
|
@@ -1060,7 +1064,7 @@
|
|
1060
1064
|
// I18n.t('hello', {scope: 'greetings'});
|
1061
1065
|
//
|
1062
1066
|
if (options.scope) {
|
1063
|
-
scope = [options.scope, scope].join(this.defaultSeparator);
|
1067
|
+
scope = [options.scope, scope].join(options.separator || this.defaultSeparator);
|
1064
1068
|
}
|
1065
1069
|
|
1066
1070
|
return scope;
|
@@ -1083,9 +1087,9 @@
|
|
1083
1087
|
};
|
1084
1088
|
|
1085
1089
|
// Set aliases, so we can save some typing.
|
1086
|
-
I18n.t = I18n.translate;
|
1087
|
-
I18n.l = I18n.localize;
|
1088
|
-
I18n.p = I18n.pluralize;
|
1090
|
+
I18n.t = I18n.translate.bind(I18n);
|
1091
|
+
I18n.l = I18n.localize.bind(I18n);
|
1092
|
+
I18n.p = I18n.pluralize.bind(I18n);
|
1089
1093
|
|
1090
1094
|
return I18n;
|
1091
1095
|
}));
|
data/i18n-js.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Nando Vieira"]
|
10
10
|
s.email = ["fnando.vieira@gmail.com"]
|
11
|
-
s.homepage = "
|
11
|
+
s.homepage = "https://rubygems.org/gems/i18n-js"
|
12
12
|
s.summary = "It's a small library to provide the Rails I18n translations on the Javascript."
|
13
13
|
s.description = s.summary
|
14
14
|
s.license = "MIT"
|
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
|
21
21
|
s.add_dependency "i18n", ">= 0.6.6"
|
22
22
|
|
23
|
-
s.add_development_dependency "appraisal", "~> 2.
|
23
|
+
s.add_development_dependency "appraisal", "~> 2.3"
|
24
24
|
s.add_development_dependency "rspec", "~> 3.0"
|
25
25
|
s.add_development_dependency "rake", "~> 12.0"
|
26
26
|
s.add_development_dependency "gem-release", ">= 0.7"
|
data/i18njs.png
ADDED
Binary file
|
data/lib/i18n/js/dependencies.rb
CHANGED
@@ -18,7 +18,7 @@ module I18n
|
|
18
18
|
# Call this in an initializer
|
19
19
|
def using_asset_pipeline?
|
20
20
|
assets_pipeline_available =
|
21
|
-
(rails3? || rails4? || rails5? || rails6?) &&
|
21
|
+
(rails3? || rails4? || rails5? || rails6? || rails7?) &&
|
22
22
|
Rails.respond_to?(:application) &&
|
23
23
|
Rails.application.config.respond_to?(:assets)
|
24
24
|
rails3_assets_enabled =
|
@@ -26,7 +26,7 @@ module I18n
|
|
26
26
|
assets_pipeline_available &&
|
27
27
|
Rails.application.config.assets.enabled != false
|
28
28
|
|
29
|
-
assets_pipeline_available && (rails4? || rails5? || rails6? || rails3_assets_enabled)
|
29
|
+
assets_pipeline_available && (rails4? || rails5? || rails6? || rails7? || rails3_assets_enabled)
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
@@ -46,6 +46,10 @@ module I18n
|
|
46
46
|
def rails6?
|
47
47
|
rails? && Rails.version.to_i == 6
|
48
48
|
end
|
49
|
+
|
50
|
+
def rails7?
|
51
|
+
rails? && Rails.version.to_i == 7
|
52
|
+
end
|
49
53
|
|
50
54
|
def safe_gem_check(*args)
|
51
55
|
if Gem::Specification.respond_to?(:find_by_name)
|
data/lib/i18n/js/engine.rb
CHANGED
@@ -21,7 +21,7 @@ module I18n
|
|
21
21
|
# https://github.com/rails/sprockets-rails/blob/master/lib/sprockets/railtie.rb
|
22
22
|
#
|
23
23
|
# Finisher hook is the place which should be used as border.
|
24
|
-
#
|
24
|
+
# https://guides.rubyonrails.org/configuring.html#initializers
|
25
25
|
#
|
26
26
|
# For detail see Pull Request:
|
27
27
|
# https://github.com/fnando/i18n-js/pull/371
|
@@ -2,10 +2,12 @@ module I18n
|
|
2
2
|
module JS
|
3
3
|
module Formatters
|
4
4
|
class Base
|
5
|
-
def initialize(js_extend: false, namespace: nil, pretty_print: false)
|
5
|
+
def initialize(js_extend: false, namespace: nil, pretty_print: false, prefix: nil, suffix: nil)
|
6
6
|
@js_extend = js_extend
|
7
7
|
@namespace = namespace
|
8
8
|
@pretty_print = pretty_print
|
9
|
+
@prefix = prefix
|
10
|
+
@suffix = suffix
|
9
11
|
end
|
10
12
|
|
11
13
|
protected
|
@@ -4,25 +4,33 @@ module I18n
|
|
4
4
|
module JS
|
5
5
|
module Formatters
|
6
6
|
class JS < Base
|
7
|
+
JSON_ESCAPE_MAP = {
|
8
|
+
"'" => "\\'",
|
9
|
+
"\\" => "\\\\"
|
10
|
+
}.freeze
|
11
|
+
private_constant :JSON_ESCAPE_MAP
|
12
|
+
|
7
13
|
def format(translations)
|
8
14
|
contents = header
|
9
15
|
translations.each do |locale, translations_for_locale|
|
10
16
|
contents << line(locale, format_json(translations_for_locale))
|
11
17
|
end
|
12
|
-
contents
|
18
|
+
contents << (@suffix || '')
|
13
19
|
end
|
14
20
|
|
15
21
|
protected
|
16
22
|
|
17
23
|
def header
|
18
|
-
|
24
|
+
text = @prefix || ''
|
25
|
+
text + %(#{@namespace}.translations || (#{@namespace}.translations = {});\n)
|
19
26
|
end
|
20
27
|
|
21
28
|
def line(locale, translations)
|
29
|
+
json_literal = @pretty_print ? translations : %(JSON.parse('#{translations.gsub(/#{Regexp.union(JSON_ESCAPE_MAP.keys)}/){|match| JSON_ESCAPE_MAP.fetch(match) }}'))
|
22
30
|
if @js_extend
|
23
|
-
%(#{@namespace}.translations["#{locale}"] = I18n.extend((#{@namespace}.translations["#{locale}"] || {}), #{
|
31
|
+
%(#{@namespace}.translations["#{locale}"] = I18n.extend((#{@namespace}.translations["#{locale}"] || {}), #{json_literal});\n)
|
24
32
|
else
|
25
|
-
%(#{@namespace}.translations["#{locale}"] = #{
|
33
|
+
%(#{@namespace}.translations["#{locale}"] = #{json_literal};\n)
|
26
34
|
end
|
27
35
|
end
|
28
36
|
end
|
data/lib/i18n/js/middleware.rb
CHANGED
@@ -45,7 +45,7 @@ module I18n
|
|
45
45
|
|
46
46
|
def save_cache(new_cache)
|
47
47
|
# path could be a symbolic link
|
48
|
-
FileUtils.mkdir_p(cache_dir) unless File.
|
48
|
+
FileUtils.mkdir_p(cache_dir) unless File.exist?(cache_dir)
|
49
49
|
File.open(cache_path, "w+") do |file|
|
50
50
|
file << new_cache.to_yaml
|
51
51
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module I18n
|
6
|
+
module JS
|
7
|
+
# @api private
|
8
|
+
module Private
|
9
|
+
# Caching implementation for I18n::JS.config
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class ConfigStore
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
def fetch
|
16
|
+
return @config if @config
|
17
|
+
|
18
|
+
yield.tap do |obj|
|
19
|
+
raise ArgumentError, "unexpected falsy object from block" unless obj
|
20
|
+
|
21
|
+
@config = obj
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def flush_cache
|
26
|
+
@config = nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/i18n/js/segment.rb
CHANGED
@@ -7,7 +7,7 @@ module I18n
|
|
7
7
|
|
8
8
|
# Class which enscapulates a translations hash and outputs a single JSON translation file
|
9
9
|
class Segment
|
10
|
-
OPTIONS = [:namespace, :pretty_print, :js_extend, :sort_translation_keys, :json_only].freeze
|
10
|
+
OPTIONS = [:namespace, :pretty_print, :js_extend, :prefix, :suffix, :sort_translation_keys, :json_only].freeze
|
11
11
|
LOCALE_INTERPOLATOR = /%\{locale\}/
|
12
12
|
|
13
13
|
attr_reader *([:file, :translations] | OPTIONS)
|
@@ -24,6 +24,8 @@ module I18n
|
|
24
24
|
@namespace = options[:namespace] || 'I18n'
|
25
25
|
@pretty_print = !!options[:pretty_print]
|
26
26
|
@js_extend = options.key?(:js_extend) ? !!options[:js_extend] : true
|
27
|
+
@prefix = options.key?(:prefix) ? options[:prefix] : nil
|
28
|
+
@suffix = options.key?(:suffix) ? options[:suffix] : nil
|
27
29
|
@sort_translation_keys = options.key?(:sort_translation_keys) ? !!options[:sort_translation_keys] : true
|
28
30
|
@json_only = options.key?(:json_only) ? !!options[:json_only] : false
|
29
31
|
end
|
@@ -31,7 +33,7 @@ module I18n
|
|
31
33
|
# Saves JSON file containing translations
|
32
34
|
def save!
|
33
35
|
if @file =~ LOCALE_INTERPOLATOR
|
34
|
-
I18n.
|
36
|
+
I18n::JS.js_available_locales.each do |locale|
|
35
37
|
write_file(file_for_locale(locale), @translations.slice(locale))
|
36
38
|
end
|
37
39
|
else
|
@@ -48,6 +50,7 @@ module I18n
|
|
48
50
|
def write_file(_file = @file, _translations = @translations)
|
49
51
|
FileUtils.mkdir_p File.dirname(_file)
|
50
52
|
_translations = Utils.deep_key_sort(_translations) if @sort_translation_keys
|
53
|
+
_translations = Utils.deep_remove_procs(_translations)
|
51
54
|
contents = formatter.format(_translations)
|
52
55
|
|
53
56
|
return if File.exist?(_file) && File.read(_file) == contents
|
@@ -68,7 +71,10 @@ module I18n
|
|
68
71
|
def formatter_options
|
69
72
|
{ js_extend: @js_extend,
|
70
73
|
namespace: @namespace,
|
71
|
-
pretty_print: @pretty_print
|
74
|
+
pretty_print: @pretty_print,
|
75
|
+
prefix: @prefix,
|
76
|
+
suffix: @suffix
|
77
|
+
}
|
72
78
|
end
|
73
79
|
end
|
74
80
|
end
|
data/lib/i18n/js/utils.rb
CHANGED
@@ -3,20 +3,25 @@ module I18n
|
|
3
3
|
module Utils
|
4
4
|
PLURAL_KEYS = %i[zero one two few many other].freeze
|
5
5
|
|
6
|
-
# Based on deep_merge by Stefan Rusterholz, see <
|
6
|
+
# Based on deep_merge by Stefan Rusterholz, see <https://www.ruby-forum.com/topic/142809>.
|
7
7
|
# This method is used to handle I18n fallbacks. Given two equivalent path nodes in two locale trees:
|
8
8
|
# 1. If the node in the current locale appears to be an I18n pluralization (:one, :other, etc.),
|
9
|
-
# use the node
|
10
|
-
# 2. Else if both nodes are Hashes, combine (merge) the key-value pairs of the two nodes into one,
|
9
|
+
# use the node, but merge in any missing/non-nil keys from the fallback (default) locale.
|
10
|
+
# 2. Else if both nodes are Hashes, combine (merge) the key-value pairs of the two nodes into one,
|
11
11
|
# prioritizing the current locale.
|
12
12
|
# 3. Else if either node is nil, use the other node.
|
13
|
+
PLURAL_MERGER = proc do |_key, v1, v2|
|
14
|
+
v1 || v2
|
15
|
+
end
|
13
16
|
MERGER = proc do |_key, v1, v2|
|
14
|
-
if Hash ===
|
15
|
-
v2
|
16
|
-
|
17
|
-
|
17
|
+
if Hash === v1 && Hash === v2
|
18
|
+
if (v2.keys - PLURAL_KEYS).empty?
|
19
|
+
slice(v2.merge(v1, &PLURAL_MERGER), v2.keys)
|
20
|
+
else
|
21
|
+
v1.merge(v2, &MERGER)
|
22
|
+
end
|
18
23
|
else
|
19
|
-
v2
|
24
|
+
v2 || v1
|
20
25
|
end
|
21
26
|
end
|
22
27
|
|
@@ -24,6 +29,14 @@ module I18n
|
|
24
29
|
v.kind_of?(Hash) ? (v.delete_if(&HASH_NIL_VALUE_CLEANER_PROC); false) : v.nil?
|
25
30
|
end
|
26
31
|
|
32
|
+
def self.slice(hash, keys)
|
33
|
+
if hash.respond_to?(:slice) # ruby 2.5 onwards
|
34
|
+
hash.slice(*keys)
|
35
|
+
else
|
36
|
+
hash.select {|key, _| keys.include?(key)}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
27
40
|
def self.strip_keys_with_nil_values(hash)
|
28
41
|
hash.dup.delete_if(&HASH_NIL_VALUE_CLEANER_PROC)
|
29
42
|
end
|
@@ -60,6 +73,19 @@ module I18n
|
|
60
73
|
seed[key] = value.is_a?(Hash) ? deep_key_sort(value) : value
|
61
74
|
end
|
62
75
|
end
|
76
|
+
|
77
|
+
def self.deep_remove_procs(hash)
|
78
|
+
# procs exist in `i18n.plural.rule` as pluralizer
|
79
|
+
# But having it in translation causes the exported JS/JSON changes every time
|
80
|
+
# https://github.com/ruby-i18n/i18n/blob/v1.8.7/lib/i18n/backend/pluralization.rb#L51
|
81
|
+
hash.keys.
|
82
|
+
each_with_object({}) do |key, seed|
|
83
|
+
value = hash[key]
|
84
|
+
next if value.is_a?(Proc)
|
85
|
+
|
86
|
+
seed[key] = value.is_a?(Hash) ? deep_remove_procs(value) : value
|
87
|
+
end
|
88
|
+
end
|
63
89
|
end
|
64
90
|
end
|
65
91
|
end
|
data/lib/i18n/js/version.rb
CHANGED
data/lib/i18n/js.rb
CHANGED
@@ -4,6 +4,7 @@ require "i18n"
|
|
4
4
|
|
5
5
|
require "i18n/js/utils"
|
6
6
|
require "i18n/js/private/hash_with_symbol_keys"
|
7
|
+
require "i18n/js/private/config_store"
|
7
8
|
|
8
9
|
module I18n
|
9
10
|
module JS
|
@@ -26,6 +27,8 @@ module I18n
|
|
26
27
|
|
27
28
|
def self.config_file_path=(new_path)
|
28
29
|
@config_file_path = new_path
|
30
|
+
# new config file path = need to re-read config from new file
|
31
|
+
Private::ConfigStore.instance.flush_cache
|
29
32
|
end
|
30
33
|
|
31
34
|
# Allow using a different backend than the one globally configured
|
@@ -78,7 +81,7 @@ module I18n
|
|
78
81
|
|
79
82
|
# deep_merge! given result with result for fallback locale
|
80
83
|
def self.merge_with_fallbacks!(result)
|
81
|
-
|
84
|
+
js_available_locales.each do |locale|
|
82
85
|
fallback_locales = FallbackLocales.new(fallbacks, locale)
|
83
86
|
fallback_locales.each do |fallback_locale|
|
84
87
|
# `result[fallback_locale]` could be missing
|
@@ -108,14 +111,16 @@ module I18n
|
|
108
111
|
# Load configuration file for partial exporting and
|
109
112
|
# custom output directory
|
110
113
|
def self.config
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
114
|
+
Private::ConfigStore.instance.fetch do
|
115
|
+
if config_file_exists?
|
116
|
+
erb_result_from_yaml_file = ERB.new(File.read(config_file_path)).result
|
117
|
+
Private::HashWithSymbolKeys.new(
|
118
|
+
(::YAML.load(erb_result_from_yaml_file) || {})
|
119
|
+
)
|
120
|
+
else
|
121
|
+
Private::HashWithSymbolKeys.new({})
|
122
|
+
end.freeze
|
123
|
+
end
|
119
124
|
end
|
120
125
|
|
121
126
|
# @api private
|
@@ -178,7 +183,7 @@ module I18n
|
|
178
183
|
#
|
179
184
|
# So the input is wrapped by our class for better `#slice`
|
180
185
|
Private::HashWithSymbolKeys.new(translations).
|
181
|
-
slice(*::I18n.
|
186
|
+
slice(*::I18n::JS.js_available_locales).
|
182
187
|
to_h
|
183
188
|
end
|
184
189
|
end
|
@@ -208,6 +213,16 @@ module I18n
|
|
208
213
|
end
|
209
214
|
end
|
210
215
|
|
216
|
+
# Get all available locales.
|
217
|
+
#
|
218
|
+
# @return [Array<Symbol>] the locales.
|
219
|
+
def self.js_available_locales
|
220
|
+
config.fetch(:js_available_locales) do
|
221
|
+
# default value
|
222
|
+
I18n.available_locales
|
223
|
+
end.map(&:to_sym)
|
224
|
+
end
|
225
|
+
|
211
226
|
def self.sort_translation_keys?
|
212
227
|
@sort_translation_keys ||= (config[:sort_translation_keys]) if config.key?(:sort_translation_keys)
|
213
228
|
@sort_translation_keys = true if @sort_translation_keys.nil?
|