i18n-js 2.1.2 → 3.8.3

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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +24 -0
  3. data/.github/FUNDING.yml +3 -0
  4. data/.github/workflows/tests.yaml +100 -0
  5. data/.gitignore +6 -4
  6. data/.npmignore +27 -0
  7. data/Appraisals +44 -0
  8. data/CHANGELOG.md +539 -0
  9. data/Gemfile +1 -1
  10. data/README.md +1086 -0
  11. data/Rakefile +19 -7
  12. data/app/assets/javascripts/i18n.js +1095 -0
  13. data/app/assets/javascripts/i18n/filtered.js.erb +23 -0
  14. data/app/assets/javascripts/i18n/shims.js +240 -0
  15. data/app/assets/javascripts/i18n/translations.js +3 -0
  16. data/gemfiles/i18n_0_6.gemfile +7 -0
  17. data/gemfiles/i18n_0_7.gemfile +7 -0
  18. data/gemfiles/i18n_0_8.gemfile +7 -0
  19. data/gemfiles/i18n_0_9.gemfile +7 -0
  20. data/gemfiles/i18n_1_0.gemfile +7 -0
  21. data/gemfiles/i18n_1_1.gemfile +7 -0
  22. data/gemfiles/i18n_1_2.gemfile +7 -0
  23. data/gemfiles/i18n_1_3.gemfile +7 -0
  24. data/gemfiles/i18n_1_4.gemfile +7 -0
  25. data/gemfiles/i18n_1_5.gemfile +7 -0
  26. data/gemfiles/i18n_1_6.gemfile +7 -0
  27. data/gemfiles/i18n_1_7.gemfile +7 -0
  28. data/gemfiles/i18n_1_8.gemfile +7 -0
  29. data/i18n-js.gemspec +12 -9
  30. data/i18njs.png +0 -0
  31. data/lib/i18n-js.rb +1 -177
  32. data/lib/i18n/js.rb +264 -0
  33. data/lib/i18n/js/dependencies.rb +63 -0
  34. data/lib/i18n/js/engine.rb +87 -0
  35. data/lib/i18n/js/fallback_locales.rb +70 -0
  36. data/lib/i18n/js/formatters/base.rb +25 -0
  37. data/lib/i18n/js/formatters/js.rb +39 -0
  38. data/lib/i18n/js/formatters/json.rb +13 -0
  39. data/lib/{i18n-js → i18n/js}/middleware.rb +32 -9
  40. data/lib/i18n/js/private/config_store.rb +31 -0
  41. data/lib/i18n/js/private/hash_with_symbol_keys.rb +36 -0
  42. data/lib/i18n/js/segment.rb +80 -0
  43. data/lib/i18n/js/utils.rb +78 -0
  44. data/lib/i18n/js/version.rb +7 -0
  45. data/lib/rails/generators/i18n/js/config/config_generator.rb +19 -0
  46. data/{config → lib/rails/generators/i18n/js/config/templates}/i18n-js.yml +11 -6
  47. data/lib/tasks/export.rake +8 -0
  48. data/package.json +25 -0
  49. data/spec/fixtures/custom_path.yml +5 -0
  50. data/spec/fixtures/default.yml +5 -0
  51. data/spec/fixtures/erb.yml +5 -0
  52. data/spec/fixtures/except_condition.yml +7 -0
  53. data/spec/fixtures/js_export_dir_custom.yml +7 -0
  54. data/spec/fixtures/js_export_dir_none.yml +6 -0
  55. data/spec/fixtures/js_extend_parent.yml +6 -0
  56. data/spec/fixtures/js_extend_segment.yml +6 -0
  57. data/spec/fixtures/js_file_per_locale.yml +7 -0
  58. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +4 -0
  59. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +6 -0
  60. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +4 -0
  61. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +4 -0
  62. data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +4 -0
  63. data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +4 -0
  64. data/spec/fixtures/js_file_with_namespace_prefix_and_pretty_print.yml +9 -0
  65. data/spec/fixtures/js_sort_translation_keys_false.yml +6 -0
  66. data/spec/fixtures/js_sort_translation_keys_true.yml +6 -0
  67. data/spec/fixtures/json_only.yml +18 -0
  68. data/spec/{resources → fixtures}/locales.yml +58 -1
  69. data/spec/fixtures/merge_plurals.yml +6 -0
  70. data/spec/fixtures/merge_plurals_with_no_overrides.yml +4 -0
  71. data/spec/fixtures/merge_plurals_with_partial_overrides.yml +4 -0
  72. data/spec/fixtures/millions.yml +4 -0
  73. data/spec/fixtures/multiple_conditions.yml +7 -0
  74. data/spec/fixtures/multiple_conditions_per_locale.yml +7 -0
  75. data/spec/fixtures/multiple_files.yml +7 -0
  76. data/spec/{resources → fixtures}/no_config.yml +0 -0
  77. data/spec/fixtures/no_scope.yml +4 -0
  78. data/spec/fixtures/simple_scope.yml +5 -0
  79. data/spec/js/currency.spec.js +62 -0
  80. data/spec/js/current_locale.spec.js +19 -0
  81. data/spec/js/dates.spec.js +276 -0
  82. data/spec/js/defaults.spec.js +31 -0
  83. data/spec/js/extend.spec.js +110 -0
  84. data/spec/js/interpolation.spec.js +124 -0
  85. data/spec/js/jasmine/MIT.LICENSE +20 -0
  86. data/spec/js/jasmine/jasmine-html.js +190 -0
  87. data/spec/js/jasmine/jasmine.css +166 -0
  88. data/spec/js/jasmine/jasmine.js +2476 -0
  89. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  90. data/spec/js/json_parsable.spec.js +14 -0
  91. data/spec/js/locales.spec.js +31 -0
  92. data/spec/js/localization.spec.js +78 -0
  93. data/spec/js/numbers.spec.js +174 -0
  94. data/spec/js/placeholder.spec.js +24 -0
  95. data/spec/js/pluralization.spec.js +219 -0
  96. data/spec/js/prepare_options.spec.js +41 -0
  97. data/spec/js/require.js +2083 -0
  98. data/spec/js/specs.html +49 -0
  99. data/spec/js/specs_requirejs.html +72 -0
  100. data/spec/js/translate.spec.js +304 -0
  101. data/spec/js/translations.js +188 -0
  102. data/spec/js/utility_functions.spec.js +20 -0
  103. data/spec/ruby/i18n/js/fallback_locales_spec.rb +84 -0
  104. data/spec/ruby/i18n/js/segment_spec.rb +261 -0
  105. data/spec/ruby/i18n/js/utils_spec.rb +106 -0
  106. data/spec/ruby/i18n/js_spec.rb +748 -0
  107. data/spec/spec_helper.rb +75 -14
  108. data/yarn.lock +131 -0
  109. metadata +223 -98
  110. data/.rspec +0 -1
  111. data/Gemfile.lock +0 -51
  112. data/README.rdoc +0 -305
  113. data/lib/i18n-js/engine.rb +0 -62
  114. data/lib/i18n-js/railtie.rb +0 -13
  115. data/lib/i18n-js/rake.rb +0 -16
  116. data/lib/i18n-js/version.rb +0 -10
  117. data/spec/i18n_spec.js +0 -768
  118. data/spec/i18n_spec.rb +0 -205
  119. data/spec/resources/custom_path.yml +0 -4
  120. data/spec/resources/default.yml +0 -4
  121. data/spec/resources/js_file_per_locale.yml +0 -3
  122. data/spec/resources/multiple_conditions.yml +0 -6
  123. data/spec/resources/multiple_files.yml +0 -6
  124. data/spec/resources/no_scope.yml +0 -3
  125. data/spec/resources/simple_scope.yml +0 -4
  126. data/vendor/assets/javascripts/i18n.js +0 -450
  127. data/vendor/assets/javascripts/i18n/translations.js.erb +0 -7
