i18n-js 4.0.0.alpha3 → 4.0.0

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: 7e0fd83a034e8eaa82e97f46beb39a96da4d671952762f7751d093d4823e1aed
4
- data.tar.gz: acff26df94aa46c0c50d5864c97b32bc1108a611a748c2dd7fff4d626ec41cf4
3
+ metadata.gz: 0b2fdace9fca14d471ff34c3b24055d4093d82ce2a1f72f2df06364a98454a31
4
+ data.tar.gz: 9ea6bd71626357c8fce24c28e991b9edc96f8a3230e55c4d55cf1dec3813c94e
5
5
  SHA512:
6
- metadata.gz: d72a28ddbe75742e605c86d8e3b06ce2e1ada21bc95f2b189e2e120b671c7c0d4d239dfc7c3753abd4b97f57820146001cfdfb89e2c407ae3054ea93fba98d72
7
- data.tar.gz: 5ed38c2ea91915fadcc6cc7a4b718d1473cc41afff0c5cd3f6dd6e1345e2eed29c53bace6403239dd516a180485b055414cd65c00291747d20c6055c6b2f6a2a
6
+ metadata.gz: 2c8da0348d02f04d805a013dadd24beb08fb7de555bfdded51e4e6c7c29ba392978a723b7e95be9d752dda2b35bbe6d6aefe4d4cf9166c1ddca09f003712533f
7
+ data.tar.gz: 882af1975b1f2e479ad4d8bdcbe812175fae165c80a2f915223e14fc71fb0b3236774254e103754169eab5a3d577e219e10803fd05e14fa3bac82471e4c770e1
@@ -4,8 +4,6 @@ name: ruby-tests
4
4
  on:
5
5
  pull_request_target:
6
6
  push:
7
- branches:
8
- - v4
9
7
  workflow_dispatch:
10
8
  inputs: {}
11
9
 
data/CHANGELOG.md CHANGED
@@ -11,6 +11,6 @@ Prefix your message with one of the following:
11
11
  - [Security] in case of vulnerabilities.
12
12
  -->
13
13
 
14
- ## Unreleased
14
+ ## Jul 29, 2022
15
15
 
16
- - Initial release.
16
+ - Official release of i18n-js v4.0.0.
data/README.md CHANGED
@@ -19,13 +19,11 @@
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" alt="Gem"></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>
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>
26
26
 
