i18n-js 4.0.0 → 4.2.0

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.
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img width="250" height="58" src="https://github.com/fnando/i18n-js/raw/main/i18njs.png" alt="i18n.js">
2
+ <img width="250" height="58" src="https://github.com/fnando/i18n-js/raw/main/images/i18njs.png" alt="i18n.js">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -12,14 +12,14 @@
12
12
  <small>
13
13
  Oh, you don't use Ruby? No problem! You can still use i18n-js
14
14
  <br>
15
- and the
16
- <a href="https://www.npmjs.com/package/i18n-js/v/next">companion JavaScript package</a>.
15
+ and the
16
+ <a href="https://www.npmjs.com/package/i18n-js/v/latest">companion JavaScript package</a>.
17
17
  </small>
18
18
  </p>
19
19
 
20
20
  <p align="center">
21
21
  <a href="https://github.com/fnando/i18n-js"><img src="https://github.com/fnando/i18n-js/workflows/ruby-tests/badge.svg" alt="Tests"></a>
22
- <a href="https://rubygems.org/gems/i18n-js"><img src="https://img.shields.io/gem/v/i18n-js.svg?include_prereleases" alt="Gem"></a>
22
+ <a href="https://rubygems.org/gems/i18n-js"><img src="https://img.shields.io/gem/v/i18n-js.svg" alt="Gem"></a>
23
23
  <a href="https://rubygems.org/gems/i18n-js"><img src="https://img.shields.io/gem/dt/i18n-js.svg" alt="Gem"></a>
24
24
  <a href="https://tldrlegal.com/license/mit-license"><img src="https://img.shields.io/:License-MIT-blue.svg" alt="MIT License"></a>
25
25
  </p>
@@ -45,6 +45,14 @@ About patterns:
45
45
  - `*.messages.*`
46
46
  - Patterns starting with `!` are excluded.
47
47
  - `!*.activerecord.*` will exclude all ActiveRecord translations.
