i18n-js 4.0.1 → 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 +14 -2
- data/MIGRATING_FROM_V3_TO_V4.md +1 -1
- data/README.md +219 -9
- data/bin/pack +79 -0
- data/i18n-js.gemspec +4 -1
- data/lib/i18n-js/cli/check_command.rb +7 -147
- data/lib/i18n-js/cli/command.rb +10 -0
- 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 +3 -2
- 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 +32 -9
@@ -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
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
class CLI
|
5
|
+
class PluginsCommand < Command
|
6
|
+
command_name "plugins"
|
7
|
+
description "List plugins that will be activated"
|
8
|
+
|
9
|
+
parse do |opts|
|
10
|
+
opts.banner = "Usage: i18n #{name} [options]"
|
11
|
+
|
12
|
+
opts.on(
|
13
|
+
"-rREQUIRE_FILE",
|
14
|
+
"--require=REQUIRE_FILE",
|
15
|
+
"A Ruby file that must be loaded"
|
16
|
+
) do |require_file|
|
17
|
+
options[:require_file] = require_file
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on("-h", "--help", "Prints this help") do
|
21
|
+
ui.exit_with opts.to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
command do
|
26
|
+
set_defaults!
|
27
|
+
ui.colored = options[:colored]
|
28
|
+
|
29
|
+
if options[:require_file]
|
30
|
+
ui.stdout_print("=> Require file:", options[:require_file].inspect)
|
31
|
+
require_file = File.expand_path(options[:require_file])
|
32
|
+
end
|
33
|
+
|
34
|
+
if require_file && !File.file?(require_file)
|
35
|
+
ui.fail_with(
|
36
|
+
"=> ERROR: require file doesn't exist at",
|
37
|
+
require_file.inspect
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
load_require_file!(require_file) if require_file
|
42
|
+
|
43
|
+
files = I18nJS.plugin_files
|
44
|
+
|
45
|
+
if files.empty?
|
46
|
+
ui.stdout_print("=> No plugins have been detected.")
|
47
|
+
else
|
48
|
+
ui.stdout_print("=> Plugins that will be activated:")
|
49
|
+
|
50
|
+
files.each do |file|
|
51
|
+
file = file.gsub("#{Dir.home}/", "~/")
|
52
|
+
|
53
|
+
ui.stdout_print(" * #{file}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private def set_defaults!
|
59
|
+
config_file = "./config/i18n.yml"
|
60
|
+
require_file = "./config/environment.rb"
|
61
|
+
|
62
|
+
options[:config_file] ||= config_file if File.file?(config_file)
|
63
|
+
options[:require_file] ||= require_file if File.file?(require_file)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/i18n-js/cli.rb
CHANGED
@@ -6,6 +6,9 @@ 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/plugins_command"
|
10
|
+
require_relative "cli/lint_translations_command"
|
11
|
+
require_relative "cli/lint_scripts_command"
|
9
12
|
require_relative "cli/check_command"
|
10
13
|
|
11
14
|
module I18nJS
|
@@ -27,7 +30,15 @@ module I18nJS
|
|
27
30
|
end
|
28
31
|
|
29
32
|
private def command_classes
|
30
|
-
[
|
33
|
+
[
|
34
|
+
InitCommand,
|
35
|
+
ExportCommand,
|
36
|
+
VersionCommand,
|
37
|
+
PluginsCommand,
|
38
|
+
LintTranslationsCommand,
|
39
|
+
LintScriptsCommand,
|
40
|
+
CheckCommand
|
41
|
+
]
|
31
42
|
end
|
32
43
|
|
33
44
|
private def commands
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
require "i18n-js/plugin"
|
5
|
+
|
6
|
+
class EmbedFallbackTranslationsPlugin < I18nJS::Plugin
|
7
|
+
CONFIG_KEY = :embed_fallback_translations
|
8
|
+
|
9
|
+
# This method must set up the basic plugin configuration, like adding the
|
10
|
+
# config's root key in case your plugin accepts configuration (defined via
|
11
|
+
# the config file).
|
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
|
17
|
+
end
|
18
|
+
|
19
|
+
# In case your plugin accepts configuration, this is where you must validate
|
20
|
+
# the configuration, making sure only valid keys and type is provided.
|
21
|
+
# If the configuration contains invalid data, then you must raise an
|
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)
|
26
|
+
|
27
|
+
plugin_config = config[CONFIG_KEY]
|
28
|
+
valid_keys = %i[enabled]
|
29
|
+
schema = I18nJS::Schema.new(config)
|
30
|
+
|
31
|
+
schema.expect_required_keys(valid_keys, plugin_config)
|
32
|
+
schema.reject_extraneous_keys(valid_keys, plugin_config)
|
33
|
+
schema.expect_enabled_config(CONFIG_KEY, plugin_config[:enabled])
|
34
|
+
end
|
35
|
+
|
36
|
+
# This method is responsible for transforming the translations. The
|
37
|
+
# translations you'll receive may be already be filtered by other plugins
|
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)
|
46
|
+
|
47
|
+
translations_glob = Glob.new(translations)
|
48
|
+
translations_glob << "*"
|
49
|
+
|
50
|
+
mapping = translations.keys.each_with_object({}) do |locale, buffer|
|
51
|
+
buffer[locale] = Glob.new(translations[locale]).tap do |glob|
|
52
|
+
glob << "*"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
default_locale = I18n.default_locale
|
57
|
+
default_locale_glob = mapping.delete(default_locale)
|
58
|
+
default_locale_paths = default_locale_glob.paths
|
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
|
71
|
+
end
|
72
|
+
|
73
|
+
translations_glob.to_h
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
I18nJS.register_plugin(EmbedFallbackTranslationsPlugin)
|
78
|
+
end
|