i18n-js 4.0.0 → 4.1.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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby-tests.yml +22 -1
- data/.gitignore +2 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +20 -1
- data/MIGRATING_FROM_V3_TO_V4.md +191 -0
- data/README.md +267 -25
- data/bin/pack +79 -0
- data/i18n-js.gemspec +5 -2
- data/lib/guard/i18n-js.rb +21 -4
- data/lib/i18n-js/cli/check_command.rb +7 -147
- data/lib/i18n-js/cli/command.rb +10 -0
- data/lib/i18n-js/cli/export_command.rb +17 -3
- data/lib/i18n-js/cli/lint_scripts_command.rb +157 -0
- data/lib/i18n-js/cli/lint_translations_command.rb +155 -0
- data/lib/i18n-js/cli/plugins_command.rb +67 -0
- data/lib/i18n-js/cli.rb +12 -1
- data/lib/i18n-js/embed_fallback_translations_plugin.rb +78 -0
- data/lib/i18n-js/lint.js +150645 -0
- data/lib/i18n-js/lint.ts +196 -0
- data/lib/i18n-js/listen.rb +25 -10
- data/lib/i18n-js/plugin.rb +38 -0
- data/lib/i18n-js/schema.rb +53 -14
- data/lib/i18n-js/version.rb +1 -1
- data/lib/i18n-js.rb +20 -3
- data/package.json +10 -0
- metadata +33 -11
- data/i18njs.png +0 -0
- data/images/i18njs-check.gif +0 -0
data/bin/pack
ADDED
@@ -0,0 +1,79 @@
|
|
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
|
+
release = 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("--release") do
|
43
|
+
release = 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
|
+
if release
|
73
|
+
system "git", "add", changelog_path, version_path
|
74
|
+
system "git", "commit", "-m", "Bump up version (v#{version})"
|
75
|
+
system "git", "tag", "v#{version}"
|
76
|
+
end
|
77
|
+
|
78
|
+
system "rake", "build"
|
79
|
+
system "git", "checkout", changelog_path, version_path unless release
|
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
|
-
|
57
|
-
|
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 "[
|
88
|
+
::Guard::UI.error "[i18n-js] #{message}"
|
72
89
|
end
|
73
90
|
|
74
91
|
def info(message)
|
75
|
-
::Guard::UI.info "[
|
92
|
+
::Guard::UI.info "[i18n-js] #{message}"
|
76
93
|
end
|
77
94
|
end
|
78
95
|
end
|
@@ -1,156 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "benchmark"
|
4
|
-
|
5
3
|
module I18nJS
|
6
4
|
class CLI
|
7
|
-
class CheckCommand <
|
5
|
+
class CheckCommand < LintTranslationsCommand
|
8
6
|
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"
|
7
|
+
description "Check for missing translations based on the default " \
|
8
|
+
"locale (DEPRECATED: Use `i18n lint:translations` instead)"
|
151
9
|
|
152
|
-
|
153
|
-
|
10
|
+
def command
|
11
|
+
ui.stderr_print "=> WARNING: `i18n check` has been deprecated in " \
|
12
|
+
"favor of `i18n lint:translations`"
|
13
|
+
super
|
154
14
|
end
|
155
15
|
end
|
156
16
|
end
|
data/lib/i18n-js/cli/command.rb
CHANGED
@@ -39,6 +39,16 @@ module I18nJS
|
|
39
39
|
@options ||= {}
|
40
40
|
end
|
41
41
|
|
42
|
+
private def load_config_file(config_file)
|
43
|
+
config = Glob::SymbolizeKeys.call(YAML.load_file(config_file))
|
44
|
+
|
45
|
+
if config.key?(:check)
|
46
|
+
config[:lint_translations] ||= config.delete(:check)
|
47
|
+
end
|
48
|
+
|
49
|
+
config
|
50
|
+
end
|
51
|
+
|
42
52
|
private def load_require_file!(require_file)
|
43
53
|
require_without_warnings(require_file)
|
44
54
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
@@ -27,6 +27,14 @@ module I18nJS
|
|
27
27
|
options[:require_file] = require_file
|
28
28
|
end
|
29
29
|
|
30
|
+
opts.on(
|
31
|
+
"-q",
|
32
|
+
"--quiet",
|
33
|
+
"A Ruby file that must be loaded"
|
34
|
+
) do |quiet|
|
35
|
+
options[:quiet] = quiet
|
36
|
+
end
|
37
|
+
|
30
38
|
opts.on("-h", "--help", "Prints this help") do
|
31
39
|
ui.exit_with opts.to_s
|
32
40
|
end
|
@@ -39,11 +47,11 @@ module I18nJS
|
|
39
47
|
ui.fail_with("=> ERROR: you need to specify the config file")
|
40
48
|
end
|
41
49
|
|
42
|
-
|
50
|
+
log("=> Config file:", options[:config_file].inspect)
|
43
51
|
config_file = File.expand_path(options[:config_file])
|
44
52
|
|
45
53
|
if options[:require_file]
|
46
|
-
|
54
|
+
log("=> Require file:", options[:require_file].inspect)
|
47
55
|
require_file = File.expand_path(options[:require_file])
|
48
56
|
end
|
49
57
|
|
@@ -66,7 +74,13 @@ module I18nJS
|
|
66
74
|
I18nJS.call(config_file: config_file)
|
67
75
|
end
|
68
76
|
|
69
|
-
|
77
|
+
log("=> Done in #{time.round(2)}s")
|
78
|
+
end
|
79
|
+
|
80
|
+
private def log(*args)
|
81
|
+
return if options[:quiet]
|
82
|
+
|
83
|
+
ui.stdout_print(*args)
|
70
84
|
end
|
71
85
|
|
72
86
|
private def set_defaults!
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
class CLI
|
5
|
+
class LintScriptsCommand < Command
|
6
|
+
command_name "lint:scripts"
|
7
|
+
description "Lint files using TypeScript"
|
8
|
+
|
9
|
+
parse do |opts|
|
10
|
+
opts.banner = "Usage: i18n #{name} [options]"
|
11
|
+
|
12
|
+
opts.on(
|
13
|
+
"-cCONFIG_FILE",
|
14
|
+
"--config=CONFIG_FILE",
|
15
|
+
"The configuration file that will be used"
|
16
|
+
) do |config_file|
|
17
|
+
options[:config_file] = config_file
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on(
|
21
|
+
"-rREQUIRE_FILE",
|
22
|
+
"--require=REQUIRE_FILE",
|
23
|
+
"A Ruby file that must be loaded"
|
24
|
+
) do |require_file|
|
25
|
+
options[:require_file] = require_file
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on(
|
29
|
+
"-nNODE_PATH",
|
30
|
+
"--node-path=NODE_PATH",
|
31
|
+
"Set node.js path"
|
32
|
+
) do |node_path|
|
33
|
+
options[:node_path] = node_path
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("-h", "--help", "Prints this help") do
|
37
|
+
ui.exit_with opts.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
command do
|
42
|
+
set_defaults!
|
43
|
+
ui.colored = options[:colored]
|
44
|
+
|
45
|
+
unless options[:config_file]
|
46
|
+
ui.fail_with("=> ERROR: you need to specify the config file")
|
47
|
+
end
|
48
|
+
|
49
|
+
ui.stdout_print("=> Config file:", options[:config_file].inspect)
|
50
|
+
config_file = File.expand_path(options[:config_file])
|
51
|
+
|
52
|
+
if options[:require_file]
|
53
|
+
ui.stdout_print("=> Require file:", options[:require_file].inspect)
|
54
|
+
require_file = File.expand_path(options[:require_file])
|
55
|
+
end
|
56
|
+
|
57
|
+
node_path = options[:node_path] || find_node
|
58
|
+
ui.stdout_print("=> Node:", node_path.inspect)
|
59
|
+
|
60
|
+
unless File.file?(config_file)
|
61
|
+
ui.fail_with(
|
62
|
+
"=> ERROR: config file doesn't exist at",
|
63
|
+
config_file.inspect
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
if require_file && !File.file?(require_file)
|
68
|
+
ui.fail_with(
|
69
|
+
"=> ERROR: require file doesn't exist at",
|
70
|
+
require_file.inspect
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
found_node = node_path && File.executable?(File.expand_path(node_path))
|
75
|
+
|
76
|
+
unless found_node
|
77
|
+
ui.fail_with(
|
78
|
+
"=> ERROR: node.js couldn't be found (path: #{node_path})"
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
config = load_config_file(config_file)
|
83
|
+
Schema.validate!(config)
|
84
|
+
|
85
|
+
load_require_file!(require_file) if require_file
|
86
|
+
|
87
|
+
available_locales = I18n.available_locales
|
88
|
+
ignored_keys = config.dig(:lint_scripts, :ignore) || []
|
89
|
+
|
90
|
+
ui.stdout_print "=> Available locales: #{available_locales.inspect}"
|
91
|
+
|
92
|
+
exported_files = I18nJS.call(config_file: config_file)
|
93
|
+
data = exported_files.each_with_object({}) do |file, buffer|
|
94
|
+
buffer.merge!(JSON.load_file(file, symbolize_names: true))
|
95
|
+
end
|
96
|
+
|
97
|
+
lint_file = File.expand_path(File.join(__dir__, "../lint.js"))
|
98
|
+
patterns = config.dig(:lint_scripts, :patterns) || %w[
|
99
|
+
!(node_modules)/**/*.js
|
100
|
+
!(node_modules)/**/*.ts
|
101
|
+
!(node_modules)/**/*.jsx
|
102
|
+
!(node_modules)/**/*.tsx
|
103
|
+
]
|
104
|
+
|
105
|
+
ui.stdout_print "=> Patterns: #{patterns.inspect}"
|
106
|
+
|
107
|
+
out = IO.popen([node_path, lint_file, patterns.join(":")]).read
|
108
|
+
scopes = JSON.parse(out, symbolize_names: true)
|
109
|
+
map = Glob::Map.call(data)
|
110
|
+
missing_count = 0
|
111
|
+
ignored_count = 0
|
112
|
+
|
113
|
+
messages = []
|
114
|
+
|
115
|
+
available_locales.each do |locale|
|
116
|
+
scopes.each do |scope|
|
117
|
+
scope_with_locale = "#{locale}.#{scope[:full]}"
|
118
|
+
|
119
|
+
ignored = ignored_keys.include?(scope[:full]) ||
|
120
|
+
ignored_keys.include?(scope_with_locale)
|
121
|
+
|
122
|
+
if ignored
|
123
|
+
ignored_count += 1
|
124
|
+
next
|
125
|
+
end
|
126
|
+
|
127
|
+
next if map.include?(scope_with_locale)
|
128
|
+
|
129
|
+
missing_count += 1
|
130
|
+
messages << " - #{scope[:location]}: #{scope_with_locale}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
ui.stdout_print "=> #{map.size} translations, #{missing_count} " \
|
135
|
+
"missing, #{ignored_count} ignored"
|
136
|
+
ui.stdout_print messages.sort.join("\n")
|
137
|
+
|
138
|
+
exit(missing_count.size)
|
139
|
+
end
|
140
|
+
|
141
|
+
private def set_defaults!
|
142
|
+
config_file = "./config/i18n.yml"
|
143
|
+
require_file = "./config/environment.rb"
|
144
|
+
|
145
|
+
options[:config_file] ||= config_file if File.file?(config_file)
|
146
|
+
options[:require_file] ||= require_file if File.file?(require_file)
|
147
|
+
end
|
148
|
+
|
149
|
+
private def find_node
|
150
|
+
ENV["PATH"]
|
151
|
+
.split(File::PATH_SEPARATOR)
|
152
|
+
.map {|dir| File.join(dir, "node") }
|
153
|
+
.find {|bin| File.executable?(bin) }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
class CLI
|
5
|
+
class LintTranslationsCommand < Command
|
6
|
+
command_name "lint:translations"
|
7
|
+
description "Check for missing translations based on the default locale"
|
8
|
+
|
9
|
+
parse do |opts|
|
10
|
+
opts.banner = "Usage: i18n #{name} [options]"
|
11
|
+
|
12
|
+
opts.on(
|
13
|
+
"-cCONFIG_FILE",
|
14
|
+
"--config=CONFIG_FILE",
|
15
|
+
"The configuration file that will be used"
|
16
|
+
) do |config_file|
|
17
|
+
options[:config_file] = config_file
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on(
|
21
|
+
"-rREQUIRE_FILE",
|
22
|
+
"--require=REQUIRE_FILE",
|
23
|
+
"A Ruby file that must be loaded"
|
24
|
+
) do |require_file|
|
25
|
+
options[:require_file] = require_file
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on(
|
29
|
+
"--[no-]color",
|
30
|
+
"Force colored output"
|
31
|
+
) do |colored|
|
32
|
+
options[:colored] = colored
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-h", "--help", "Prints this help") do
|
36
|
+
ui.exit_with opts.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
command do
|
41
|
+
set_defaults!
|
42
|
+
ui.colored = options[:colored]
|
43
|
+
|
44
|
+
unless options[:config_file]
|
45
|
+
ui.fail_with("=> ERROR: you need to specify the config file")
|
46
|
+
end
|
47
|
+
|
48
|
+
ui.stdout_print("=> Config file:", options[:config_file].inspect)
|
49
|
+
config_file = File.expand_path(options[:config_file])
|
50
|
+
|
51
|
+
if options[:require_file]
|
52
|
+
ui.stdout_print("=> Require file:", options[:require_file].inspect)
|
53
|
+
require_file = File.expand_path(options[:require_file])
|
54
|
+
end
|
55
|
+
|
56
|
+
unless File.file?(config_file)
|
57
|
+
ui.fail_with(
|
58
|
+
"=> ERROR: config file doesn't exist at",
|
59
|
+
config_file.inspect
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
if require_file && !File.file?(require_file)
|
64
|
+
ui.fail_with(
|
65
|
+
"=> ERROR: require file doesn't exist at",
|
66
|
+
require_file.inspect
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
config = load_config_file(config_file)
|
71
|
+
Schema.validate!(config)
|
72
|
+
|
73
|
+
load_require_file!(require_file) if require_file
|
74
|
+
|
75
|
+
default_locale = I18n.default_locale
|
76
|
+
available_locales = I18n.available_locales
|
77
|
+
ignored_keys = config.dig(:lint_translations, :ignore) || []
|
78
|
+
|
79
|
+
mapping = available_locales.each_with_object({}) do |locale, buffer|
|
80
|
+
buffer[locale] =
|
81
|
+
Glob::Map.call(Glob.filter(I18nJS.translations, ["#{locale}.*"]))
|
82
|
+
.map {|key| key.gsub(/^.*?\./, "") }
|
83
|
+
end
|
84
|
+
|
85
|
+
default_locale_keys = mapping.delete(default_locale)
|
86
|
+
|
87
|
+
if ignored_keys.any?
|
88
|
+
ui.stdout_print "=> Check #{options[:config_file].inspect} for " \
|
89
|
+
"ignored keys."
|
90
|
+
end
|
91
|
+
|
92
|
+
ui.stdout_print "=> #{default_locale}: #{default_locale_keys.size} " \
|
93
|
+
"translations"
|
94
|
+
|
95
|
+
total_missing_count = 0
|
96
|
+
|
97
|
+
mapping.each do |locale, partial_keys|
|
98
|
+
ignored_count = 0
|
99
|
+
|
100
|
+
# Compute list of filtered keys (i.e. keys not ignored)
|
101
|
+
filtered_keys = partial_keys.reject do |key|
|
102
|
+
key = "#{locale}.#{key}"
|
103
|
+
|
104
|
+
ignored = ignored_keys.include?(key)
|
105
|
+
ignored_count += 1 if ignored
|
106
|
+
ignored
|
107
|
+
end
|
108
|
+
|
109
|
+
extraneous = (partial_keys - default_locale_keys).reject do |key|
|
110
|
+
key = "#{locale}.#{key}"
|
111
|
+
ignored = ignored_keys.include?(key)
|
112
|
+
ignored_count += 1 if ignored
|
113
|
+
ignored
|
114
|
+
end
|
115
|
+
|
116
|
+
missing = (default_locale_keys - (filtered_keys - extraneous))
|
117
|
+
.reject {|key| ignored_keys.include?("#{locale}.#{key}") }
|
118
|
+
|
119
|
+
ignored_count += extraneous.size
|
120
|
+
total_missing_count += missing.size
|
121
|
+
|
122
|
+
ui.stdout_print "=> #{locale}: #{missing.size} missing, " \
|
123
|
+
"#{extraneous.size} extraneous, " \
|
124
|
+
"#{ignored_count} ignored"
|
125
|
+
|
126
|
+
all_keys = (default_locale_keys + extraneous + missing).uniq.sort
|
127
|
+
|
128
|
+
all_keys.each do |key|
|
129
|
+
next if ignored_keys.include?("#{locale}.#{key}")
|
130
|
+
|
131
|
+
label = if extraneous.include?(key)
|
132
|
+
ui.yellow("extraneous")
|
133
|
+
elsif missing.include?(key)
|
134
|
+
ui.red("missing")
|
135
|
+
else
|
136
|
+
next
|
137
|
+
end
|
138
|
+
|
139
|
+
ui.stdout_print(" - #{locale}.#{key} (#{label})")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
exit(1) if total_missing_count.nonzero?
|
144
|
+
end
|
145
|
+
|
146
|
+
private def set_defaults!
|
147
|
+
config_file = "./config/i18n.yml"
|
148
|
+
require_file = "./config/environment.rb"
|
149
|
+
|
150
|
+
options[:config_file] ||= config_file if File.file?(config_file)
|
151
|
+
options[:require_file] ||= require_file if File.file?(require_file)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|