i18n-js 4.0.1 → 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.
@@ -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 < Command
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
- options[:config_file] ||= config_file if File.file?(config_file)
153
- options[:require_file] ||= require_file if File.file?(require_file)
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
@@ -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
@@ -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
- [InitCommand, ExportCommand, VersionCommand, CheckCommand]
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,59 @@
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
+ def self.setup
10
+ I18nJS::Schema.root_keys << CONFIG_KEY
11
+ end
12
+
13
+ def self.validate_schema(config:)
14
+ return unless config.key?(CONFIG_KEY)
15
+
16
+ plugin_config = config[CONFIG_KEY]
17
+ valid_keys = %i[enabled]
18
+ schema = I18nJS::Schema.new(config)
19
+
20
+ schema.expect_required_keys(valid_keys, plugin_config)
21
+ schema.reject_extraneous_keys(valid_keys, plugin_config)
22
+ schema.expect_enabled_config(CONFIG_KEY, plugin_config[:enabled])
23
+ end
24
+
25
+ def self.transform(translations:, config:)
26
+ return translations unless config.dig(CONFIG_KEY, :enabled)
27
+
28
+ translations_glob = Glob.new(translations)
29
+ translations_glob << "*"
30
+
31
+ mapping = translations.keys.each_with_object({}) do |locale, buffer|
32
+ buffer[locale] = Glob.new(translations[locale]).tap do |glob|
33
+ glob << "*"
34
+ end
35
+ end
36
+
37
+ default_locale = I18n.default_locale
38
+ default_locale_glob = mapping.delete(default_locale)
39
+ default_locale_paths = default_locale_glob.paths
40
+
41
+ mapping.each do |locale, glob|
42
+ missing_keys = default_locale_paths - glob.paths
43
+
44
+ missing_keys.each do |key|
45
+ components = key.split(".").map(&:to_sym)
46
+ fallback_translation = translations.dig(default_locale, *components)
47
+
48
+ next unless fallback_translation
49
+
50
+ translations_glob.set([locale, key].join("."), fallback_translation)
51
+ end
52
+ end
53
+
54
+ translations_glob.to_h
55
+ end
56
+ end
57
+
58
+ I18nJS.register_plugin(EmbedFallbackTranslationsPlugin)
59
+ end