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.
@@ -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,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