i18n-js 3.8.2 → 4.2.3
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/CODEOWNERS +4 -0
- data/.github/FUNDING.yml +1 -1
- data/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +38 -0
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/ruby-tests.yml +73 -0
- data/.gitignore +13 -7
- data/.rubocop.yml +19 -0
- data/CHANGELOG.md +47 -512
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +79 -0
- data/Gemfile +3 -0
- data/LICENSE.md +20 -0
- data/MIGRATING_FROM_V3_TO_V4.md +192 -0
- data/README.md +425 -921
- data/Rakefile +10 -20
- data/bin/release +81 -0
- data/exe/i18n +5 -0
- data/i18n-js.gemspec +51 -29
- data/lib/guard/i18n-js/templates/Guardfile +10 -0
- data/lib/guard/i18n-js/version.rb +13 -0
- data/lib/guard/i18n-js.rb +95 -0
- data/lib/i18n-js/clean_hash.rb +13 -0
- data/lib/i18n-js/cli/check_command.rb +17 -0
- data/lib/i18n-js/cli/command.rb +79 -0
- data/lib/i18n-js/cli/export_command.rb +95 -0
- data/lib/i18n-js/cli/init_command.rb +52 -0
- data/lib/i18n-js/cli/lint_scripts_command.rb +159 -0
- data/lib/i18n-js/cli/lint_translations_command.rb +157 -0
- data/lib/i18n-js/cli/plugins_command.rb +67 -0
- data/lib/i18n-js/cli/ui.rb +64 -0
- data/lib/i18n-js/cli/version_command.rb +18 -0
- data/lib/i18n-js/cli.rb +66 -0
- data/lib/i18n-js/embed_fallback_translations_plugin.rb +70 -0
- data/lib/i18n-js/export_files_plugin.rb +103 -0
- data/lib/i18n-js/lint.js +150645 -0
- data/lib/i18n-js/lint.ts +196 -0
- data/lib/i18n-js/listen.rb +96 -0
- data/lib/i18n-js/plugin.rb +103 -0
- data/lib/i18n-js/schema.rb +216 -0
- data/lib/i18n-js/sort_hash.rb +12 -0
- data/lib/i18n-js/version.rb +5 -0
- data/lib/i18n-js.rb +107 -1
- data/package.json +5 -20
- metadata +149 -189
- data/.editorconfig +0 -24
- data/.github/workflows/tests.yaml +0 -100
- data/.npmignore +0 -27
- data/Appraisals +0 -44
- data/app/assets/javascripts/i18n/filtered.js.erb +0 -23
- data/app/assets/javascripts/i18n/shims.js +0 -240
- data/app/assets/javascripts/i18n/translations.js +0 -3
- data/app/assets/javascripts/i18n.js +0 -1095
- data/gemfiles/i18n_0_6.gemfile +0 -7
- data/gemfiles/i18n_0_7.gemfile +0 -7
- data/gemfiles/i18n_0_8.gemfile +0 -7
- data/gemfiles/i18n_0_9.gemfile +0 -7
- data/gemfiles/i18n_1_0.gemfile +0 -7
- data/gemfiles/i18n_1_1.gemfile +0 -7
- data/gemfiles/i18n_1_2.gemfile +0 -7
- data/gemfiles/i18n_1_3.gemfile +0 -7
- data/gemfiles/i18n_1_4.gemfile +0 -7
- data/gemfiles/i18n_1_5.gemfile +0 -7
- data/gemfiles/i18n_1_6.gemfile +0 -7
- data/gemfiles/i18n_1_7.gemfile +0 -7
- data/gemfiles/i18n_1_8.gemfile +0 -7
- data/i18njs.png +0 -0
- data/lib/i18n/js/dependencies.rb +0 -63
- data/lib/i18n/js/engine.rb +0 -87
- data/lib/i18n/js/fallback_locales.rb +0 -70
- data/lib/i18n/js/formatters/base.rb +0 -25
- data/lib/i18n/js/formatters/js.rb +0 -32
- data/lib/i18n/js/formatters/json.rb +0 -13
- data/lib/i18n/js/middleware.rb +0 -82
- data/lib/i18n/js/private/config_store.rb +0 -31
- data/lib/i18n/js/private/hash_with_symbol_keys.rb +0 -36
- data/lib/i18n/js/segment.rb +0 -80
- data/lib/i18n/js/utils.rb +0 -78
- data/lib/i18n/js/version.rb +0 -7
- data/lib/i18n/js.rb +0 -264
- data/lib/rails/generators/i18n/js/config/config_generator.rb +0 -19
- data/lib/rails/generators/i18n/js/config/templates/i18n-js.yml +0 -27
- data/lib/tasks/export.rake +0 -8
- data/spec/fixtures/custom_path.yml +0 -5
- data/spec/fixtures/default.yml +0 -5
- data/spec/fixtures/erb.yml +0 -5
- data/spec/fixtures/except_condition.yml +0 -7
- data/spec/fixtures/js_export_dir_custom.yml +0 -7
- data/spec/fixtures/js_export_dir_none.yml +0 -6
- data/spec/fixtures/js_extend_parent.yml +0 -6
- data/spec/fixtures/js_extend_segment.yml +0 -6
- data/spec/fixtures/js_file_per_locale.yml +0 -7
- data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +0 -4
- data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +0 -6
- data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +0 -4
- data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +0 -4
- data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +0 -4
- data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +0 -4
- data/spec/fixtures/js_file_with_namespace_prefix_and_pretty_print.yml +0 -9
- data/spec/fixtures/js_sort_translation_keys_false.yml +0 -6
- data/spec/fixtures/js_sort_translation_keys_true.yml +0 -6
- data/spec/fixtures/json_only.yml +0 -18
- data/spec/fixtures/locales.yml +0 -133
- data/spec/fixtures/merge_plurals.yml +0 -6
- data/spec/fixtures/merge_plurals_with_no_overrides.yml +0 -4
- data/spec/fixtures/merge_plurals_with_partial_overrides.yml +0 -4
- data/spec/fixtures/millions.yml +0 -4
- data/spec/fixtures/multiple_conditions.yml +0 -7
- data/spec/fixtures/multiple_conditions_per_locale.yml +0 -7
- data/spec/fixtures/multiple_files.yml +0 -7
- data/spec/fixtures/no_config.yml +0 -2
- data/spec/fixtures/no_scope.yml +0 -4
- data/spec/fixtures/simple_scope.yml +0 -5
- data/spec/js/currency.spec.js +0 -62
- data/spec/js/current_locale.spec.js +0 -19
- data/spec/js/dates.spec.js +0 -276
- data/spec/js/defaults.spec.js +0 -31
- data/spec/js/extend.spec.js +0 -110
- data/spec/js/interpolation.spec.js +0 -124
- data/spec/js/jasmine/MIT.LICENSE +0 -20
- data/spec/js/jasmine/jasmine-html.js +0 -190
- data/spec/js/jasmine/jasmine.css +0 -166
- data/spec/js/jasmine/jasmine.js +0 -2476
- data/spec/js/jasmine/jasmine_favicon.png +0 -0
- data/spec/js/locales.spec.js +0 -31
- data/spec/js/localization.spec.js +0 -78
- data/spec/js/numbers.spec.js +0 -174
- data/spec/js/placeholder.spec.js +0 -24
- data/spec/js/pluralization.spec.js +0 -219
- data/spec/js/prepare_options.spec.js +0 -41
- data/spec/js/require.js +0 -2083
- data/spec/js/specs.html +0 -49
- data/spec/js/specs_requirejs.html +0 -72
- data/spec/js/translate.spec.js +0 -304
- data/spec/js/translations.js +0 -188
- data/spec/js/utility_functions.spec.js +0 -20
- data/spec/ruby/i18n/js/fallback_locales_spec.rb +0 -84
- data/spec/ruby/i18n/js/segment_spec.rb +0 -219
- data/spec/ruby/i18n/js/utils_spec.rb +0 -106
- data/spec/ruby/i18n/js_spec.rb +0 -748
- data/spec/spec_helper.rb +0 -80
- data/yarn.lock +0 -131
@@ -0,0 +1,159 @@
|
|
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
|
+
I18nJS.load_plugins!
|
84
|
+
I18nJS.initialize_plugins!(config: config)
|
85
|
+
Schema.validate!(config)
|
86
|
+
|
87
|
+
load_require_file!(require_file) if require_file
|
88
|
+
|
89
|
+
available_locales = I18n.available_locales
|
90
|
+
ignored_keys = config.dig(:lint_scripts, :ignore) || []
|
91
|
+
|
92
|
+
ui.stdout_print "=> Available locales: #{available_locales.inspect}"
|
93
|
+
|
94
|
+
exported_files = I18nJS.call(config_file: config_file)
|
95
|
+
data = exported_files.each_with_object({}) do |file, buffer|
|
96
|
+
buffer.merge!(JSON.load_file(file, symbolize_names: true))
|
97
|
+
end
|
98
|
+
|
99
|
+
lint_file = File.expand_path(File.join(__dir__, "../lint.js"))
|
100
|
+
patterns = config.dig(:lint_scripts, :patterns) || %w[
|
101
|
+
!(node_modules)/**/*.js
|
102
|
+
!(node_modules)/**/*.ts
|
103
|
+
!(node_modules)/**/*.jsx
|
104
|
+
!(node_modules)/**/*.tsx
|
105
|
+
]
|
106
|
+
|
107
|
+
ui.stdout_print "=> Patterns: #{patterns.inspect}"
|
108
|
+
|
109
|
+
out = IO.popen([node_path, lint_file, patterns.join(":")]).read
|
110
|
+
scopes = JSON.parse(out, symbolize_names: true)
|
111
|
+
map = Glob::Map.call(data)
|
112
|
+
missing_count = 0
|
113
|
+
ignored_count = 0
|
114
|
+
|
115
|
+
messages = []
|
116
|
+
|
117
|
+
available_locales.each do |locale|
|
118
|
+
scopes.each do |scope|
|
119
|
+
scope_with_locale = "#{locale}.#{scope[:full]}"
|
120
|
+
|
121
|
+
ignored = ignored_keys.include?(scope[:full]) ||
|
122
|
+
ignored_keys.include?(scope_with_locale)
|
123
|
+
|
124
|
+
if ignored
|
125
|
+
ignored_count += 1
|
126
|
+
next
|
127
|
+
end
|
128
|
+
|
129
|
+
next if map.include?(scope_with_locale)
|
130
|
+
|
131
|
+
missing_count += 1
|
132
|
+
messages << " - #{scope[:location]}: #{scope_with_locale}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
ui.stdout_print "=> #{map.size} translations, #{missing_count} " \
|
137
|
+
"missing, #{ignored_count} ignored"
|
138
|
+
ui.stdout_print messages.sort.join("\n")
|
139
|
+
|
140
|
+
exit(missing_count.size)
|
141
|
+
end
|
142
|
+
|
143
|
+
private def set_defaults!
|
144
|
+
config_file = "./config/i18n.yml"
|
145
|
+
require_file = "./config/environment.rb"
|
146
|
+
|
147
|
+
options[:config_file] ||= config_file if File.file?(config_file)
|
148
|
+
options[:require_file] ||= require_file if File.file?(require_file)
|
149
|
+
end
|
150
|
+
|
151
|
+
private def find_node
|
152
|
+
ENV["PATH"]
|
153
|
+
.split(File::PATH_SEPARATOR)
|
154
|
+
.map {|dir| File.join(dir, "node") }
|
155
|
+
.find {|bin| File.executable?(bin) }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,157 @@
|
|
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
|
+
I18nJS.load_plugins!
|
72
|
+
I18nJS.initialize_plugins!(config: config)
|
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(:lint_translations, :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) || mapping
|
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
|
@@ -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
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
class CLI
|
5
|
+
class UI
|
6
|
+
attr_reader :stdout, :stderr
|
7
|
+
attr_accessor :colored
|
8
|
+
|
9
|
+
def initialize(stdout:, stderr:, colored: nil)
|
10
|
+
@stdout = stdout
|
11
|
+
@stderr = stderr
|
12
|
+
@colored = colored
|
13
|
+
end
|
14
|
+
|
15
|
+
def stdout_print(*message)
|
16
|
+
stdout << "#{message.join(' ')}\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
def stderr_print(*message)
|
20
|
+
stderr << "#{message.join(' ')}\n"
|
21
|
+
end
|
22
|
+
|
23
|
+
def fail_with(*message)
|
24
|
+
stderr_print(message)
|
25
|
+
exit(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
def exit_with(*message)
|
29
|
+
stdout_print(message)
|
30
|
+
exit(0)
|
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
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
class CLI
|
5
|
+
class VersionCommand < Command
|
6
|
+
command_name "version"
|
7
|
+
description "Show package version"
|
8
|
+
|
9
|
+
parse do |opts|
|
10
|
+
opts.banner = "Usage: i18n #{name}"
|
11
|
+
|
12
|
+
opts.on_tail do
|
13
|
+
ui.exit_with("v#{I18nJS::VERSION}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/i18n-js/cli.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../i18n-js"
|
4
|
+
require_relative "cli/command"
|
5
|
+
require_relative "cli/ui"
|
6
|
+
require_relative "cli/init_command"
|
7
|
+
require_relative "cli/version_command"
|
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"
|
12
|
+
require_relative "cli/check_command"
|
13
|
+
|
14
|
+
module I18nJS
|
15
|
+
class CLI
|
16
|
+
attr_reader :ui
|
17
|
+
|
18
|
+
def initialize(argv:, stdout:, stderr:, colored: stdout.tty?)
|
19
|
+
@argv = argv.dup
|
20
|
+
@ui = UI.new(stdout: stdout, stderr: stderr, colored: colored)
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
command_name = @argv.shift
|
25
|
+
command = commands.find {|cmd| cmd.name == command_name }
|
26
|
+
|
27
|
+
ui.fail_with(root_help) unless command
|
28
|
+
|
29
|
+
command.call
|
30
|
+
end
|
31
|
+
|
32
|
+
private def command_classes
|
33
|
+
[
|
34
|
+
InitCommand,
|
35
|
+
ExportCommand,
|
36
|
+
VersionCommand,
|
37
|
+
PluginsCommand,
|
38
|
+
LintTranslationsCommand,
|
39
|
+
LintScriptsCommand,
|
40
|
+
CheckCommand
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
private def commands
|
45
|
+
command_classes.map do |command_class|
|
46
|
+
command_class.new(argv: @argv, ui: ui)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private def root_help
|
51
|
+
commands_list = commands
|
52
|
+
.map {|cmd| "- #{cmd.name}: #{cmd.description}" }
|
53
|
+
.join("\n")
|
54
|
+
|
55
|
+
<<~TEXT
|
56
|
+
Usage: i18n COMMAND FLAGS
|
57
|
+
|
58
|
+
Commands:
|
59
|
+
|
60
|
+
#{commands_list}
|
61
|
+
|
62
|
+
Run `i18n COMMAND --help` for more information on specific commands.
|
63
|
+
TEXT
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nJS
|
4
|
+
require "i18n-js/plugin"
|
5
|
+
|
6
|
+
class EmbedFallbackTranslationsPlugin < I18nJS::Plugin
|
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
|
32
|
+
|
33
|
+
def self.deep_merge(target_hash, hash)
|
34
|
+
target_hash.merge(hash, &MERGER)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup
|
39
|
+
I18nJS::Schema.root_keys << config_key
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_schema
|
43
|
+
valid_keys = %i[enabled]
|
44
|
+
|
45
|
+
schema.expect_required_keys(keys: valid_keys, path: [config_key])
|
46
|
+
schema.reject_extraneous_keys(keys: valid_keys, path: [config_key])
|
47
|
+
end
|
48
|
+
|
49
|
+
def transform(translations:)
|
50
|
+
return translations unless enabled?
|
51
|
+
|
52
|
+
fallback_locale = I18n.default_locale.to_sym
|
53
|
+
locales_to_fallback = translations.keys - [fallback_locale]
|
54
|
+
|
55
|
+
translations_with_fallback = {}
|
56
|
+
translations_with_fallback[fallback_locale] =
|
57
|
+
translations[fallback_locale]
|
58
|
+
|
59
|
+
locales_to_fallback.each do |locale|
|
60
|
+
translations_with_fallback[locale] = Utils.deep_merge(
|
61
|
+
translations[fallback_locale], translations[locale]
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
translations_with_fallback
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
I18nJS.register_plugin(EmbedFallbackTranslationsPlugin)
|
70
|
+
end
|
@@ -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
|