27
- ![This branch contains the code for v4, our next major release.](https://messages-svg.herokuapp.com/warning.svg?message=This%20branch%20contains%20the%20code%20for%20v4%2C%20our%20next%20major%20release.)
28
-
29
27
  ## Installation
30
28
 
31
29
  ```bash
@@ -35,7 +33,7 @@ gem install i18n-js
35
33
  Or add the following line to your project's Gemfile:
36
34
 
37
35
  ```ruby
38
- gem "i18n-js", "~> 4.0.0.alpha1"
36
+ gem "i18n-js"
39
37
  ```
40
38
 
41
39
  ## Usage
@@ -82,14 +80,61 @@ I18nJS.call(config: config)
82
80
  The CLI API:
83
81
 
84
82
  ```console
85
- $ i18n init --config config/i18n.yml
86
- $ i18n export --config config/i18n.yml --require config/environment.rb
83
+ $ i18n --help
84
+ Usage: i18n COMMAND FLAGS
85
+
86
+ Commands:
87
+
88
+ - init: Initialize a project
89
+ - export: Export translations as JSON files
90
+ - version: Show package version
91
+ - check: Check for missing translations
92
+
93
+ Run `i18n COMMAND --help` for more information on specific commands.
87
94
  ```
88
95
 
89
96
  By default, `i18n` will use `config/i18n.yml` and `config/environment.rb` as the
90
97
  configuration files. If you don't have these files, then you'll need to specify
91
98
  both `--config` and `--require`.
92
99
 
100
+ ### Listing missing translations
101
+
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:
106
+
107
+ ![`i18n check` command in action](https://github.com/fnando/i18n-js/raw/main/images/i18njs-check.gif)
108
+
109
+ This command will exist with status 1 whenever there are missing translations.
110
+ This way you can use it as a CI linting.
111
+
112
+ You can ignore keys by adding a list to the config file:
113
+
114
+ ```yml
115
+ ---
116
+ translations:
117
+ - file: app/frontend/locales/en.json
118
+ patterns:
119
+ - "*"
120
+ - "!*.activerecord"
121
+ - "!*.errors"
122
+ - "!*.number.nth"
123
+
124
+ - file: app/frontend/locales/:locale.:digest.json
125
+ patterns:
126
+ - "*"
127
+
128
+ check:
129
+ ignore:
130
+ - en.mailer.login.subject
131
+ - en.mailer.login.body
132
+ ```
133
+
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.*`.
137
+
93
138
  ## Automatically export translations
94
139
 
95
140
  ### Using watchman
Binary file
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+
5
+ module I18nJS
6
+ class CLI
7
+ class CheckCommand < Command
8
+ command_name "check"
9
+ description "Check for missing translations"
10
+
11
+ parse do |opts|
12
+ opts.banner = "Usage: i18n #{name} [options]"
13
+
14
+ opts.on(
15
+ "-cCONFIG_FILE",
16
+ "--config=CONFIG_FILE",
17
+ "The configuration file that will be used"
18
+ ) do |config_file|
19
+ options[:config_file] = config_file
20
+ end
21
+
22
+ opts.on(
23
+ "-rREQUIRE_FILE",
24
+ "--require=REQUIRE_FILE",
25
+ "A Ruby file that must be loaded"
26
+ ) do |require_file|
27
+ options[:require_file] = require_file
28
+ end
29
+
30
+ opts.on(
31
+ "--[no-]color",
32
+ "Force colored output"
33
+ ) do |colored|
34
+ options[:colored] = colored
35
+ end
36
+
37
+ opts.on("-h", "--help", "Prints this help") do
38
+ ui.exit_with opts.to_s
39
+ end
40
+ end
41
+
42
+ command do
43
+ set_defaults!
44
+ ui.colored = options[:colored]
45
+
46
+ unless options[:config_file]
47
+ ui.fail_with("=> ERROR: you need to specify the config file")
48
+ end
49
+
50
+ ui.stdout_print("=> Config file:", options[:config_file].inspect)
51
+ config_file = File.expand_path(options[:config_file])
52
+
53
+ if options[:require_file]
54
+ ui.stdout_print("=> Require file:", options[:require_file].inspect)
55
+ require_file = File.expand_path(options[:require_file])
56
+ end
57
+
58
+ unless File.file?(config_file)
59
+ ui.fail_with(
60
+ "=> ERROR: config file doesn't exist at",
61
+ config_file.inspect
62
+ )
63
+ end
64
+
65
+ if require_file && !File.file?(require_file)
66
+ ui.fail_with(
67
+ "=> ERROR: require file doesn't exist at",
68
+ require_file.inspect
69
+ )
70
+ end
71
+
72
+ config = Glob::SymbolizeKeys.call(YAML.load_file(config_file))
73
+ Schema.validate!(config)
74
+
75
+ load_require_file!(require_file) if require_file
76
+
77
+ default_locale = I18n.default_locale
78
+ available_locales = I18n.available_locales
79
+ ignored_keys = config.dig(:check, :ignore) || []
80
+
81
+ mapping = available_locales.each_with_object({}) do |locale, buffer|
82
+ buffer[locale] =
83
+ Glob::Map.call(Glob.filter(I18nJS.translations, ["#{locale}.*"]))
84
+ .map {|key| key.gsub(/^.*?\./, "") }
85
+ end
86
+
87
+ default_locale_keys = mapping.delete(default_locale)
88
+
89
+ if ignored_keys.any?
90
+ ui.stdout_print "=> Check #{options[:config_file].inspect} for " \
91
+ "ignored keys."
92
+ end
93
+
94
+ ui.stdout_print "=> #{default_locale}: #{default_locale_keys.size} " \
95
+ "translations"
96
+
97
+ total_missing_count = 0
98
+
99
+ mapping.each do |locale, partial_keys|
100
+ ignored_count = 0
101
+
102
+ # Compute list of filtered keys (i.e. keys not ignored)
103
+ filtered_keys = partial_keys.reject do |key|
104
+ key = "#{locale}.#{key}"
105
+
106
+ ignored = ignored_keys.include?(key)
107
+ ignored_count += 1 if ignored
108
+ ignored
109
+ end
110
+
111
+ extraneous = (partial_keys - default_locale_keys).reject do |key|
112
+ key = "#{locale}.#{key}"
113
+ ignored = ignored_keys.include?(key)
114
+ ignored_count += 1 if ignored
115
+ ignored
116
+ end
117
+
118
+ missing = (default_locale_keys - (filtered_keys - extraneous))
119
+ .reject {|key| ignored_keys.include?("#{locale}.#{key}") }
120
+
121
+ ignored_count += extraneous.size
122
+ total_missing_count += missing.size
123
+
124
+ ui.stdout_print "=> #{locale}: #{missing.size} missing, " \
125
+ "#{extraneous.size} extraneous, " \
126
+ "#{ignored_count} ignored"
127
+
128
+ all_keys = (default_locale_keys + extraneous + missing).uniq.sort
129
+
130
+ all_keys.each do |key|
131
+ next if ignored_keys.include?("#{locale}.#{key}")
132
+
133
+ label = if extraneous.include?(key)
134
+ ui.yellow("extraneous")
135
+ elsif missing.include?(key)
136
+ ui.red("missing")
137
+ else
138
+ next
139
+ end
140
+
141
+ ui.stdout_print(" - #{locale}.#{key} (#{label})")
142
+ end
143
+ end
144
+
145
+ exit(1) if total_missing_count.nonzero?
146
+ end
147
+
148
+ private def set_defaults!
149
+ config_file = "./config/i18n.yml"
150
+ require_file = "./config/environment.rb"
151
+
152
+ options[:config_file] ||= config_file if File.file?(config_file)
153
+ options[:require_file] ||= require_file if File.file?(require_file)
154
+ end
155
+ end
156
+ end
157
+ end
@@ -38,6 +38,32 @@ module I18nJS
38
38
  def options
39
39
  @options ||= {}
40
40
  end
41
+
42
+ private def load_require_file!(require_file)
43
+ require_without_warnings(require_file)
44
+ rescue Exception => error # rubocop:disable Lint/RescueException
45
+ ui.stderr_print("=> ERROR: couldn't load",
46
+ options[:require_file].inspect)
47
+ ui.fail_with(
48
+ "\n#{error_description(error)}\n#{error.backtrace.join("\n")}"
49
+ )
50
+ end
51
+
52
+ private def error_description(error)
53
+ [
54
+ error.class.name,
55
+ error.message
56
+ ].reject(&:empty?).join(" => ")
57
+ end
58
+
59
+ private def require_without_warnings(path)
60
+ old_verbose = $VERBOSE
61
+ $VERBOSE = nil
62
+
63
+ load path
64
+ ensure
65
+ $VERBOSE = old_verbose
66
+ end
41
67
  end
42
68
  end
43
69
  end
@@ -14,7 +14,7 @@ module I18nJS
14
14
  opts.on(
15
15
  "-cCONFIG_FILE",
16
16
  "--config=CONFIG_FILE",
17
- "The configuration file that will be generated"
17
+ "The configuration file that will be used"
18
18
  ) do |config_file|
19
19
  options[:config_file] = config_file
20
20
  end
@@ -35,16 +35,15 @@ module I18nJS
35
35
  command do
36
36
  set_defaults!
37
37
 
38
- ui.stdout_print("=> config file:", options[:config_file].inspect)
39
- ui.stdout_print("=> require file:", options[:require_file].inspect)
40
-
41
38
  unless options[:config_file]
42
39
  ui.fail_with("=> ERROR: you need to specify the config file")
43
40
  end
44
41
 
42
+ ui.stdout_print("=> Config file:", options[:config_file].inspect)
45
43
  config_file = File.expand_path(options[:config_file])
46
44
 
47
45
  if options[:require_file]
46
+ ui.stdout_print("=> Require file:", options[:require_file].inspect)
48
47
  require_file = File.expand_path(options[:require_file])
49
48
  end
50
49
 
@@ -67,7 +66,7 @@ module I18nJS
67
66
  I18nJS.call(config_file: config_file)
68
67
  end
69
68
 
70
- ui.stdout_print("=> done in #{time.round(2)}s")
69
+ ui.stdout_print("=> Done in #{time.round(2)}s")
71
70
  end
72
71
 
73
72
  private def set_defaults!
@@ -77,32 +76,6 @@ module I18nJS
77
76
  options[:config_file] ||= config_file if File.file?(config_file)
78
77
  options[:require_file] ||= require_file if File.file?(require_file)
79
78
  end
80
-
81
- private def load_require_file!(require_file)
82
- require_without_warnings(require_file)
83
- rescue Exception => error # rubocop:disable Lint/RescueException
84
- ui.stderr_print("=> ERROR: couldn't load",
85
- options[:require_file].inspect)
86
- ui.fail_with(
87
- "\n#{error_description(error)}\n#{error.backtrace.join("\n")}"
88
- )
89
- end
90
-
91
- private def error_description(error)
92
- [
93
- error.class.name,
94
- error.message
95
- ].reject(&:empty?).join(" => ")
96
- end
97
-
98
- private def require_without_warnings(path)
99
- old_verbose = $VERBOSE
100
- $VERBOSE = nil
101
-
102
- require path
103
- ensure
104
- $VERBOSE = old_verbose
105
- end
106
79
  end
107
80
  end
108
81
  end
@@ -3,17 +3,21 @@
3
3
  module I18nJS
4
4
  class CLI
5
5
  class UI
6
- def initialize(stdout:, stderr:)
6
+ attr_reader :stdout, :stderr
7
+ attr_accessor :colored
8
+
9
+ def initialize(stdout:, stderr:, colored: nil)
7
10
  @stdout = stdout
8
11
  @stderr = stderr
12
+ @colored = colored
9
13
  end
10
14
 
11
15
  def stdout_print(*message)
12
- @stdout << "#{message.join(' ')}\n"
16
+ stdout << "#{message.join(' ')}\n"
13
17
  end
14
18
 
15
19
  def stderr_print(*message)
16
- @stderr << "#{message.join(' ')}\n"
20
+ stderr << "#{message.join(' ')}\n"
17
21
  end
18
22
 
19
23
  def fail_with(*message)
@@ -25,6 +29,36 @@ module I18nJS
25
29
  stdout_print(message)
26
30
  exit(0)
27
31
  end
32
+
33
+ def yellow(text)
34
+ ansi(text, 33)
35
+ end
36
+
37
+ def red(text)
38
+ ansi(text, 31)
39
+ end
40
+
41
+ def colored?
42
+ colored_output = if colored.nil?
43
+ stdout.tty?
44
+ else
45
+ colored
46
+ end
47
+
48
+ colored_output && !no_color?
49
+ end
50
+
51
+ def ansi(text, code)
52
+ if colored?
53
+ "\e[#{code}m#{text}\e[0m"
54
+ else
55
+ text
56
+ end
57
+ end
58
+
59
+ def no_color?
60
+ !ENV["NO_COLOR"].nil? && ENV["NO_COLOR"] == "1"
61
+ end
28
62
  end
29
63
  end
30
64
  end
data/lib/i18n-js/cli.rb CHANGED
@@ -6,14 +6,15 @@ require_relative "cli/ui"
6
6
  require_relative "cli/init_command"
7
7
  require_relative "cli/version_command"
8
8
  require_relative "cli/export_command"
9
+ require_relative "cli/check_command"
9
10
 
10
11
  module I18nJS
11
12
  class CLI
12
13
  attr_reader :ui
13
14
 
14
- def initialize(argv:, stdout:, stderr:)
15
+ def initialize(argv:, stdout:, stderr:, colored: stdout.tty?)
15
16
  @argv = argv.dup
16
- @ui = UI.new(stdout: stdout, stderr: stderr)
17
+ @ui = UI.new(stdout: stdout, stderr: stderr, colored: colored)
17
18
  end
18
19
 
19
20
  def call
@@ -26,7 +27,7 @@ module I18nJS
26
27
  end
27
28
 
28
29
  private def command_classes
29
- [InitCommand, ExportCommand, VersionCommand]
30
+ [InitCommand, ExportCommand, VersionCommand, CheckCommand]
30
31
  end
31
32
 
32
33
  private def commands
@@ -4,8 +4,9 @@ module I18nJS
4
4
  class Schema
5
5
  InvalidError = Class.new(StandardError)
6
6
 
7
- ROOT_KEYS = %i[translations].freeze
7
+ ROOT_KEYS = %i[translations check].freeze
8
8
  REQUIRED_ROOT_KEYS = %i[translations].freeze
9
+ REQUIRED_CHECK_KEYS = %i[ignore].freeze
9
10
  REQUIRED_TRANSLATION_KEYS = %i[file patterns].freeze
10
11
  TRANSLATION_KEYS = %i[file patterns].freeze
11
12
 
@@ -25,6 +26,17 @@ module I18nJS
25
26
  expect_required_keys(REQUIRED_ROOT_KEYS, target)
26
27
  reject_extraneous_keys(ROOT_KEYS, target)
27
28
  validate_translations
29
+ validate_check
30
+ end
31
+
32
+ def validate_check
33
+ return unless target.key?(:check)
34
+
35
+ check = target[:check]
36
+
37
+ expect_type(:check, check, Hash, target)
38
+ expect_required_keys(REQUIRED_CHECK_KEYS, check)
39
+ expect_type(:ignore, check[:ignore], Array, check)
28
40
  end
29
41
 
30
42
  def validate_translations
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nJS
4
- VERSION = "4.0.0.alpha3"
4
+ VERSION = "4.0.0"
5
5
  end
data/lib/i18n-js.rb CHANGED
@@ -21,24 +21,31 @@ module I18nJS
21
21
 
22
22
  config = Glob::SymbolizeKeys.call(config || YAML.load_file(config_file))
23
23
  Schema.validate!(config)
24
+ exported_files = []
24
25
 
25
26
  config[:translations].each do |group|
26
- export_group(group)
27
+ exported_files += export_group(group)
27
28
  end
29
+
30
+ exported_files
28
31
  end
29
32
 
30
33
  def self.export_group(group)
31
34
  filtered_translations = Glob.filter(translations, group[:patterns])
32
35
  output_file_path = File.expand_path(group[:file])
36
+ exported_files = []
33
37
 
34
38
  if output_file_path.include?(":locale")
35
39
  filtered_translations.each_key do |locale|
36
40
  locale_file_path = output_file_path.gsub(/:locale/, locale.to_s)
37
- write_file(locale_file_path, locale => filtered_translations[locale])
41
+ exported_files << write_file(locale_file_path,
42
+ locale => filtered_translations[locale])
38
43
  end
39
44
  else
40
- write_file(output_file_path, filtered_translations)
45
+ exported_files << write_file(output_file_path, filtered_translations)
41
46
  end
47
+
48
+ exported_files
42
49
  end
43
50
 
44
51
  def self.write_file(file_path, translations)
@@ -51,6 +58,8 @@ module I18nJS
51
58
  File.open(file_path, "w") do |file|
52
59
  file << contents
53
60
  end
61
+
62
+ file_path
54
63
  end
55
64
 
56
65
  def self.translations
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.0.0.alpha3
4
+ version: 4.0.0
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-07-20 00:00:00.000000000 Z
11
+ date: 2022-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glob
@@ -178,11 +178,13 @@ files:
178
178
  - exe/i18n
179
179
  - i18n-js.gemspec
180
180
  - i18njs.png
181
+ - images/i18njs-check.gif
181
182
  - lib/guard/i18n-js.rb
182
183
  - lib/guard/i18n-js/templates/Guardfile
183
184
  - lib/guard/i18n-js/version.rb
184
185
  - lib/i18n-js.rb
185
186
  - lib/i18n-js/cli.rb
187
+ - lib/i18n-js/cli/check_command.rb
186
188
  - lib/i18n-js/cli/command.rb
187
189
  - lib/i18n-js/cli/export_command.rb
188
190
  - lib/i18n-js/cli/init_command.rb
@@ -198,10 +200,10 @@ metadata:
198
200
  rubygems_mfa_required: 'true'
199
201
  homepage_uri: https://github.com/fnando/i18n-js
200
202
  bug_tracker_uri: https://github.com/fnando/i18n-js/issues
201
- source_code_uri: https://github.com/fnando/i18n-js/tree/v4.0.0.alpha3
202
- changelog_uri: https://github.com/fnando/i18n-js/tree/v4.0.0.alpha3/CHANGELOG.md
203
- documentation_uri: https://github.com/fnando/i18n-js/tree/v4.0.0.alpha3/README.md
204
- license_uri: https://github.com/fnando/i18n-js/tree/v4.0.0.alpha3/LICENSE.md
203
+ source_code_uri: https://github.com/fnando/i18n-js/tree/v4.0.0
204
+ changelog_uri: https://github.com/fnando/i18n-js/tree/v4.0.0/CHANGELOG.md
205
+ documentation_uri: https://github.com/fnando/i18n-js/tree/v4.0.0/README.md
206
+ license_uri: https://github.com/fnando/i18n-js/tree/v4.0.0/LICENSE.md
205
207
  post_install_message:
206
208
  rdoc_options: []
207
209
  require_paths:
@@ -213,11 +215,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
213
215
  version: 2.6.0
214
216
  required_rubygems_version: !ruby/object:Gem::Requirement
215
217
  requirements:
216
- - - ">"
218
+ - - ">="
217
219
  - !ruby/object:Gem::Version
218
- version: 1.3.1
220
+ version: '0'
219
221
  requirements: []
220
- rubygems_version: 3.3.17
222
+ rubygems_version: 3.3.7
221
223
  signing_key:
222
224
  specification_version: 4
223
225
  summary: Export i18n translations and use them on JavaScript.