i18n-js 4.2.0 → 4.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dac70b6202710a44a8303deff7206848d702b4cdb3cc926552aec41114ff745
4
- data.tar.gz: 2631494d675d0d8459ab5450bb634fee6c305dd6c01d32bb03d647ac2a657d9e
3
+ metadata.gz: f4299c989d73925fef9ee5651623b38c8b89f334dba54781e91e093860dc4ebf
4
+ data.tar.gz: 07d036910ef839e283030f481e6845fab011f77311fe48def9a564885009ecbd
5
5
  SHA512:
6
- metadata.gz: 67c7b4a499d4848967461c1d3a798de2d814af972049f6977d7cf4dc5c6781bfc6ecdc1129068f963ba8dc115ebe5c219f078ef00b92204f86c54a114d55aba3
7
- data.tar.gz: 329799184d84e27455783a6a4522b9a0cce446ba33ab3d5cfe74e22d08f685736abb4329e2506e1c2d83504d963c777317f8998a838fb3fae8c358f65569c5b5
6
+ metadata.gz: c45b7571d0854ce7eb5b225b382628ee3d4c682a640864f050e357d299fc9416f9885ed94518abf192e4a99956c10ab1ccfa6f7a61f954f89ce3efd60a6c78b6
7
+ data.tar.gz: '0087a9c072c0f84fac37fff95bddcc541e0a247ad6c924f4754c062ed066da77ed2d5c96d6511ffcb607ff4c0459296f9e1af42b6c6036d3365080c47a6dae4f'
@@ -44,7 +44,7 @@ jobs:
44
44
  hashFiles('package.json') }}
45
45
 
46
46
  - name: Set up Node
47
- uses: actions/setup-node@v2-beta
47
+ uses: actions/setup-node@v3.5.1
48
48
  with:
49
49
  node-version: ${{ matrix.node }}
50
50
 
data/CHANGELOG.md CHANGED
@@ -11,6 +11,20 @@ Prefix your message with one of the following:
11
11
  - [Security] in case of vulnerabilities.
12
12
  -->
13
13
 
14
+ ## v4.2.2 - Dec 30, 2022
15
+
16
+ - [Changed] Do not re-export files whose contents haven't changed.
17
+ - [Changed] Translations will always be deep sorted.
18
+ - [Fixed] Remove procs from translations before exporting files.
19
+
20
+ ## v4.2.1 - Dec 25, 2022
21
+
22
+ - [Changed] Change plugin api to be based on instance methods. This avoids
23
+ having to pass in the config for each and every method. It also allows us
24
+ adding helper methods to the base class.
25
+ - [Fixed] Fix performance issues with embed fallback translations' initial
26
+ implementation.
27
+
14
28
  ## v4.2.0 - Dec 10, 2022
15
29
 
16
30
  - [Added] Add `I18nJS::Plugin.after_export(files:, config:)` method, that's
@@ -47,8 +47,8 @@ JSON
47
47
  ```
48
48
 
49
49
  You can also use guard. Make sure you have both
50
- [guard](https://rubygems.org/packages/guard) and
51
- [guard-compat](https://rubygems.org/packages/guard-compat) installed and use
50
+ [guard](https://rubygems.org/gems/guard) and
51
+ [guard-compat](https://rubygems.org/gems/guard-compat) installed and use
52
52
  Guardfile file with the following contents:
53
53
 
54
54
  ```ruby
@@ -174,8 +174,8 @@ translations:
174
174
 
175
175
  Other configuration options:
176
176
 