48
+ - You can use groups:
49
+ - `{pt-BR,en}.js.*` will include only `pt-BR` and `en` translations, even if
50
+ more languages are available.
51
+
52
+ > **Note**:
53
+ >
54
+ > Patterns use [glob](https://rubygems.org/gems/glob), so check it out for the
55
+ > most up-to-date documentation about what's available.
48
56
 
49
57
  The config file:
50
58
 
@@ -68,6 +76,22 @@ The output path can use the following placeholders:
68
76
  - `:locale`: the language that's being exported.
69
77
  - `:digest`: the MD5 hex digest of the exported file.
70
78
 
79
+ The config file is processed as erb, so you can have dynamic content on it if
80
+ you want. The following example shows how to use groups from a variable.
81
+
82
+ ```yml
83
+ ---
84
+ <% group = "{en,pt}" %>
85
+
86
+ translations:
87
+ - file: app/frontend/translations.json
88
+ patterns:
89
+ - "<%= group %>.*"
90
+ - "!<%= group %>.activerecord"
91
+ - "!<%= group %>.errors"
92
+ - "!<%= group %>.number.nth"
93
+ ```
94
+
71
95
  The Ruby API:
72
96
 
73
97
  ```ruby
@@ -88,7 +112,9 @@ Commands:
88
112
  - init: Initialize a project
89
113
  - export: Export translations as JSON files
90
114
  - version: Show package version
91
- - check: Check for missing translations
115
+ - plugins: List plugins that will be activated
116
+ - lint:translations: Check for missing translations
117
+ - lint:scripts: Lint files using TypeScript
92
118
 
93
119
  Run `i18n COMMAND --help` for more information on specific commands.
94
120
  ```
@@ -97,14 +123,174 @@ By default, `i18n` will use `config/i18n.yml` and `config/environment.rb` as the
97
123
  configuration files. If you don't have these files, then you'll need to specify
98
124
  both `--config` and `--require`.
99
125
 
126
+ ### Plugins
127
+
128
+ #### Built-in plugins:
129
+
130
+ ##### `embed_fallback_translations`:
131
+
132
+ Embed fallback translations inferred from the default locale. This can be useful
133
+ in cases where you have multiple large translation files and don't want to load
134
+ the default locale together with the target locale.
135
+
136
+ To use it, add the following to your configuration file:
137
+
138
+ ```yaml
139
+ embed_fallback_translations:
140
+ enabled: true
141
+ ```
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
+
209
+ #### Plugin API
210
+
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:
217
+
218
+ ```ruby
219
+ # frozen_string_literal: true
220
+
221
+ module I18nJS
222
+ class Plugin
223
+ # This method is responsible for transforming the translations. The
224
+ # translations you'll receive may be already be filtered by other plugins
225
+ # and by the default filtering itself. If you need to access the original
226
+ # 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:)
232
+ translations
233
+ end
234
+
235
+ # In case your plugin accepts configuration, this is where you must validate
236
+ # the configuration, making sure only valid keys and type is provided.
237
+ # If the configuration contains invalid data, then you must raise an
238
+ # exception using something like
239
+ # `raise I18nJS::Schema::InvalidError, error_message`.
240
+ def self.validate_schema(config:)
241
+ end
242
+
243
+ # This method must set up the basic plugin configuration, like adding the
244
+ # config's root key in case your plugin accepts configuration (defined via
245
+ # the config file).
246
+ #
247
+ # If you don't add this key, the linter will prevent non-default keys from
248
+ # being added to the configuration file.
249
+ def self.setup
250
+ end
251
+
252
+ # This method is called whenever `I18nJS.call(**kwargs)` finishes exporting
253
+ # JSON files based on your configuration.
254
+ #
255
+ # You can use it to further process exported files, or generate new files
256
+ # 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:)
261
+ end
262
+ end
263
+ end
264
+ ```
265
+
266
+ To distribute this plugin, you need to create a gem package that matches the
267
+ pattern `i18n-js/*_plugin.rb`. You can test whether your plugin will be found by
268
+ installing your gem, opening a iRB session and running
269
+ `Gem.find_files("i18n-js/*_plugin.rb")`. If your plugin is not listed, then you
270
+ need to double check your gem load path and see why the file is not being
271
+ loaded.
272
+
100
273
  ### Listing missing translations
101
274
 
102
- To list missing and extraneous translations, you can use `i18n check`. This
103
- command will load your translations similarly to how `i18n export` does, but
104
- will output the list of keys that don't have a matching translation against the
105
- default locale. Here's an example:
275
+ To list missing and extraneous translations, you can use
276
+ `i18n lint:translations`. This command will load your translations similarly to
277
+ how `i18n export` does, but will output the list of keys that don't have a
278
+ matching translation against the default locale. Here's an example:
106
279
 
107
- ![`i18n check` command in action](https://github.com/fnando/i18n-js/raw/main/images/i18njs-check.gif)
280
+ ```console
281
+ $ i18n lint:translations
282
+ => Config file: "./config/i18n.yml"
283
+ => Require file: "./config/environment.rb"
284
+ => Check "./config/i18n.yml" for ignored keys.
285
+ => en: 232 translations
286
+ => pt-BR: 5 missing, 1 extraneous, 1 ignored
287
+ - pt-BR.actors.github.metrics (missing)
288
+ - pt-BR.actors.github.metrics_hint (missing)
289
+ - pt-BR.actors.github.repo_metrics (missing)
290
+ - pt-BR.actors.github.repository (missing)
291
+ - pt-BR.actors.github.user_metrics (missing)
292
+ - pt-BR.github.repository (extraneous)
293
+ ```
108
294
 
109
295
  This command will exist with status 1 whenever there are missing translations.
110
296
  This way you can use it as a CI linting.
@@ -125,19 +311,97 @@ translations:
125
311
  patterns:
126
312
  - "*"
127
313
 
128
- check:
314
+ lint_translations:
129
315
  ignore:
130
316
  - en.mailer.login.subject
131
317
  - en.mailer.login.body
132
318
  ```
133
319
 
134
- > **Note**: In order to avoid mistakenly ignoring keys, this configuration
135
- > option only accepts the full translation scope, rather than accepting a
136
- > pattern like `pt.ignored.scope.*`.
320
+ > **Note**:
321
+ >
322
+ > In order to avoid mistakenly ignoring keys, this configuration option only
323
+ > accepts the full translation scope, rather than accepting a pattern like
324
+ > `pt.ignored.scope.*`.
325
+
326
+ ### Linting your JavaScript/TypeScript files
327
+
328
+ To lint your script files and check for missing translations (which can signal
329
+ that you're either using wrong scopes or forgot to add the translation), use
330
+ `i18n lint:scripts`. This command will parse your JavaScript/TypeScript files
331
+ and extract all scopes being used. This command requires a Node.js runtime. You
332
+ can either specify one via `--node-path`, or let the plugin infer a binary from
333
+ your `$PATH`.
334
+
335
+ The comparison will be made against the export JSON files, which means it'll
336
+ consider transformations performed by plugins (e.g. the output files may be
337
+ affected by `embed_fallback_translations` plugin).
338
+
339
+ The translations that will be extract must be called as one of the following
340
+ ways:
341
+
342
+ - `i18n.t(scope, options)`
343
+ - `i18n.translate(scope, options)`
344
+ - `t(scope, options)`
345
+
346
+ Notice that only literal strings can be used, as in `i18n.t("message")`. If
347
+ you're using dynamic scoping through variables (e.g.
348
+ `const scope = "message"; i18n.t(scope)`), they will be skipped.
349
+
350
+ ```console
351
+ $ i18n lint:scripts
352
+ => Config file: "./config/i18n.yml"
353
+ => Require file: "./config/environment.rb"
354
+ => Node: "/Users/fnando/.asdf/shims/node"
355
+ => Available locales: [:en, :es, :pt]
356
+ => Patterns: ["!(node_modules)/**/*.js", "!(node_modules)/**/*.ts", "!(node_modules)/**/*.jsx", "!(node_modules)/**/*.tsx"]
357
+ => 9 translations, 11 missing, 4 ignored
358
+ - test/scripts/lint/file.js:1:1: en.js.missing
359
+ - test/scripts/lint/file.js:1:1: es.js.missing
360
+ - test/scripts/lint/file.js:1:1: pt.js.missing
361
+ - test/scripts/lint/file.js:2:8: en.base.js.missing
362
+ - test/scripts/lint/file.js:2:8: es.base.js.missing
363
+ - test/scripts/lint/file.js:2:8: pt.base.js.missing
364
+ - test/scripts/lint/file.js:4:8: en.js.missing
365
+ - test/scripts/lint/file.js:4:8: es.js.missing
366
+ - test/scripts/lint/file.js:4:8: pt.js.missing
367
+ - test/scripts/lint/file.js:6:1: en.another_ignore_scope
368
+ - test/scripts/lint/file.js:6:1: es.another_ignore_scope
369
+ ```
370
+
371
+ This command will list all locales and their missing translations. Avoid listing
372
+ a particular translation, you can set `lint.ignore` on your config file.
373
+
374
+ ```yaml
375
+ ---
376
+ translations:
377
+ - file: app/frontend/translations.json
378
+ patterns:
379
+ - "*"
380
+
381
+ lint_scripts:
382
+ ignore:
383
+ - ignore_scope # will ignore this scope on all languages
384
+ - pt.another_ignore_scope # will ignore this scope only on `pt`
385
+ ```
386
+
387
+ You can also set the patterns that will be looked up. By default, it scans all
388
+ JavaScript and TypeScript files that don't live on `node_modules`.
389
+
390
+ ```yaml
391
+ ---
392
+ translations:
393
+ - file: app/frontend/translations.json
394
+ patterns:
395
+ - "*"
396
+
397
+ lint:
398
+ patterns:
399
+ - "app/assets/**/*.ts"
400
+ ```
137
401
 
138
402
  ## Automatically export translations
139
403
 
140
- ### Using watchman
404
+ ### Using [watchman](https://facebook.github.io/watchman/)
141
405
 
142
406
  Create a script at `bin/i18n-watch`.
143
407
 
@@ -182,11 +446,11 @@ line to your Procfile:
182
446
  i18n: ./bin/i18n-watch
183
447
  ```
184
448
 
185
- ### Using guard
449
+ ### Using [guard](https://rubygems.org/gems/guard)
186
450
 
187
- Install [guard](https://rubygems.org/packages/guard) and
188
- [guard-compat](https://rubygems.org/packages/guard-compat). Then create a
189
- Guardfile with the following configuration:
451
+ Install [guard](https://rubygems.org/gems/guard) and
452
+ [guard-compat](https://rubygems.org/gems/guard-compat). Then create a Guardfile
453
+ with the following configuration:
190
454
 
191
455
  ```ruby
192
456
  guard(:"i18n-js",
@@ -204,7 +468,7 @@ accordingly.
204
468
 
205
469
  Now you can run `guard start -i`.
206
470
 
207
- ### Using listen
471
+ ### Using [listen](https://rubygems.org/gems/listen)
208
472
 
209
473
  Create a file under `config/initializers/i18n.rb` with the following content:
210
474
 
@@ -216,8 +480,25 @@ end
216
480
  ```
217
481
 
218
482
  The code above will watch for changes based on `config/i18n.yml` and
219
- `config/locales`. You can customize these options with
220
- `I18nJS.listen(config_file: "config/i18n.yml", locales_dir: "config/locales")`.
483
+ `config/locales`. You can customize these options:
484
+
485
+ - `config_file` - i18n-js configuration file
486
+ - `locales_dir` - one or multiple directories to watch for locales changes
487
+ - `options` - passed directly to
488
+ [listen](https://github.com/guard/listen/#options)
489
+ - `run_on_start` - export files on start. Defaults to `true`. When disabled,
490
+ files will be exported only when there are file changes.
491
+
492
+ Example:
493
+
494
+ ```ruby
495
+ I18nJS.listen(
496
+ config_file: "config/i18n.yml",
497
+ locales_dir: ["config/locales", "app/views"],
498
+ options: {only: %r{.yml$}},
499
+ run_on_start: false
500
+ )
501
+ ```
221
502
 
222
503
  ### Integrating with your frontend
223
504
 
@@ -227,6 +508,13 @@ that loads all the exported translation.
227
508
 
228
509
  ### FAQ
229
510
 
511
+ #### I'm running v3. Is there a migration plan?
512
+
513
+ [There's a document](https://github.com/fnando/i18n-js/tree/main/MIGRATING_FROM_V3_TO_V4.md)
514
+ 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
516
+ during the migration is not outline is that document.
517
+
230
518
  #### How can I export translations without having a database around?
231
519
 
232
520
  Some people may have a build process using something like Docker that don't
@@ -246,9 +534,11 @@ require "action_view/railtie"
246
534
  I18n.load_path += Dir["./config/locales/**/*.yml"]
247
535
  ```
248
536
 
249
- Notice that you may not need to load ActiveSupport and ActionView lines, or even
250
- may need to add additional requires for other libs. With this approach you have
251
- full control on what's going to be loaded.
537
+ > **Note**:
538
+ >
539
+ > You may not need to load ActiveSupport and ActionView lines, or even may need
540
+ > to add additional requires for other libs. With this approach you have full
541
+ > control on what's going to be loaded.
252
542
 
253
543
  ## Maintainer
254
544
 
data/bin/release ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require_relative "../lib/i18n-js/version"
6
+
7
+ def write_file(path, contents)
8
+ File.open(File.expand_path(path), "w") do |io|
9
+ io << contents
10
+ end
11
+ end
12
+
13
+ changelog_path = "./CHANGELOG.md"
14
+ version_path = "./lib/i18n-js/version.rb"
15
+
16
+ version = nil
17
+ segments = I18nJS::VERSION.split(".")
18
+ major, minor, patch = *segments.take(3).map(&:to_i)
19
+ pre = segments[4].to_s
20
+ pre_version = pre.gsub(/[^\d]/m, "").to_i
21
+ date = Time.now.strftime("%b %d, %Y")
22
+ dry_run = false
23
+ alpha = false
24
+
25
+ OptionParser.new do |opts|
26
+ opts.on("--major") do
27
+ version = "#{major + 1}.0.0"
28
+ end
29
+
30
+ opts.on("--minor") do
31
+ version = "#{major}.#{minor + 1}.0"
32
+ end
33
+
34
+ opts.on("--patch") do
35
+ version = "#{major}.#{minor}.#{patch + 1}"
36
+ end
37
+
38
+ opts.on("--alpha") do
39
+ alpha = true
40
+ end
41
+
42
+ opts.on("--dry-run") do
43
+ dry_run = true
44
+ end
45
+ end.parse!
46
+
47
+ version = "#{version}.alpha#{pre_version + 1}" if alpha
48
+
49
+ unless version
50
+ puts "ERROR: You need to use either one of: --major, --minor, --patch"
51
+ exit 1
52
+ end
53
+
54
+ puts "=> Current version: #{I18nJS::VERSION}"
55
+ puts "=> Next version: #{version}"
56
+
57
+ system "yarn", "install"
58
+ system "yarn", "compile"
59
+
60
+ write_file changelog_path,
61
+ File.read(changelog_path)
62
+ .gsub("Unreleased", "v#{version} - #{date}")
63
+
64
+ puts "=> Updated #{changelog_path}"
65
+
66
+ write_file version_path,
67
+ File.read(version_path)
68
+ .gsub(/VERSION = ".*?"/, %[VERSION = "#{version}"])
69
+
70
+ puts "=> Updated #{version_path}"
71
+
72
+ unless dry_run
73
+ system "git", "add", changelog_path, version_path
74
+ system "git", "commit", "-m", "Bump up version (v#{version})"
75
+ system "rake", "release"
76
+ end
77
+
78
+ if dry_run
79
+ system "rake", "build"
80
+ system "git", "checkout", changelog_path, version_path
81
+ end
data/i18n-js.gemspec CHANGED
@@ -28,19 +28,22 @@ Gem::Specification.new do |spec|
28
28
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
29
  `git ls-files -z`
30
30
  .split("\x0")
31
- .reject {|f| f.match(%r{^(test|spec|features)/}) }
31
+ .reject {|f| f.match(%r{^(test|spec|features|images)/}) }
32
32
  end
33
33
 
34
+ spec.files << "lib/i18n-js/lint.js"
35
+
34
36
  spec.bindir = "exe"
35
37
  spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
36
38
  spec.require_paths = ["lib"]
37
39
 
38
- spec.add_dependency "glob"
40
+ spec.add_dependency "glob", ">= 0.4.0"
39
41
  spec.add_dependency "i18n"
40
42
 
41
43
  spec.add_development_dependency "activesupport"
42
44
  spec.add_development_dependency "minitest"
43
45
  spec.add_development_dependency "minitest-utils"
46
+ spec.add_development_dependency "mocha"
44
47
  spec.add_development_dependency "pry-meta"
45
48
  spec.add_development_dependency "rake"
46
49
  spec.add_development_dependency "rubocop"
data/lib/guard/i18n-js.rb CHANGED
@@ -53,13 +53,30 @@ module Guard
53
53
  info("Changes detected: #{changes.join(', ')}") if changes
54
54
 
55
55
  @current_thread = Thread.new do
56
- require @require_file
57
- ::I18nJS.call(config_file: @config_file)
56
+ capture do
57
+ system "i18n",
58
+ "export",
59
+ "--config",
60
+ config_file.to_s,
61
+ "--require",
62
+ require_file.to_s,
63
+ "--quiet"
64
+ end
58
65
  end
59
66
 
60
67
  current_thread.join
61
68
  end
62
69
 
70
+ def capture
71
+ original = $stdout
72
+ $stdout = StringIO.new
73
+ yield
74
+ rescue StandardError
75
+ # noop
76
+ ensure
77
+ $stdout = original
78
+ end
79
+
63
80
  def validate_file(key, file)
64
81
  return true if file && File.file?(file)
65
82
 
@@ -68,11 +85,11 @@ module Guard
68
85
  end
69
86
 
70
87
  def error(message)
71
- ::Guard::UI.error "[guard-i18n-js] #{message}"
88
+ ::Guard::UI.error "[i18n-js] #{message}"
72
89
  end
73
90
 
74
91
  def info(message)
75
- ::Guard::UI.info "[guard-i18n-js] #{message}"
92
+ ::Guard::UI.info "[i18n-js] #{message}"
76
93
  end
77
94
  end
78
95
  end