i18n-js 4.1.0 → 4.2.1
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/.github/workflows/ruby-tests.yml +1 -1
- data/CHANGELOG.md +18 -0
- data/MIGRATING_FROM_V3_TO_V4.md +4 -4
- data/README.md +123 -68
- data/bin/{pack → release} +9 -7
- data/lib/i18n-js/embed_fallback_translations_plugin.rb +46 -54
- data/lib/i18n-js/export_files_plugin.rb +103 -0
- data/lib/i18n-js/plugin.rb +72 -7
- data/lib/i18n-js/schema.rb +122 -49
- data/lib/i18n-js/version.rb +1 -1
- data/lib/i18n-js.rb +13 -6
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0545bfe4fee26d8a5aed4aec6262e8a3db733ac951dbb0111f05e8fb2488e516
|
4
|
+
data.tar.gz: 50dd6e5e4e019f01e7b72fcfacb71faf2faa73ed7157082c647432b4ef96f263
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5366522c08e868ce0b555a4d0c0d10029bbee9270cb9602600f1ca4f2e89473f02baa577644dd5ae9a38046c3c03abf6c66029d79bac7d4ca41f50ed6ea4b86
|
7
|
+
data.tar.gz: c3a0670b398c51c9bd1e01b163c46b2e69fe8f4ee06a099d8d46dc78ee57da1e78bff51e35db98ddb15b91fe2f422c8a8584fadb0b4066dc191a01eab65adac4
|
data/CHANGELOG.md
CHANGED
@@ -11,6 +11,22 @@ Prefix your message with one of the following:
|
|
11
11
|
- [Security] in case of vulnerabilities.
|
12
12
|
-->
|
13
13
|
|
14
|
+
## v4.2.1 - Dec 25, 2022
|
15
|
+
|
16
|
+
- [Changed] Change plugin api to be based on instance methods. This avoids
|
17
|
+
having to pass in the config for each and every method. It also allows us
|
18
|
+
adding helper methods to the base class.
|
19
|
+
- [Fixed] Fix performance issues with embed fallback translations' initial
|
20
|
+
implementation.
|
21
|
+
|
22
|
+
## v4.2.0 - Dec 10, 2022
|
23
|
+
|
24
|
+
- [Added] Add `I18nJS::Plugin.after_export(files:, config:)` method, that's
|
25
|
+
called whenever whenever I18nJS finishes exporting files. You can use it to
|
26
|
+
further process files, or generate new files based on the exported files.
|
27
|
+
- [Added] Bult-in plugin `I18nJS::ExportFilesPlugin`, which allows exporting
|
28
|
+
files out of the translations file by using a custom template.
|
29
|
+
|
14
30
|
## v4.1.0 - Dec 09, 2022
|
15
31
|
|
16
32
|
- [Added] Parse configuration files as erb.
|
@@ -18,6 +34,8 @@ Prefix your message with one of the following:
|
|
18
34
|
exported during `I18n.listen`'s boot. The default value is `true`.
|
19
35
|
- [Added] Now it's possible to transform translations before exporting them
|
20
36
|
using a stable plugin api.
|
37
|
+
- [Added] Built-in plugin `I18nJS::EmbedFallbackTranslationsPlugin`, which
|
38
|
+
allows embedding missing translations on exported files.
|
21
39
|
- [Deprecated] The `i18n check` has been deprecated. Use
|
22
40
|
`i18n lint:translations` instead.
|
23
41
|
- [Added] Use `i18n lint:scripts` to lint JavaScript/TypeScript.
|
data/MIGRATING_FROM_V3_TO_V4.md
CHANGED
@@ -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/
|
51
|
-
[guard-compat](https://rubygems.org/
|
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`:
|
178
|
-
- `fallbacks`:
|
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
@@ -140,30 +140,91 @@ embed_fallback_translations:
|
|
140
140
|
enabled: true
|
141
141
|
```
|
142
142
|
|
143
|
+
##### `export_files`:
|
144
|
+
|
145
|
+
By default, i18n-js will export only JSON files out of your translations. This
|
146
|
+
plugin allows exporting other file formats. To use it, add the following to your
|
147
|
+
configuration file:
|
148
|
+
|
149
|
+
```yaml
|
150
|
+
export_files:
|
151
|
+
enabled: true
|
152
|
+
files:
|
153
|
+
- template: path/to/template.erb
|
154
|
+
output: "%{dir}/%{base_name}.ts"
|
155
|
+
```
|
156
|
+
|
157
|
+
You can export multiple files by define more entries.
|
158
|
+
|
159
|
+
The output name can use the following placeholders:
|
160
|
+
|
161
|
+
- `%{dir}`: the directory where the translation file is.
|
162
|
+
- `%{name}`: file name with extension.
|
163
|
+
- `%{base_name}`: file name without extension.
|
164
|
+
- `%{digest}`: MD5 hexdigest from the generated file.
|
165
|
+
|
166
|
+
The template file must be a valid eRB template. You can execute arbitrary Ruby
|
167
|
+
code, so be careful. An example of how you can generate a file can be seen
|
168
|
+
below:
|
169
|
+
|
170
|
+
```erb
|
171
|
+
/* eslint-disable */
|
172
|
+
<%= banner %>
|
173
|
+
|
174
|
+
import { i18n } from "config/i18n";
|
175
|
+
|
176
|
+
i18n.store(<%= JSON.pretty_generate(translations) %>);
|
177
|
+
```
|
178
|
+
|
179
|
+
This template is loading the instance from `config/i18n` and storing the
|
180
|
+
translations that have been loaded. The
|
181
|
+
`banner(comment: "// ", include_time: true)` method is built-in. The generated
|
182
|
+
file will look something like this:
|
183
|
+
|
184
|
+
```typescript
|
185
|
+
/* eslint-disable */
|
186
|
+
// File generated by i18n-js on 2022-12-10 15:37:00 +0000
|
187
|
+
|
188
|
+
import { i18n } from "config/i18n";
|
189
|
+
|
190
|
+
i18n.store({
|
191
|
+
en: {
|
192
|
+
"bunny rabbit adventure": "bunny rabbit adventure",
|
193
|
+
"hello sunshine!": "hello sunshine!",
|
194
|
+
"time for bed!": "time for bed!",
|
195
|
+
},
|
196
|
+
es: {
|
197
|
+
"bunny rabbit adventure": "conejito conejo aventura",
|
198
|
+
bye: "adios",
|
199
|
+
"time for bed!": "hora de acostarse!",
|
200
|
+
},
|
201
|
+
pt: {
|
202
|
+
"bunny rabbit adventure": "a aventura da coelhinha",
|
203
|
+
bye: "tchau",
|
204
|
+
"time for bed!": "hora de dormir!",
|
205
|
+
},
|
206
|
+
});
|
207
|
+
```
|
208
|
+
|
143
209
|
#### Plugin API
|
144
210
|
|
145
211
|
You can transform the exported translations by adding plugins. A plugin must
|
146
|
-
inherit from `I18nJS::Plugin` and can have
|
147
|
-
|
148
|
-
implemented.
|
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)
|
149
214
|
|
150
215
|
```ruby
|
151
216
|
# frozen_string_literal: true
|
152
217
|
|
153
218
|
module I18nJS
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
219
|
+
class SamplePlugin < I18nJS::Plugin
|
220
|
+
# This method is responsible for transforming the translations. The
|
221
|
+
# translations you'll receive may be already be filtered by other plugins
|
222
|
+
# and by the default filtering itself. If you need to access the original
|
223
|
+
# translations, use `I18nJS.translations`.
|
224
|
+
def transform(translations:)
|
225
|
+
# transform `translations` here…
|
158
226
|
|
159
|
-
|
160
|
-
# config's root key in case your plugin accepts configuration (defined via
|
161
|
-
# the config file).
|
162
|
-
#
|
163
|
-
# If you don't add this key, the linter will prevent non-default keys from
|
164
|
-
# being added to the configuration file.
|
165
|
-
def self.setup
|
166
|
-
I18nJS::Schema.root_keys << CONFIG_KEY
|
227
|
+
translations
|
167
228
|
end
|
168
229
|
|
169
230
|
# In case your plugin accepts configuration, this is where you must validate
|
@@ -171,63 +232,44 @@ module I18nJS
|
|
171
232
|
# If the configuration contains invalid data, then you must raise an
|
172
233
|
# exception using something like
|
173
234
|
# `raise I18nJS::Schema::InvalidError, error_message`.
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
schema = I18nJS::Schema.new(config)
|
180
|
-
|
181
|
-
schema.expect_required_keys(valid_keys, plugin_config)
|
182
|
-
schema.reject_extraneous_keys(valid_keys, plugin_config)
|
183
|
-
schema.expect_enabled_config(CONFIG_KEY, plugin_config[:enabled])
|
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…
|
184
240
|
end
|
185
241
|
|
186
|
-
# This method
|
187
|
-
#
|
188
|
-
#
|
189
|
-
# translations, use `I18nJS.translations`.
|
242
|
+
# This method must set up the basic plugin configuration, like adding the
|
243
|
+
# config's root key in case your plugin accepts configuration (defined via
|
244
|
+
# the config file).
|
190
245
|
#
|
191
|
-
#
|
192
|
-
#
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
translations_glob = Glob.new(translations)
|
198
|
-
translations_glob << "*"
|
199
|
-
|
200
|
-
mapping = translations.keys.each_with_object({}) do |locale, buffer|
|
201
|
-
buffer[locale] = Glob.new(translations[locale]).tap do |glob|
|
202
|
-
glob << "*"
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
default_locale = I18n.default_locale
|
207
|
-
default_locale_glob = mapping.delete(default_locale)
|
208
|
-
default_locale_paths = default_locale_glob.paths
|
209
|
-
|
210
|
-
mapping.each do |locale, glob|
|
211
|
-
missing_keys = default_locale_paths - glob.paths
|
212
|
-
|
213
|
-
missing_keys.each do |key|
|
214
|
-
components = key.split(".").map(&:to_sym)
|
215
|
-
fallback_translation = translations.dig(default_locale, *components)
|
216
|
-
|
217
|
-
next unless fallback_translation
|
218
|
-
|
219
|
-
translations_glob.set([locale, key].join("."), fallback_translation)
|
220
|
-
end
|
221
|
-
end
|
246
|
+
# If you don't add this key, the linter will prevent non-default keys from
|
247
|
+
# being added to the configuration file.
|
248
|
+
def setup
|
249
|
+
# If you plugin has configuration, uncomment the line below
|
250
|
+
# I18nJS::Schema.root_keys << config_key
|
251
|
+
end
|
222
252
|
|
223
|
-
|
253
|
+
# This method is called whenever `I18nJS.call(**kwargs)` finishes exporting
|
254
|
+
# JSON files based on your configuration.
|
255
|
+
#
|
256
|
+
# You can use it to further process exported files, or generate new files
|
257
|
+
# based on the translations that have been exported.
|
258
|
+
def after_export(files:)
|
259
|
+
# process exported files here…
|
224
260
|
end
|
225
261
|
end
|
226
|
-
|
227
|
-
I18nJS.register_plugin(EmbedFallbackTranslationsPlugin)
|
228
262
|
end
|
229
263
|
```
|
230
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
|
+
|
231
273
|
To distribute this plugin, you need to create a gem package that matches the
|
232
274
|
pattern `i18n-js/*_plugin.rb`. You can test whether your plugin will be found by
|
233
275
|
installing your gem, opening a iRB session and running
|
@@ -242,7 +284,20 @@ To list missing and extraneous translations, you can use
|
|
242
284
|
how `i18n export` does, but will output the list of keys that don't have a
|
243
285
|
matching translation against the default locale. Here's an example:
|
244
286
|
|
245
|
-
|
287
|
+
```console
|
288
|
+
$ i18n lint:translations
|
289
|
+
=> Config file: "./config/i18n.yml"
|
290
|
+
=> Require file: "./config/environment.rb"
|
291
|
+
=> Check "./config/i18n.yml" for ignored keys.
|
292
|
+
=> en: 232 translations
|
293
|
+
=> pt-BR: 5 missing, 1 extraneous, 1 ignored
|
294
|
+
- pt-BR.actors.github.metrics (missing)
|
295
|
+
- pt-BR.actors.github.metrics_hint (missing)
|
296
|
+
- pt-BR.actors.github.repo_metrics (missing)
|
297
|
+
- pt-BR.actors.github.repository (missing)
|
298
|
+
- pt-BR.actors.github.user_metrics (missing)
|
299
|
+
- pt-BR.github.repository (extraneous)
|
300
|
+
```
|
246
301
|
|
247
302
|
This command will exist with status 1 whenever there are missing translations.
|
248
303
|
This way you can use it as a CI linting.
|
@@ -300,9 +355,9 @@ you're using dynamic scoping through variables (e.g.
|
|
300
355
|
`const scope = "message"; i18n.t(scope)`), they will be skipped.
|
301
356
|
|
302
357
|
```console
|
303
|
-
$ i18n lint:scripts
|
304
|
-
=> Config file: "
|
305
|
-
=> Require file: "
|
358
|
+
$ i18n lint:scripts
|
359
|
+
=> Config file: "./config/i18n.yml"
|
360
|
+
=> Require file: "./config/environment.rb"
|
306
361
|
=> Node: "/Users/fnando/.asdf/shims/node"
|
307
362
|
=> Available locales: [:en, :es, :pt]
|
308
363
|
=> Patterns: ["!(node_modules)/**/*.js", "!(node_modules)/**/*.ts", "!(node_modules)/**/*.jsx", "!(node_modules)/**/*.tsx"]
|
@@ -464,7 +519,7 @@ that loads all the exported translation.
|
|
464
519
|
|
465
520
|
[There's a document](https://github.com/fnando/i18n-js/tree/main/MIGRATING_FROM_V3_TO_V4.md)
|
466
521
|
outlining some of the things you need to do to migrate from v3 to v4. It may not
|
467
|
-
be as complete as we'd like it to be, so let
|
522
|
+
be as complete as we'd like it to be, so let us know if you face any issues
|
468
523
|
during the migration is not outline is that document.
|
469
524
|
|
470
525
|
#### How can I export translations without having a database around?
|
data/bin/{pack → release}
RENAMED
@@ -19,7 +19,7 @@ major, minor, patch = *segments.take(3).map(&:to_i)
|
|
19
19
|
pre = segments[4].to_s
|
20
20
|
pre_version = pre.gsub(/[^\d]/m, "").to_i
|
21
21
|
date = Time.now.strftime("%b %d, %Y")
|
22
|
-
|
22
|
+
dry_run = false
|
23
23
|
alpha = false
|
24
24
|
|
25
25
|
OptionParser.new do |opts|
|
@@ -39,8 +39,8 @@ OptionParser.new do |opts|
|
|
39
39
|
alpha = true
|
40
40
|
end
|
41
41
|
|
42
|
-
opts.on("--
|
43
|
-
|
42
|
+
opts.on("--dry-run") do
|
43
|
+
dry_run = true
|
44
44
|
end
|
45
45
|
end.parse!
|
46
46
|
|
@@ -69,11 +69,13 @@ write_file version_path,
|
|
69
69
|
|
70
70
|
puts "=> Updated #{version_path}"
|
71
71
|
|
72
|
-
|
72
|
+
unless dry_run
|
73
73
|
system "git", "add", changelog_path, version_path
|
74
74
|
system "git", "commit", "-m", "Bump up version (v#{version})"
|
75
|
-
system "
|
75
|
+
system "rake", "release"
|
76
76
|
end
|
77
77
|
|
78
|
-
|
79
|
-
system "
|
78
|
+
if dry_run
|
79
|
+
system "rake", "build"
|
80
|
+
system "git", "checkout", changelog_path, version_path
|
81
|
+
end
|
@@ -4,73 +4,65 @@ module I18nJS
|
|
4
4
|
require "i18n-js/plugin"
|
5
5
|
|
6
6
|
class EmbedFallbackTranslationsPlugin < I18nJS::Plugin
|
7
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
# If you don't add this key, the linter will prevent non-default keys from
|
14
|
-
# being added to the configuration file.
|
15
|
-
def self.setup
|
16
|
-
I18nJS::Schema.root_keys << CONFIG_KEY
|
33
|
+
def self.deep_merge(target_hash, hash)
|
34
|
+
target_hash.merge(hash, &MERGER)
|
35
|
+
end
|
17
36
|
end
|
18
37
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
# exception using something like
|
23
|
-
# `raise I18nJS::Schema::InvalidError, error_message`.
|
24
|
-
def self.validate_schema(config:)
|
25
|
-
return unless config.key?(CONFIG_KEY)
|
38
|
+
def setup
|
39
|
+
I18nJS::Schema.root_keys << config_key
|
40
|
+
end
|
26
41
|
|
27
|
-
|
42
|
+
def validate_schema
|
28
43
|
valid_keys = %i[enabled]
|
29
|
-
schema = I18nJS::Schema.new(config)
|
30
44
|
|
31
|
-
schema.expect_required_keys(valid_keys,
|
32
|
-
schema.reject_extraneous_keys(valid_keys,
|
33
|
-
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])
|
34
47
|
end
|
35
48
|
|
36
|
-
|
37
|
-
|
38
|
-
# and by the default filtering itself. If you need to access the original
|
39
|
-
# translations, use `I18nJS.translations`.
|
40
|
-
#
|
41
|
-
# Make sure you always check whether your plugin is active before
|
42
|
-
# transforming translations; otherwise, opting out transformation won't be
|
43
|
-
# possible.
|
44
|
-
def self.transform(translations:, config:)
|
45
|
-
return translations unless config.dig(CONFIG_KEY, :enabled)
|
49
|
+
def transform(translations:)
|
50
|
+
return translations unless enabled?
|
46
51
|
|
47
|
-
|
48
|
-
|
52
|
+
fallback_locale = I18n.default_locale.to_sym
|
53
|
+
locales_to_fallback = translations.keys - [fallback_locale]
|
49
54
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
end
|
55
|
+
translations_with_fallback = {}
|
56
|
+
translations_with_fallback[fallback_locale] =
|
57
|
+
translations[fallback_locale]
|
55
58
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
mapping.each do |locale, glob|
|
61
|
-
missing_keys = default_locale_paths - glob.paths
|
62
|
-
|
63
|
-
missing_keys.each do |key|
|
64
|
-
components = key.split(".").map(&:to_sym)
|
65
|
-
fallback_translation = translations.dig(default_locale, *components)
|
66
|
-
|
67
|
-
next unless fallback_translation
|
68
|
-
|
69
|
-
translations_glob.set([locale, key].join("."), fallback_translation)
|
70
|
-
end
|
59
|
+
locales_to_fallback.each do |locale|
|
60
|
+
translations_with_fallback[locale] = Utils.deep_merge(
|
61
|
+
translations[fallback_locale], translations[locale]
|
62
|
+
)
|
71
63
|
end
|
72
64
|
|
73
|
-
|
65
|
+
translations_with_fallback
|
74
66
|
end
|
75
67
|
end
|
76
68
|
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
require "i18n-js/plugin"
|
5
|
+
|
6
|
+
class ExportFilesPlugin < I18nJS::Plugin
|
7
|
+
def setup
|
8
|
+
I18nJS::Schema.root_keys << config_key
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_schema
|
12
|
+
valid_keys = %i[enabled files]
|
13
|
+
|
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]
|
20
|
+
|
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
|
42
|
+
|
43
|
+
def after_export(files:)
|
44
|
+
require "erb"
|
45
|
+
require "digest/md5"
|
46
|
+
require "json"
|
47
|
+
|
48
|
+
files.each do |file|
|
49
|
+
dir = File.dirname(file)
|
50
|
+
name = File.basename(file)
|
51
|
+
extension = File.extname(name)
|
52
|
+
base_name = File.basename(file, extension)
|
53
|
+
|
54
|
+
config[:files].each do |export|
|
55
|
+
translations = JSON.load_file(file)
|
56
|
+
template = Template.new(
|
57
|
+
file: file,
|
58
|
+
translations: translations,
|
59
|
+
template: export[:template]
|
60
|
+
)
|
61
|
+
|
62
|
+
contents = template.render
|
63
|
+
|
64
|
+
output = format(
|
65
|
+
export[:output],
|
66
|
+
dir: dir,
|
67
|
+
name: name,
|
68
|
+
extension: extension,
|
69
|
+
digest: Digest::MD5.hexdigest(contents),
|
70
|
+
base_name: base_name
|
71
|
+
)
|
72
|
+
|
73
|
+
File.open(output, "w") do |io|
|
74
|
+
io << contents
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Template
|
81
|
+
attr_accessor :file, :translations, :template
|
82
|
+
|
83
|
+
def initialize(**kwargs)
|
84
|
+
kwargs.each do |key, value|
|
85
|
+
public_send("#{key}=", value)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def banner(comment: "// ", include_time: true)
|
90
|
+
[
|
91
|
+
"#{comment}File generated by i18n-js",
|
92
|
+
include_time ? " on #{Time.now}" : nil
|
93
|
+
].compact.join
|
94
|
+
end
|
95
|
+
|
96
|
+
def render
|
97
|
+
ERB.new(File.read(template)).result(binding)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
I18nJS.register_plugin(ExportFilesPlugin)
|
103
|
+
end
|
data/lib/i18n-js/plugin.rb
CHANGED
@@ -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
|
-
|
12
|
-
plugin.setup
|
15
|
+
available_plugins << plugin
|
13
16
|
end
|
14
17
|
|
15
18
|
def self.plugin_files
|
@@ -22,17 +25,79 @@ 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
|
26
|
-
|
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
|
+
|
70
|
+
# This method is responsible for transforming the translations. The
|
71
|
+
# translations you'll receive may be already be filtered by other plugins
|
72
|
+
# and by the default filtering itself. If you need to access the original
|
73
|
+
# translations, use `I18nJS.translations`.
|
74
|
+
def transform(translations:)
|
27
75
|
translations
|
28
76
|
end
|
29
77
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
78
|
+
# In case your plugin accepts configuration, this is where you must validate
|
79
|
+
# the configuration, making sure only valid keys and type is provided.
|
80
|
+
# If the configuration contains invalid data, then you must raise an
|
81
|
+
# exception using something like
|
82
|
+
# `raise I18nJS::Schema::InvalidError, error_message`.
|
83
|
+
def validate_schema
|
84
|
+
end
|
85
|
+
|
86
|
+
# This method must set up the basic plugin configuration, like adding the
|
87
|
+
# config's root key in case your plugin accepts configuration (defined via
|
88
|
+
# the config file).
|
89
|
+
#
|
90
|
+
# If you don't add this key, the linter will prevent non-default keys from
|
91
|
+
# being added to the configuration file.
|
92
|
+
def setup
|
33
93
|
end
|
34
94
|
|
35
|
-
|
95
|
+
# This method is called whenever `I18nJS.call(**kwargs)` finishes exporting
|
96
|
+
# JSON files based on your configuration.
|
97
|
+
#
|
98
|
+
# You can use it to further process exported files, or generate new files
|
99
|
+
# based on the translations that have been exported.
|
100
|
+
def after_export(files:)
|
36
101
|
end
|
37
102
|
end
|
38
103
|
end
|
data/lib/i18n-js/schema.rb
CHANGED
@@ -25,7 +25,7 @@ module I18nJS
|
|
25
25
|
def self.validate!(target)
|
26
26
|
schema = new(target)
|
27
27
|
schema.validate!
|
28
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
expect_type(:
|
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
|
-
|
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.
|
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(
|
84
|
-
expect_required_keys(
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
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 #{
|
151
|
+
"Expected #{path.join('.').inspect} to be #{type_desc};",
|
112
152
|
"got #{actual_type} instead"
|
113
153
|
].join(" ")
|
114
154
|
|
115
|
-
reject message,
|
155
|
+
reject message, target
|
116
156
|
end
|
117
157
|
|
118
|
-
def expect_array_with_items(
|
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 #{
|
166
|
+
reject "Expected #{path.join('.').inspect} to have at least one item",
|
167
|
+
target
|
122
168
|
end
|
123
169
|
|
124
|
-
def expect_required_keys(
|
125
|
-
|
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
|
-
|
128
|
-
next if
|
175
|
+
keys.each do |key|
|
176
|
+
next if actual_keys.include?(key)
|
129
177
|
|
130
|
-
|
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(
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
data/lib/i18n-js/version.rb
CHANGED
data/lib/i18n-js.rb
CHANGED
@@ -23,25 +23,32 @@ module I18nJS
|
|
23
23
|
"you must set either `config_file` or `config`"
|
24
24
|
end
|
25
25
|
|
26
|
-
load_plugins!
|
27
|
-
|
28
26
|
config = Glob::SymbolizeKeys.call(config || load_config_file(config_file))
|
29
27
|
|
28
|
+
load_plugins!
|
29
|
+
initialize_plugins!(config: config)
|
30
30
|
Schema.validate!(config)
|
31
|
+
|
31
32
|
exported_files = []
|
32
33
|
|
33
|
-
config[:translations].each
|
34
|
-
|
34
|
+
config[:translations].each {|group| exported_files += export_group(group) }
|
35
|
+
|
36
|
+
plugins.each do |plugin|
|
37
|
+
plugin.after_export(files: exported_files.dup) if plugin.enabled?
|
35
38
|
end
|
36
39
|
|
37
40
|
exported_files
|
38
41
|
end
|
39
42
|
|
40
|
-
def self.export_group(group
|
43
|
+
def self.export_group(group)
|
41
44
|
filtered_translations = Glob.filter(translations, group[:patterns])
|
42
45
|
filtered_translations =
|
43
46
|
plugins.reduce(filtered_translations) do |buffer, plugin|
|
44
|
-
plugin.
|
47
|
+
if plugin.enabled?
|
48
|
+
plugin.transform(translations: buffer)
|
49
|
+
else
|
50
|
+
buffer
|
51
|
+
end
|
45
52
|
end
|
46
53
|
|
47
54
|
output_file_path = File.expand_path(group[:file])
|
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.1
|
4
|
+
version: 4.2.1
|
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
|
+
date: 2022-12-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: glob
|
@@ -190,7 +190,7 @@ files:
|
|
190
190
|
- MIGRATING_FROM_V3_TO_V4.md
|
191
191
|
- README.md
|
192
192
|
- Rakefile
|
193
|
-
- bin/
|
193
|
+
- bin/release
|
194
194
|
- exe/i18n
|
195
195
|
- i18n-js.gemspec
|
196
196
|
- lib/guard/i18n-js.rb
|
@@ -208,6 +208,7 @@ files:
|
|
208
208
|
- lib/i18n-js/cli/ui.rb
|
209
209
|
- lib/i18n-js/cli/version_command.rb
|
210
210
|
- lib/i18n-js/embed_fallback_translations_plugin.rb
|
211
|
+
- lib/i18n-js/export_files_plugin.rb
|
211
212
|
- lib/i18n-js/lint.js
|
212
213
|
- lib/i18n-js/lint.ts
|
213
214
|
- lib/i18n-js/listen.rb
|
@@ -222,10 +223,10 @@ metadata:
|
|
222
223
|
rubygems_mfa_required: 'true'
|
223
224
|
homepage_uri: https://github.com/fnando/i18n-js
|
224
225
|
bug_tracker_uri: https://github.com/fnando/i18n-js/issues
|
225
|
-
source_code_uri: https://github.com/fnando/i18n-js/tree/v4.1
|
226
|
-
changelog_uri: https://github.com/fnando/i18n-js/tree/v4.1
|
227
|
-
documentation_uri: https://github.com/fnando/i18n-js/tree/v4.1
|
228
|
-
license_uri: https://github.com/fnando/i18n-js/tree/v4.1
|
226
|
+
source_code_uri: https://github.com/fnando/i18n-js/tree/v4.2.1
|
227
|
+
changelog_uri: https://github.com/fnando/i18n-js/tree/v4.2.1/CHANGELOG.md
|
228
|
+
documentation_uri: https://github.com/fnando/i18n-js/tree/v4.2.1/README.md
|
229
|
+
license_uri: https://github.com/fnando/i18n-js/tree/v4.2.1/LICENSE.md
|
229
230
|
post_install_message:
|
230
231
|
rdoc_options: []
|
231
232
|
require_paths:
|
@@ -241,7 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
241
242
|
- !ruby/object:Gem::Version
|
242
243
|
version: '0'
|
243
244
|
requirements: []
|
244
|
-
rubygems_version: 3.3.
|
245
|
+
rubygems_version: 3.3.7
|
245
246
|
signing_key:
|
246
247
|
specification_version: 4
|
247
248
|
summary: Export i18n translations and use them on JavaScript.
|