@@ -0,0 +1,25 @@
1
+ module I18n
2
+ module JS
3
+ module Formatters
4
+ class Base
5
+ def initialize(js_extend: false, namespace: nil, pretty_print: false, prefix: nil, suffix: nil)
6
+ @js_extend = js_extend
7
+ @namespace = namespace
8
+ @pretty_print = pretty_print
9
+ @prefix = prefix
10
+ @suffix = suffix
11
+ end
12
+
13
+ protected
14
+
15
+ def format_json(translations)
16
+ if @pretty_print
17
+ ::JSON.pretty_generate(translations)
18
+ else
19
+ translations.to_json
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ require "i18n/js/formatters/base"
2
+
3
+ module I18n
4
+ module JS
5
+ module Formatters
6
+ class JS < Base
7
+ JSON_ESCAPE_MAP = {
8
+ "'" => "\\'",
9
+ "\\" => "\\\\"
10
+ }.freeze
11
+ private_constant :JSON_ESCAPE_MAP
12
+
13
+ def format(translations)
14
+ contents = header
15
+ translations.each do |locale, translations_for_locale|
16
+ contents << line(locale, format_json(translations_for_locale))
17
+ end
18
+ contents << (@suffix || '')
19
+ end
20
+
21
+ protected
22
+
23
+ def header
24
+ text = @prefix || ''
25
+ text + %(#{@namespace}.translations || (#{@namespace}.translations = {});\n)
26
+ end
27
+
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) }}'))
30
+ if @js_extend
31
+ %(#{@namespace}.translations["#{locale}"] = I18n.extend((#{@namespace}.translations["#{locale}"] || {}), #{json_literal});\n)
32
+ else
33
+ %(#{@namespace}.translations["#{locale}"] = #{json_literal};\n)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ require "i18n/js/formatters/base"
2
+
3
+ module I18n
4
+ module JS
5
+ module Formatters
6
+ class JSON < Base
7
+ def format(translations)
8
+ format_json(translations)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,8 +1,11 @@
1
- module SimplesIdeias
2
- module I18n
1
+ require "fileutils"
2
+
3
+ module I18n
4
+ module JS
3
5
  class Middleware
4
6
  def initialize(app)
5
7
  @app = app
8
+ clear_cache
6
9
  end
7
10
 
8
11
  def call(env)
@@ -13,7 +16,11 @@ module SimplesIdeias
13
16
 
14
17
  private
15
18
  def cache_path
16
- @cache_path ||= Rails.root.join("tmp/cache/i18n-js.yml")
19
+ @cache_path ||= cache_dir.join("i18n-js.yml")
20
+ end
21
+
22
+ def cache_dir
23
+ @cache_dir ||= Rails.root.join("tmp/cache")
17
24
  end
18
25
 
19
26
  def cache
@@ -26,6 +33,24 @@ module SimplesIdeias
26
33
  end
27
34
  end
28
35
 
36
+ def clear_cache
37
+ # `File.delete` will raise error when "multiple worker"
38
+ # Are running at the same time, like in a parallel test
39
+ #
40
+ # `FileUtils.rm_f` is tested manually
41
+ #
42
+ # See https://github.com/fnando/i18n-js/issues/436
43
+ FileUtils.rm_f(cache_path) if File.exist?(cache_path)
44
+ end
45
+
46
+ def save_cache(new_cache)
47
+ # path could be a symbolic link
48
+ FileUtils.mkdir_p(cache_dir) unless File.exist?(cache_dir)
49
+ File.open(cache_path, "w+") do |file|
50
+ file << new_cache.to_yaml
51
+ end
52
+ end
53
+
29
54
  # Check if translations should be regenerated.
30
55
  # ONLY REGENERATE when these conditions are met:
31
56
  #
@@ -46,13 +71,11 @@ module SimplesIdeias
46
71
  new_cache[path] = changed_at
47
72
  end
48
73
 
49
- unless valid_cache.all?
50
- File.open(cache_path, "w+") do |file|
51
- file << new_cache.to_yaml
52
- end
74
+ return if valid_cache.all?
53
75
 
54
- SimplesIdeias::I18n.export!
55
- end
76
+ save_cache(new_cache)
77
+
78
+ ::I18n::JS.export
56
79
  end
57
80
  end
58
81
  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
@@ -0,0 +1,36 @@
1
+ module I18n
2
+ module JS
3
+ # @api private
4
+ module Private
5
+ # Hash with string keys converted to symbol keys
6
+ # Used for handling values read on YAML
7
+ #
8
+ # @api private
9
+ class HashWithSymbolKeys < ::Hash
10
+ # An instance can only be created by passing in another hash
11
+ def initialize(hash)
12
+ raise TypeError unless hash.is_a?(::Hash)
13
+
14
+ hash.each_key do |key|
15
+ # Objects like `Integer` does not have `to_sym`
16
+ new_key = key.respond_to?(:to_sym) ? key.to_sym : key
17
+ self[new_key] = hash[key]
18
+ end
19
+
20
+ self.default = hash.default if hash.default
21
+ self.default_proc = hash.default_proc if hash.default_proc
22
+
23
+ freeze
24
+ end
25
+
26
+ # From AS Core extension
27
+ def slice(*keys)
28
+ hash = keys.each_with_object(Hash.new) do |k, hash|
29
+ hash[k] = self[k] if has_key?(k)
30
+ end
31
+ self.class.new(hash)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,80 @@
1
+ require "i18n/js/private/hash_with_symbol_keys"
2
+ require "i18n/js/formatters/js"
3
+ require "i18n/js/formatters/json"
4
+
5
+ module I18n
6
+ module JS
7
+
8
+ # Class which enscapulates a translations hash and outputs a single JSON translation file
9
+ class Segment
10
+ OPTIONS = [:namespace, :pretty_print, :js_extend, :prefix, :suffix, :sort_translation_keys, :json_only].freeze
11
+ LOCALE_INTERPOLATOR = /%\{locale\}/
12
+
13
+ attr_reader *([:file, :translations] | OPTIONS)
14
+
15
+ def initialize(file, translations, options = {})
16
+ @file = file
17
+ # `#slice` will be used
18
+ # But when activesupport is absent,
19
+ # the core extension from `i18n` gem will be used instead
20
+ # And it's causing errors (at least in test)
21
+ #
22
+ # So the input is wrapped by our class for better `#slice`
23
+ @translations = Private::HashWithSymbolKeys.new(translations)
24
+ @namespace = options[:namespace] || 'I18n'
25
+ @pretty_print = !!options[:pretty_print]
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
29
+ @sort_translation_keys = options.key?(:sort_translation_keys) ? !!options[:sort_translation_keys] : true
30
+ @json_only = options.key?(:json_only) ? !!options[:json_only] : false
31
+ end
32
+
33
+ # Saves JSON file containing translations
34
+ def save!
35
+ if @file =~ LOCALE_INTERPOLATOR
36
+ I18n.available_locales.each do |locale|
37
+ write_file(file_for_locale(locale), @translations.slice(locale))
38
+ end
39
+ else
40
+ write_file
41
+ end
42
+ end
43
+
44
+ protected
45
+
46
+ def file_for_locale(locale)
47
+ @file.gsub(LOCALE_INTERPOLATOR, locale.to_s)
48
+ end
49
+
50
+ def write_file(_file = @file, _translations = @translations)
51
+ FileUtils.mkdir_p File.dirname(_file)
52
+ _translations = Utils.deep_key_sort(_translations) if @sort_translation_keys
53
+ contents = formatter.format(_translations)
54
+
55
+ return if File.exist?(_file) && File.read(_file) == contents
56
+
57
+ File.open(_file, "w+") do |f|
58
+ f << contents
59
+ end
60
+ end
61
+
62
+ def formatter
63
+ if @json_only
64
+ Formatters::JSON.new(**formatter_options)
65
+ else
66
+ Formatters::JS.new(**formatter_options)
67
+ end
68
+ end
69
+
70
+ def formatter_options
71
+ { js_extend: @js_extend,
72
+ namespace: @namespace,
73
+ pretty_print: @pretty_print,
74
+ prefix: @prefix,
75
+ suffix: @suffix
76
+ }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,78 @@
1
+ module I18n
2
+ module JS
3
+ module Utils
4
+ PLURAL_KEYS = %i[zero one two few many other].freeze
5
+
6
+ # Based on deep_merge by Stefan Rusterholz, see <http://www.ruby-forum.com/topic/142809>.
7
+ # This method is used to handle I18n fallbacks. Given two equivalent path nodes in two locale trees:
8
+ # 1. If the node in the current locale appears to be an I18n pluralization (:one, :other, etc.),
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
+ # prioritizing the current locale.
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
16
+ MERGER = proc do |_key, v1, v2|
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
23
+ else
24
+ v2 || v1
25
+ end
26
+ end
27
+
28
+ HASH_NIL_VALUE_CLEANER_PROC = proc do |k, v|
29
+ v.kind_of?(Hash) ? (v.delete_if(&HASH_NIL_VALUE_CLEANER_PROC); false) : v.nil?
30
+ end
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
+
40
+ def self.strip_keys_with_nil_values(hash)
41
+ hash.dup.delete_if(&HASH_NIL_VALUE_CLEANER_PROC)
42
+ end
43
+
44
+ def self.deep_merge(target_hash, hash) # :nodoc:
45
+ target_hash.merge(hash, &MERGER)
46
+ end
47
+
48
+ def self.deep_merge!(target_hash, hash) # :nodoc:
49
+ target_hash.merge!(hash, &MERGER)
50
+ end
51
+
52
+ def self.deep_reject(hash, scopes = [], &block)
53
+ hash.each_with_object({}) do |(k, v), memo|
54
+ unless block.call(k, v, scopes + [k.to_s])
55
+ memo[k] = v.kind_of?(Hash) ? deep_reject(v, scopes + [k.to_s], &block) : v
56
+ end
57
+ end
58
+ end
59
+
60
+ def self.scopes_match?(scopes1, scopes2)
61
+ if scopes1.length == scopes2.length
62
+ [scopes1, scopes2].transpose.all? do |scope1, scope2|
63
+ scope1.to_s == '*' || scope2.to_s == '*' || scope1.to_s == scope2.to_s
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.deep_key_sort(hash)
69
+ # Avoid things like `true` or `1` from YAML which causes error
70
+ hash.keys.sort {|a, b| a.to_s <=> b.to_s}.
71
+ each_with_object({}) do |key, seed|
72
+ value = hash[key]
73
+ seed[key] = value.is_a?(Hash) ? deep_key_sort(value) : value
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module JS
5
+ VERSION = "3.8.3"
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module I18n
2
+ module Js
3
+ class ConfigGenerator < Rails::Generators::Base
4
+ # Copied files come from templates folder
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ # Generator desc
8
+ desc <<-DESC
9
+ Creates a default i18n-js.yml configuration file in your app's config
10
+ folder. This file allows you to customize i18n:js:export rake task
11
+ outputted files.
12
+ DESC
13
+
14
+ def copy_initializer_file
15
+ copy_file "i18n-js.yml", "config/i18n-js.yml"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,4 +1,5 @@
1
1
  # Split context in several files.
2
+ #
2
3
  # By default only one file with all translations is exported and
3
4
  # no configuration is required. Your settings for asset pipeline
4
5
  # are automatically recognized.
@@ -7,16 +8,20 @@
7
8
  # locale contexts that will be exported, just use this file to do
8
9
  # so.
9
10
  #
11
+ # For more informations about the export options with this file, please
12
+ # refer to the README
13
+ #
14
+ #
10
15
  # If you're going to use the Rails 3.1 asset pipeline, change
11
16
  # the following configuration to something like this:
12
17
  #
13
- # translations:
14
- # - file: "app/assets/javascripts/i18n/translations.js"
18
+ # translations:
19
+ # - file: "app/assets/javascripts/i18n/translations.js"
15
20
  #
16
21
  # If you're running an old version, you can use something
17
22
  # like this:
18
23
  #
19
- # translations:
20
- # - file: "public/javascripts/translations.js"
21
- # only: "*"
22
- #
24
+ # translations:
25
+ # - file: "app/assets/javascripts/i18n/translations.js"
26
+ # only: "*"
27
+ #
@@ -0,0 +1,8 @@
1
+ namespace :i18n do
2
+ namespace :js do
3
+ desc "Export translations to JS file(s)"
4
+ task :export => :environment do
5
+ I18n::JS.export
6
+ end
7
+ end
8
+ end