i18n-js 3.3.0 → 3.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +1 -1
  3. data/.github/FUNDING.yml +3 -0
  4. data/.github/workflows/tests.yaml +106 -0
  5. data/Appraisals +16 -0
  6. data/CHANGELOG.md +153 -4
  7. data/README.md +505 -333
  8. data/app/assets/javascripts/i18n/shims.js +35 -3
  9. data/app/assets/javascripts/i18n.js +37 -33
  10. data/gemfiles/i18n_1_10.gemfile +7 -0
  11. data/gemfiles/i18n_1_7.gemfile +7 -0
  12. data/gemfiles/i18n_1_8.gemfile +7 -0
  13. data/gemfiles/i18n_1_9.gemfile +7 -0
  14. data/i18n-js.gemspec +2 -2
  15. data/i18njs.png +0 -0
  16. data/lib/i18n/js/dependencies.rb +6 -2
  17. data/lib/i18n/js/engine.rb +1 -1
  18. data/lib/i18n/js/formatters/base.rb +3 -1
  19. data/lib/i18n/js/formatters/js.rb +12 -4
  20. data/lib/i18n/js/middleware.rb +1 -1
  21. data/lib/i18n/js/private/config_store.rb +31 -0
  22. data/lib/i18n/js/segment.rb +9 -3
  23. data/lib/i18n/js/utils.rb +34 -8
  24. data/lib/i18n/js/version.rb +1 -1
  25. data/lib/i18n/js.rb +25 -10
  26. data/package.json +2 -2
  27. data/spec/fixtures/js_available_locales_custom.yml +1 -0
  28. data/spec/fixtures/{js_file_with_namespace_and_pretty_print.yml → js_file_with_namespace_prefix_and_pretty_print.yml} +2 -0
  29. data/spec/fixtures/locales.yml +38 -0
  30. data/spec/fixtures/merge_plurals_with_no_overrides.yml +4 -0
  31. data/spec/fixtures/merge_plurals_with_partial_overrides.yml +4 -0
  32. data/spec/fixtures/millions.yml +4 -0
  33. data/spec/js/dates.spec.js +1 -0
  34. data/spec/js/json_parsable.spec.js +14 -0
  35. data/spec/js/localization.spec.js +38 -14
  36. data/spec/js/numbers.spec.js +4 -0
  37. data/spec/js/pluralization.spec.js +19 -2
  38. data/spec/js/require.js +4 -4
  39. data/spec/js/translate.spec.js +68 -48
  40. data/spec/js/translations.js +27 -2
  41. data/spec/ruby/i18n/js/segment_spec.rb +75 -8
  42. data/spec/ruby/i18n/js/utils_spec.rb +32 -0
  43. data/spec/ruby/i18n/js_spec.rb +169 -35
  44. data/spec/spec_helper.rb +1 -0
  45. data/yarn.lock +32 -25
  46. metadata +26 -9
  47. 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: http://es5.github.com/#x15.4.4.18
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: http://es5.github.com/#x9.11
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: http://es5.github.com/#x9.11
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 (http://www.ietf.org/rfc/rfc4646.txt)
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 http://www.w3.org/TR/html401/struct/dirlang.html
278
- // @see http://en.wikipedia.org/wiki/IETF_language_tag
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 http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
284
- // @see http://www.iso.org/iso/home/standards/language_codes.htm
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 http://en.wikipedia.org/wiki/ISO_3166
288
- // @see http://www.iso.org/iso/country_codes.htm
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('_',' ').replace(/([a-z])([A-Z])/g,
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 - Hour of the day, 24-hour clock (0..23)
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
- unit = this.t("number.human.storage_units.units.byte", {count: size});
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
- unit = this.t("number.human.storage_units.units." + SIZE_UNITS[iterations]);
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
  }));
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "i18n", "~> 1.10.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "i18n", "~> 1.7.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "i18n", "~> 1.8.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "i18n", "~> 1.9.0"
6
+
7
+ gemspec path: "../"
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 = "http://rubygems.org/gems/i18n-js"
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.0"
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
@@ -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)
@@ -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
- # http://guides.rubyonrails.org/configuring.html#initializers
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
- %(#{@namespace}.translations || (#{@namespace}.translations = {});\n)
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}"] || {}), #{translations});\n)
31
+ %(#{@namespace}.translations["#{locale}"] = I18n.extend((#{@namespace}.translations["#{locale}"] || {}), #{json_literal});\n)
24
32
  else
25
- %(#{@namespace}.translations["#{locale}"] = #{translations};\n)
33
+ %(#{@namespace}.translations["#{locale}"] = #{json_literal};\n)
26
34
  end
27
35
  end
28
36
  end
@@ -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.exists?(cache_dir)
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
@@ -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.available_locales.each do |locale|
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 <http://www.ruby-forum.com/topic/142809>.
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 as-is without merging. This prevents mixing locales with different pluralization schemes.
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 === v2 && (v2.keys - PLURAL_KEYS).empty?
15
- v2
16
- elsif Hash === v1 && Hash === v2
17
- v1.merge(v2, &MERGER)
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.nil? ? v1 : 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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module I18n
4
4
  module JS
5
- VERSION = "3.3.0"
5
+ VERSION = "3.9.2"
6
6
  end
7
7
  end
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
- I18n.available_locales.each do |locale|
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
- if config_file_exists?
112
- erb_result_from_yaml_file = ERB.new(File.read(config_file_path)).result
113
- Private::HashWithSymbolKeys.new(
114
- (::YAML.load(erb_result_from_yaml_file) || {})
115
- )
116
- else
117
- Private::HashWithSymbolKeys.new({})
118
- end.freeze
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.available_locales).
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?