177
- - `export_i18n_js`: removed without an equivalent
178
- - `fallbacks`: removed without an equivalent
177
+ - `export_i18n_js`: replaced by [export_files plugin](https://github.com/fnando/i18n-js#export_files)
178
+ - `fallbacks`: replaced by [embed_fallback_translations plugin](https://github.com/fnando/i18n-js#embed_fallback_translations)
179
179
  - `js_available_locales`: removed (on v4 you can use groups, like in
180
180
  `{pt-BR,en}.*`)
181
181
  - `namespace`: removed without an equivalent
data/README.md CHANGED
@@ -209,26 +209,21 @@ i18n.store({
209
209
  #### Plugin API
210
210
 
211
211
  You can transform the exported translations by adding plugins. A plugin must
212
- inherit from `I18nJS::Plugin` and can have 4 class methods. To see a real
213
- example, see
214
- [lib/i18n-js/embed_fallback_translations_plugin.rb](https://github.com/fnando/i18n-js/blob/main/lib/i18n-js/embed_fallback_translations_plugin.rb)
215
-
216
- Here's the base `I18nJS::Plugin` class with the documented api:
212
+ inherit from `I18nJS::Plugin` and can have 4 class methods (they're all optional
213
+ and will default to a noop implementation). For real examples, see [lib/i18n-js/embed_fallback_translations_plugin.rb](https://github.com/fnando/i18n-js/blob/main/lib/i18n-js/embed_fallback_translations_plugin.rb) and [lib/i18n-js/export_files_plugin.rb](https://github.com/fnando/i18n-js/blob/main/lib/i18n-js/export_files_plugin.rb)
217
214
 
218
215
  ```ruby
219
216
  # frozen_string_literal: true
220
217
 
221
218
  module I18nJS
222
- class Plugin
219
+ class SamplePlugin < I18nJS::Plugin
223
220
  # This method is responsible for transforming the translations. The
224
221
  # translations you'll receive may be already be filtered by other plugins
225
222
  # and by the default filtering itself. If you need to access the original
226
223
  # translations, use `I18nJS.translations`.
227
- #
228
- # Make sure you always check whether your plugin is active before
229
- # transforming translations; otherwise, opting out transformation won't be
230
- # possible.
231
- def self.transform(translations:, config:)
224
+ def transform(translations:)
225
+ # transform `translations` here…
226
+
232
227
  translations
233
228
  end
234
229
 
@@ -237,7 +232,11 @@ module I18nJS
237
232
  # If the configuration contains invalid data, then you must raise an
238
233
  # exception using something like
239
234
  # `raise I18nJS::Schema::InvalidError, error_message`.
240
- def self.validate_schema(config:)
235
+ #
236
+ # Notice the validation will only happen when the plugin configuration is
237
+ # set (i.e. the configuration contains your config key).
238
+ def validate_schema
239
+ # validate plugin schema here…
241
240
  end
242
241
 
243
242
  # This method must set up the basic plugin configuration, like adding the
@@ -246,7 +245,9 @@ module I18nJS
246
245
  #
247
246
  # If you don't add this key, the linter will prevent non-default keys from
248
247
  # being added to the configuration file.
249
- def self.setup
248
+ def setup
249
+ # If you plugin has configuration, uncomment the line below
250
+ # I18nJS::Schema.root_keys << config_key
250
251
  end
251
252
 
252
253
  # This method is called whenever `I18nJS.call(**kwargs)` finishes exporting
@@ -254,15 +255,21 @@ module I18nJS
254
255
  #
255
256
  # You can use it to further process exported files, or generate new files
256
257
  # based on the translations that have been exported.
257
- #
258
- # Make sure you always check whether your plugin is active before
259
- # processing files; otherwise, opting out won't be possible.
260
- def self.after_export(files:, config:)
258
+ def after_export(files:)
259
+ # process exported files here…
261
260
  end
262
261
  end
263
262
  end
264
263
  ```
265
264
 
265
+ The class `I18nJS::Plugin` implements some helper methods that you can use:
266
+
267
+ - `I18nJS::Plugin#config_key`: the configuration key that was inferred out of
268
+ your plugin's class name.
269
+ - `I18nJS::Plugin#config`: the plugin configuration.
270
+ - `I18nJS::Plugin#enabled?`: whether the plugin is enabled or not based on the
271
+ plugin's configuration.
272
+
266
273
  To distribute this plugin, you need to create a gem package that matches the
267
274
  pattern `i18n-js/*_plugin.rb`. You can test whether your plugin will be found by
268
275
  installing your gem, opening a iRB session and running
@@ -512,7 +519,7 @@ that loads all the exported translation.
512
519
 
513
520
  [There's a document](https://github.com/fnando/i18n-js/tree/main/MIGRATING_FROM_V3_TO_V4.md)
514
521
  outlining some of the things you need to do to migrate from v3 to v4. It may not
515
- be as complete as we'd like it to be, so let's know if you face any issues
522
+ be as complete as we'd like it to be, so let us know if you face any issues
516
523
  during the migration is not outline is that document.
517
524
 
518
525
  #### How can I export translations without having a database around?
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nJS
4
+ def self.clean_hash(hash)
5
+ hash.keys.each_with_object({}) do |key, buffer|
6
+ value = hash[key]
7
+
8
+ next if value.is_a?(Proc)
9
+
10
+ buffer[key] = value.is_a?(Hash) ? clean_hash(value) : value
11
+ end
12
+ end
13
+ end
@@ -4,54 +4,65 @@ module I18nJS
4
4
  require "i18n-js/plugin"
5
5
 
6
6
  class EmbedFallbackTranslationsPlugin < I18nJS::Plugin
7
- CONFIG_KEY = :embed_fallback_translations
7
+ module Utils
8
+ # Based on deep_merge by Stefan Rusterholz, see
9
+ # <https://www.ruby-forum.com/topic/142809>.
10
+ # This method is used to handle I18n fallbacks. Given two equivalent path
11
+ # nodes in two locale trees:
12
+ # 1. If the node in the current locale appears to be an I18n pluralization
13
+ # (:one, :other, etc.), use the node, but merge in any missing/non-nil
14
+ # keys from the fallback (default) locale.
15
+ # 2. Else if both nodes are Hashes, combine (merge) the key-value pairs of
16
+ # the two nodes into one, prioritizing the current locale.
17
+ # 3. Else if either node is nil, use the other node.
18
+
19
+ PLURAL_KEYS = %i[zero one two few many other].freeze
20
+ PLURAL_MERGER = proc {|_key, v1, v2| v1 || v2 }
21
+ MERGER = proc do |_key, v1, v2|
22
+ if v1.is_a?(Hash) && v2.is_a?(Hash)
23
+ if (v2.keys - PLURAL_KEYS).empty?
24
+ v2.merge(v1, &PLURAL_MERGER).slice(*v2.keys)
25
+ else
26
+ v1.merge(v2, &MERGER)
27
+ end
28
+ else
29
+ v2 || v1
30
+ end
31
+ end
8
32
 
9
- def self.setup
10
- I18nJS::Schema.root_keys << CONFIG_KEY
33
+ def self.deep_merge(target_hash, hash)
34
+ target_hash.merge(hash, &MERGER)
35
+ end
11
36
  end
12
37
 
13
- def self.validate_schema(config:)
14
- return unless config.key?(CONFIG_KEY)
38
+ def setup
39
+ I18nJS::Schema.root_keys << config_key
40
+ end
15
41
 
16
- plugin_config = config[CONFIG_KEY]
42
+ def validate_schema
17
43
  valid_keys = %i[enabled]
18
- schema = I18nJS::Schema.new(config)
19
44
 
20
- schema.expect_required_keys(valid_keys, plugin_config)
21
- schema.reject_extraneous_keys(valid_keys, plugin_config)
22
- schema.expect_enabled_config(CONFIG_KEY, plugin_config[:enabled])
45
+ schema.expect_required_keys(keys: valid_keys, path: [config_key])
46
+ schema.reject_extraneous_keys(keys: valid_keys, path: [config_key])
23
47
  end
24
48
 
25
- def self.transform(translations:, config:)
26
- return translations unless config.dig(CONFIG_KEY, :enabled)
49
+ def transform(translations:)
50
+ return translations unless enabled?
27
51
 
28
- translations_glob = Glob.new(translations)
29
- translations_glob << "*"
52
+ fallback_locale = I18n.default_locale.to_sym
53
+ locales_to_fallback = translations.keys - [fallback_locale]
30
54
 
31
- mapping = translations.keys.each_with_object({}) do |locale, buffer|
32
- buffer[locale] = Glob.new(translations[locale]).tap do |glob|
33
- glob << "*"
34
- end
35
- end
55
+ translations_with_fallback = {}
56
+ translations_with_fallback[fallback_locale] =
57
+ translations[fallback_locale]
36
58
 
37
- default_locale = I18n.default_locale
38
- default_locale_glob = mapping.delete(default_locale)
39
- default_locale_paths = default_locale_glob.paths
40
-
41
- mapping.each do |locale, glob|
42
- missing_keys = default_locale_paths - glob.paths
43
-
44
- missing_keys.each do |key|
45
- components = key.split(".").map(&:to_sym)
46
- fallback_translation = translations.dig(default_locale, *components)
47
-
48
- next unless fallback_translation
49
-
50
- translations_glob.set([locale, key].join("."), fallback_translation)
51
- end
59
+ locales_to_fallback.each do |locale|
60
+ translations_with_fallback[locale] = Utils.deep_merge(
61
+ translations[fallback_locale], translations[locale]
62
+ )
52
63
  end
53
64
 
54
- translations_glob.to_h
65
+ translations_with_fallback
55
66
  end
56
67
  end
57
68
 
@@ -4,37 +4,43 @@ module I18nJS
4
4
  require "i18n-js/plugin"
5
5
 
6
6
  class ExportFilesPlugin < I18nJS::Plugin
7
- CONFIG_KEY = :export_files
8
-
9
- def self.setup
10
- I18nJS::Schema.root_keys << CONFIG_KEY
7
+ def setup
8
+ I18nJS::Schema.root_keys << config_key
11
9
  end
12
10
 
13
- def self.validate_schema(config:)
14
- return unless config.key?(CONFIG_KEY)
15
-
16
- plugin_config = config[CONFIG_KEY]
11
+ def validate_schema
17
12
  valid_keys = %i[enabled files]
18
- schema = I18nJS::Schema.new(config)
19
-
20
- schema.expect_required_keys(valid_keys, plugin_config)
21
- schema.reject_extraneous_keys(valid_keys, plugin_config)
22
- schema.expect_enabled_config(CONFIG_KEY, plugin_config[:enabled])
23
- schema.expect_array_with_items(:files, plugin_config[:files])
24
-
25
- plugin_config[:files].each do |exports|
26
- schema.expect_required_keys(%i[template output], exports)
27
- schema.reject_extraneous_keys(%i[template output], exports)
28
- schema.expect_type(:template, exports[:template], String, exports)
29
- schema.expect_type(:output, exports[:output], String, exports)
30
- end
31
- end
32
13
 
33
- def self.after_export(files:, config:)
34
- return unless config.dig(CONFIG_KEY, :enabled)
14
+ schema.expect_required_keys(keys: valid_keys, path: [config_key])
15
+ schema.reject_extraneous_keys(keys: valid_keys, path: [config_key])
16
+ schema.expect_array_with_items(path: [config_key, :files])
17
+
18
+ config[:files].each_with_index do |_exports, index|
19
+ export_keys = %i[template output]
35
20
 
36
- exports = config.dig(CONFIG_KEY, :files)
21
+ schema.expect_required_keys(
22
+ keys: export_keys,
23
+ path: [config_key, :files, index]
24
+ )
25
+
26
+ schema.reject_extraneous_keys(
27
+ keys: export_keys,
28
+ path: [config_key, :files, index]
29
+ )
30
+
31
+ schema.expect_type(
32
+ path: [config_key, :files, index, :template],
33
+ types: String
34
+ )
35
+
36
+ schema.expect_type(
37
+ path: [config_key, :files, index, :output],
38
+ types: String
39
+ )
40
+ end
41
+ end
37
42
 
43
+ def after_export(files:)
38
44
  require "erb"
39
45
  require "digest/md5"
40
46
  require "json"
@@ -45,7 +51,7 @@ module I18nJS
45
51
  extension = File.extname(name)
46
52
  base_name = File.basename(file, extension)
47
53
 
48
- exports.each do |export|
54
+ config[:files].each do |export|
49
55
  translations = JSON.load_file(file)
50
56
  template = Template.new(
51
57
  file: file,
@@ -3,13 +3,16 @@
3
3
  require_relative "schema"
4
4
 
5
5
  module I18nJS
6
+ def self.available_plugins
7
+ @available_plugins ||= Set.new
8
+ end
9
+
6
10
  def self.plugins
7
11
  @plugins ||= []
8
12
  end
9
13
 
10
14
  def self.register_plugin(plugin)
11
- plugins << plugin
12
- plugin.setup
15
+ available_plugins << plugin
13
16
  end
14
17
 
15
18
  def self.plugin_files
@@ -22,16 +25,53 @@ module I18nJS
22
25
  end
23
26
  end
24
27
 
28
+ def self.initialize_plugins!(config:)
29
+ @plugins = available_plugins.map do |plugin|
30
+ plugin.new(config: config).tap(&:setup)
31
+ end
32
+ end
33
+
25
34
  class Plugin
35
+ # The configuration that's being used to export translations.
36
+ attr_reader :main_config
37
+
38
+ # The `I18nJS::Schema` instance that can be used to validate your plugin's
39
+ # configuration.
40
+ attr_reader :schema
41
+
42
+ def initialize(config:)
43
+ @main_config = config
44
+ @schema = I18nJS::Schema.new(@main_config)
45
+ end
46
+
47
+ # Infer the config key name out of the class.
48
+ # If you plugin is called `MySamplePlugin`, the key will be `my_sample`.
49
+ def config_key
50
+ self.class.name.split("::").last
51
+ .gsub(/Plugin$/, "")
52
+ .gsub(/^([A-Z]+)([A-Z])/) { "#{$1.downcase}#{$2}" }
53
+ .gsub(/^([A-Z]+)/) { $1.downcase }
54
+ .gsub(/([A-Z]+)/m) { "_#{$1.downcase}" }
55
+ .downcase
56
+ .to_sym
57
+ end
58
+
59
+ # Return the plugin configuration
60
+ def config
61
+ main_config[config_key] || {}
62
+ end
63
+
64
+ # Check whether plugin is enabled or not.
65
+ # A plugin is enabled when the plugin configuration has `enabled: true`.
66
+ def enabled?
67
+ config[:enabled]
68
+ end
69
+
26
70
  # This method is responsible for transforming the translations. The
27
71
  # translations you'll receive may be already be filtered by other plugins
28
72
  # and by the default filtering itself. If you need to access the original
29
73
  # translations, use `I18nJS.translations`.
30
- #
31
- # Make sure you always check whether your plugin is active before
32
- # transforming translations; otherwise, opting out transformation won't be
33
- # possible.
34
- def self.transform(translations:, config:) # rubocop:disable Lint/UnusedMethodArgument
74
+ def transform(translations:)
35
75
  translations
36
76
  end
37
77
 
@@ -40,7 +80,7 @@ module I18nJS
40
80
  # If the configuration contains invalid data, then you must raise an
41
81
  # exception using something like
42
82
  # `raise I18nJS::Schema::InvalidError, error_message`.
43
- def self.validate_schema(config:)
83
+ def validate_schema
44
84
  end
45
85
 
46
86
  # This method must set up the basic plugin configuration, like adding the
@@ -49,7 +89,7 @@ module I18nJS
49
89
  #
50
90
  # If you don't add this key, the linter will prevent non-default keys from
51
91
  # being added to the configuration file.
52
- def self.setup
92
+ def setup
53
93
  end
54
94
 
55
95
  # This method is called whenever `I18nJS.call(**kwargs)` finishes exporting
@@ -57,10 +97,7 @@ module I18nJS
57
97
  #
58
98
  # You can use it to further process exported files, or generate new files
59
99
  # based on the translations that have been exported.
60
- #
61
- # Make sure you always check whether your plugin is active before
62
- # processing files; otherwise, opting out won't be possible.
63
- def self.after_export(files:, config:)
100
+ def after_export(files:)
64
101
  end
65
102
  end
66
103
  end
@@ -25,7 +25,7 @@ module I18nJS
25
25
  def self.validate!(target)
26
26
  schema = new(target)
27
27
  schema.validate!
28
- I18nJS.plugins.each {|plugin| plugin.validate_schema(config: target) }
28
+ schema
29
29
  end
30
30
 
31
31
  attr_reader :target
@@ -35,13 +35,44 @@ module I18nJS
35
35
  end
36
36
 
37
37
  def validate!
38
- expect_type(:root, target, Hash, target)
38
+ validate_root
39
+
40
+ expect_required_keys(
41
+ keys: self.class.required_root_keys,
42
+ path: nil
43
+ )
44
+
45
+ reject_extraneous_keys(
46
+ keys: self.class.root_keys,
47
+ path: nil
48
+ )
39
49
 
40
- expect_required_keys(self.class.required_root_keys, target)
41
- reject_extraneous_keys(self.class.root_keys, target)
42
50
  validate_translations
43
51
  validate_lint_translations
44
52
  validate_lint_scripts
53
+ validate_plugins
54
+ end
55
+
56
+ def validate_plugins
57
+ I18nJS.plugins.each do |plugin|
58
+ next unless target.key?(plugin.config_key)
59
+
60
+ expect_type(
61
+ path: [plugin.config_key, :enabled],
62
+ types: [TrueClass, FalseClass]
63
+ )
64
+
65
+ plugin.validate_schema
66
+ end
67
+ end
68
+
69
+ def validate_root
70
+ return if target.is_a?(Hash)
71
+
72
+ message = "Expected config to be \"Hash\"; " \
73
+ "got #{target.class} instead"
74
+
75
+ reject message, target
45
76
  end
46
77
 
47
78
  def validate_lint_translations
@@ -49,11 +80,14 @@ module I18nJS
49
80
 
50
81
  return unless target.key?(key)
51
82
 
52
- config = target[key]
83
+ expect_type(path: [key], types: Hash)
84
+
85
+ expect_required_keys(
86
+ keys: REQUIRED_LINT_TRANSLATIONS_KEYS,
87
+ path: [key]
88
+ )
53
89
 
54
- expect_type(key, config, Hash, target)
55
- expect_required_keys(REQUIRED_LINT_TRANSLATIONS_KEYS, config)
56
- expect_type(:ignore, config[:ignore], Array, config)
90
+ expect_type(path: [key, :ignore], types: Array)
57
91
  end
58
92
 
59
93
  def validate_lint_scripts
@@ -61,31 +95,36 @@ module I18nJS
61
95
 
62
96
  return unless target.key?(key)
63
97
 
64
- config = target[key]
65
-
66
- expect_type(key, config, Hash, target)
67
- expect_required_keys(REQUIRED_LINT_SCRIPTS_KEYS, config)
68
- expect_type(:ignore, config[:ignore], Array, config)
69
- expect_type(:patterns, config[:patterns], Array, config)
98
+ expect_type(path: [key], types: Hash)
99
+ expect_required_keys(
100
+ keys: REQUIRED_LINT_SCRIPTS_KEYS,
101
+ path: [key]
102
+ )
103
+ expect_type(path: [key, :ignore], types: Array)
104
+ expect_type(path: [key, :patterns], types: Array)
70
105
  end
71
106
 
72
107
  def validate_translations
73
- translations = target[:translations]
74
-
75
- expect_type(:translations, translations, Array, target)
76
- expect_array_with_items(:translations, translations)
108
+ expect_array_with_items(path: [:translations])
77
109
 
78
- translations.each do |translation|
79
- validate_translation(translation)
110
+ target[:translations].each_with_index do |translation, index|
111
+ validate_translation(translation, index)
80
112
  end
81
113
  end
82
114
 
83
- def validate_translation(translation)
84
- expect_required_keys(REQUIRED_TRANSLATION_KEYS, translation)
85
- reject_extraneous_keys(TRANSLATION_KEYS, translation)
86
- expect_type(:file, translation[:file], String, translation)
87
- expect_type(:patterns, translation[:patterns], Array, translation)
88
- expect_array_with_items(:patterns, translation[:patterns], translation)
115
+ def validate_translation(_translation, index)
116
+ expect_required_keys(
117
+ path: [:translations, index],
118
+ keys: REQUIRED_TRANSLATION_KEYS
119
+ )
120
+
121
+ reject_extraneous_keys(
122
+ keys: TRANSLATION_KEYS,
123
+ path: [:translations, index]
124
+ )
125
+
126
+ expect_type(path: [:translations, index, :file], types: String)
127
+ expect_array_with_items(path: [:translations, index, :patterns])
89
128
  end
90
129
 
91
130
  def reject(error_message, node = nil)
@@ -93,51 +132,85 @@ module I18nJS
93
132
  raise InvalidError, "#{error_message}#{node_json}"
94
133
  end
95
134
 
96
- def expect_enabled_config(config_key, value)
97
- return if [TrueClass, FalseClass].include?(value.class)
98
-
99
- actual_type = value.class
100
-
101
- reject "Expected #{config_key}.enabled to be a boolean; " \
102
- "got #{actual_type} instead"
103
- end
135
+ def expect_type(path:, types:)
136
+ path = prepare_path(path: path)
137
+ value = value_for(path: path)
138
+ types = Array(types)
104
139
 
105
- def expect_type(attribute, value, expected_type, payload)
106
- return if value.is_a?(expected_type)
140
+ return if types.any? {|type| value.is_a?(type) }
107
141
 
108
142
  actual_type = value.class
109
143
 
144
+ type_desc = if types.size == 1
145
+ types[0].to_s.inspect
146
+ else
147
+ "one of #{types.inspect}"
148
+ end
149
+
110
150
  message = [
111
- "Expected #{attribute.inspect} to be #{expected_type};",
151
+ "Expected #{path.join('.').inspect} to be #{type_desc};",
112
152
  "got #{actual_type} instead"
113
153
  ].join(" ")
114
154
 
115
- reject message, payload
155
+ reject message, target
116
156
  end
117
157
 
118
- def expect_array_with_items(attribute, value, payload = value)
158
+ def expect_array_with_items(path:)
159
+ expect_type(path: path, types: Array)
160
+
161
+ path = prepare_path(path: path)
162
+ value = value_for(path: path)
163
+
119
164
  return unless value.empty?
120
165
 
121
- reject "Expected #{attribute.inspect} to have at least one item", payload
166
+ reject "Expected #{path.join('.').inspect} to have at least one item",
167
+ target
122
168
  end
123
169
 
124
- def expect_required_keys(required_keys, value)
125
- keys = value.keys.map(&:to_sym)
170
+ def expect_required_keys(keys:, path:)
171
+ path = prepare_path(path: path)
172
+ value = value_for(path: path)
173
+ actual_keys = value.keys.map(&:to_sym)
126
174
 
127
- required_keys.each do |key|
128
- next if keys.include?(key)
175
+ keys.each do |key|
176
+ next if actual_keys.include?(key)
129
177
 
130
- reject "Expected #{key.inspect} to be defined", value
178
+ path_desc = if path.empty?
179
+ key.to_s.inspect
180
+ else
181
+ (path + [key]).join(".").inspect
182
+ end
183
+
184
+ reject "Expected #{path_desc} to be defined", target
131
185
  end
132
186
  end
133
187
 
134
- def reject_extraneous_keys(allowed_keys, value)
135
- keys = value.keys.map(&:to_sym)
136
- extraneous = keys.to_a - allowed_keys.to_a
188
+ def reject_extraneous_keys(keys:, path:)
189
+ path = prepare_path(path: path)
190
+ value = value_for(path: path)
191
+
192
+ actual_keys = value.keys.map(&:to_sym)
193
+ extraneous = actual_keys.to_a - keys.to_a
137
194
 
138
195
  return if extraneous.empty?
139
196
 
140
- reject "Unexpected keys: #{extraneous.join(', ')}", value
197
+ path_desc = if path.empty?
198
+ "config"
199
+ else
200
+ path.join(".").inspect
201
+ end
202
+
203
+ reject "#{path_desc} has unexpected keys: #{extraneous.inspect}",
204
+ target
205
+ end
206
+
207
+ def prepare_path(path:)
208
+ path = path.to_s.split(".").map(&:to_sym) unless path.is_a?(Array)
209
+ path
210
+ end
211
+
212
+ def value_for(path:)
213
+ path.empty? ? target : target.dig(*path)
141
214
  end
142
215
  end
143
216
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nJS
4
+ def self.sort_hash(hash)
5
+ return hash unless hash.is_a?(Hash)
6
+
7
+ hash.keys.sort_by(&:to_s).each_with_object({}) do |key, seed|
8
+ value = hash[key]
9
+ seed[key] = value.is_a?(Hash) ? sort_hash(value) : value
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nJS
4
- VERSION = "4.2.0"
4
+ VERSION = "4.2.2"
5
5
  end
data/lib/i18n-js.rb CHANGED
@@ -13,6 +13,8 @@ require "digest/md5"
13
13
  require_relative "i18n-js/schema"
14
14
  require_relative "i18n-js/version"
15
15
  require_relative "i18n-js/plugin"
16
+ require_relative "i18n-js/sort_hash"
17
+ require_relative "i18n-js/clean_hash"
16
18
 
17
19
  module I18nJS
18
20
  MissingConfigError = Class.new(StandardError)
@@ -23,31 +25,35 @@ module I18nJS
23
25
  "you must set either `config_file` or `config`"
24
26
  end
25
27
 
26
- load_plugins!
27
-
28
28
  config = Glob::SymbolizeKeys.call(config || load_config_file(config_file))
29
29
 
30
+ load_plugins!
31
+ initialize_plugins!(config: config)
30
32
  Schema.validate!(config)
33
+
31
34
  exported_files = []
32
35
 
33
- config[:translations].each do |group|
34
- exported_files += export_group(group, config)
35
- end
36
+ config[:translations].each {|group| exported_files += export_group(group) }
36
37
 
37
38
  plugins.each do |plugin|
38
- plugin.after_export(files: exported_files.dup, config: config)
39
+ plugin.after_export(files: exported_files.dup) if plugin.enabled?
39
40
  end
40
41
 
41
42
  exported_files
42
43
  end
43
44
 
44
- def self.export_group(group, config)
45
+ def self.export_group(group)
45
46
  filtered_translations = Glob.filter(translations, group[:patterns])
46
47
  filtered_translations =
47
48
  plugins.reduce(filtered_translations) do |buffer, plugin|
48
- plugin.transform(translations: buffer, config: config)
49
+ if plugin.enabled?
50
+ plugin.transform(translations: buffer)
51
+ else
52
+ buffer
53
+ end
49
54
  end
50
55
 
56
+ filtered_translations = sort_hash(clean_hash(filtered_translations))
51
57
  output_file_path = File.expand_path(group[:file])
52
58
  exported_files = []
53
59
 
@@ -71,6 +77,13 @@ module I18nJS
71
77
  digest = Digest::MD5.hexdigest(contents)
72
78
  file_path = file_path.gsub(/:digest/, digest)
73
79
 
80
+ # Don't rewrite the file if it already exists and has the same content.
81
+ # It helps the asset pipeline or webpack understand that file wasn't
82
+ # changed.
83
+ if File.exist?(file_path) && File.read(file_path) == contents
84
+ return file_path
85
+ end
86
+
74
87
  File.open(file_path, "w") do |file|
75
88
  file << contents
76
89
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-js
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 4.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nando Vieira
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-12-11 00:00:00.000000000 Z
11
+ date: 2022-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glob
@@ -197,6 +197,7 @@ files:
197
197
  - lib/guard/i18n-js/templates/Guardfile
198
198
  - lib/guard/i18n-js/version.rb
199
199
  - lib/i18n-js.rb
200
+ - lib/i18n-js/clean_hash.rb
200
201
  - lib/i18n-js/cli.rb
201
202
  - lib/i18n-js/cli/check_command.rb
202
203
  - lib/i18n-js/cli/command.rb
@@ -214,6 +215,7 @@ files:
214
215
  - lib/i18n-js/listen.rb
215
216
  - lib/i18n-js/plugin.rb
216
217
  - lib/i18n-js/schema.rb
218
+ - lib/i18n-js/sort_hash.rb
217
219
  - lib/i18n-js/version.rb
218
220
  - package.json
219
221
  homepage: https://github.com/fnando/i18n-js
@@ -223,10 +225,10 @@ metadata:
223
225
  rubygems_mfa_required: 'true'
224
226
  homepage_uri: https://github.com/fnando/i18n-js
225
227
  bug_tracker_uri: https://github.com/fnando/i18n-js/issues
226
- source_code_uri: https://github.com/fnando/i18n-js/tree/v4.2.0
227
- changelog_uri: https://github.com/fnando/i18n-js/tree/v4.2.0/CHANGELOG.md
228
- documentation_uri: https://github.com/fnando/i18n-js/tree/v4.2.0/README.md
229
- license_uri: https://github.com/fnando/i18n-js/tree/v4.2.0/LICENSE.md
228
+ source_code_uri: https://github.com/fnando/i18n-js/tree/v4.2.2
229
+ changelog_uri: https://github.com/fnando/i18n-js/tree/v4.2.2/CHANGELOG.md
230
+ documentation_uri: https://github.com/fnando/i18n-js/tree/v4.2.2/README.md
231
+ license_uri: https://github.com/fnando/i18n-js/tree/v4.2.2/LICENSE.md
230
232
  post_install_message:
231
233
  rdoc_options: []
232
234
  require_paths:
@@ -242,7 +244,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
244
  - !ruby/object:Gem::Version
243
245
  version: '0'
244
246
  requirements: []
245
- rubygems_version: 3.3.26
247
+ rubygems_version: 3.4.1
246
248
  signing_key:
247
249
  specification_version: 4
248
250
  summary: Export i18n translations and use them on JavaScript.