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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +4 -0
  3. data/.github/FUNDING.yml +1 -1
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  5. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +38 -0
  8. data/.github/dependabot.yml +15 -0
  9. data/.github/workflows/ruby-tests.yml +73 -0
  10. data/.gitignore +13 -7
  11. data/.rubocop.yml +19 -0
  12. data/CHANGELOG.md +47 -512
  13. data/CODE_OF_CONDUCT.md +74 -0
  14. data/CONTRIBUTING.md +79 -0
  15. data/Gemfile +3 -0
  16. data/LICENSE.md +20 -0
  17. data/MIGRATING_FROM_V3_TO_V4.md +192 -0
  18. data/README.md +425 -921
  19. data/Rakefile +10 -20
  20. data/bin/release +81 -0
  21. data/exe/i18n +5 -0
  22. data/i18n-js.gemspec +51 -29
  23. data/lib/guard/i18n-js/templates/Guardfile +10 -0
  24. data/lib/guard/i18n-js/version.rb +13 -0
  25. data/lib/guard/i18n-js.rb +95 -0
  26. data/lib/i18n-js/clean_hash.rb +13 -0
  27. data/lib/i18n-js/cli/check_command.rb +17 -0
  28. data/lib/i18n-js/cli/command.rb +79 -0
  29. data/lib/i18n-js/cli/export_command.rb +95 -0
  30. data/lib/i18n-js/cli/init_command.rb +52 -0
  31. data/lib/i18n-js/cli/lint_scripts_command.rb +159 -0
  32. data/lib/i18n-js/cli/lint_translations_command.rb +157 -0
  33. data/lib/i18n-js/cli/plugins_command.rb +67 -0
  34. data/lib/i18n-js/cli/ui.rb +64 -0
  35. data/lib/i18n-js/cli/version_command.rb +18 -0
  36. data/lib/i18n-js/cli.rb +66 -0
  37. data/lib/i18n-js/embed_fallback_translations_plugin.rb +70 -0
  38. data/lib/i18n-js/export_files_plugin.rb +103 -0
  39. data/lib/i18n-js/lint.js +150645 -0
  40. data/lib/i18n-js/lint.ts +196 -0
  41. data/lib/i18n-js/listen.rb +96 -0
  42. data/lib/i18n-js/plugin.rb +103 -0
  43. data/lib/i18n-js/schema.rb +216 -0
  44. data/lib/i18n-js/sort_hash.rb +12 -0
  45. data/lib/i18n-js/version.rb +5 -0
  46. data/lib/i18n-js.rb +107 -1
  47. data/package.json +5 -20
  48. metadata +149 -189
  49. data/.editorconfig +0 -24
  50. data/.github/workflows/tests.yaml +0 -100
  51. data/.npmignore +0 -27
  52. data/Appraisals +0 -44
  53. data/app/assets/javascripts/i18n/filtered.js.erb +0 -23
  54. data/app/assets/javascripts/i18n/shims.js +0 -240
  55. data/app/assets/javascripts/i18n/translations.js +0 -3
  56. data/app/assets/javascripts/i18n.js +0 -1095
  57. data/gemfiles/i18n_0_6.gemfile +0 -7
  58. data/gemfiles/i18n_0_7.gemfile +0 -7
  59. data/gemfiles/i18n_0_8.gemfile +0 -7
  60. data/gemfiles/i18n_0_9.gemfile +0 -7
  61. data/gemfiles/i18n_1_0.gemfile +0 -7
  62. data/gemfiles/i18n_1_1.gemfile +0 -7
  63. data/gemfiles/i18n_1_2.gemfile +0 -7
  64. data/gemfiles/i18n_1_3.gemfile +0 -7
  65. data/gemfiles/i18n_1_4.gemfile +0 -7
  66. data/gemfiles/i18n_1_5.gemfile +0 -7
  67. data/gemfiles/i18n_1_6.gemfile +0 -7
  68. data/gemfiles/i18n_1_7.gemfile +0 -7
  69. data/gemfiles/i18n_1_8.gemfile +0 -7
  70. data/i18njs.png +0 -0
  71. data/lib/i18n/js/dependencies.rb +0 -63
  72. data/lib/i18n/js/engine.rb +0 -87
  73. data/lib/i18n/js/fallback_locales.rb +0 -70
  74. data/lib/i18n/js/formatters/base.rb +0 -25
  75. data/lib/i18n/js/formatters/js.rb +0 -32
  76. data/lib/i18n/js/formatters/json.rb +0 -13
  77. data/lib/i18n/js/middleware.rb +0 -82
  78. data/lib/i18n/js/private/config_store.rb +0 -31
  79. data/lib/i18n/js/private/hash_with_symbol_keys.rb +0 -36
  80. data/lib/i18n/js/segment.rb +0 -80
  81. data/lib/i18n/js/utils.rb +0 -78
  82. data/lib/i18n/js/version.rb +0 -7
  83. data/lib/i18n/js.rb +0 -264
  84. data/lib/rails/generators/i18n/js/config/config_generator.rb +0 -19
  85. data/lib/rails/generators/i18n/js/config/templates/i18n-js.yml +0 -27
  86. data/lib/tasks/export.rake +0 -8
  87. data/spec/fixtures/custom_path.yml +0 -5
  88. data/spec/fixtures/default.yml +0 -5
  89. data/spec/fixtures/erb.yml +0 -5
  90. data/spec/fixtures/except_condition.yml +0 -7
  91. data/spec/fixtures/js_export_dir_custom.yml +0 -7
  92. data/spec/fixtures/js_export_dir_none.yml +0 -6
  93. data/spec/fixtures/js_extend_parent.yml +0 -6
  94. data/spec/fixtures/js_extend_segment.yml +0 -6
  95. data/spec/fixtures/js_file_per_locale.yml +0 -7
  96. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +0 -4
  97. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +0 -6
  98. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +0 -4
  99. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +0 -4
  100. data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +0 -4
  101. data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +0 -4
  102. data/spec/fixtures/js_file_with_namespace_prefix_and_pretty_print.yml +0 -9
  103. data/spec/fixtures/js_sort_translation_keys_false.yml +0 -6
  104. data/spec/fixtures/js_sort_translation_keys_true.yml +0 -6
  105. data/spec/fixtures/json_only.yml +0 -18
  106. data/spec/fixtures/locales.yml +0 -133
  107. data/spec/fixtures/merge_plurals.yml +0 -6
  108. data/spec/fixtures/merge_plurals_with_no_overrides.yml +0 -4
  109. data/spec/fixtures/merge_plurals_with_partial_overrides.yml +0 -4
  110. data/spec/fixtures/millions.yml +0 -4
  111. data/spec/fixtures/multiple_conditions.yml +0 -7
  112. data/spec/fixtures/multiple_conditions_per_locale.yml +0 -7
  113. data/spec/fixtures/multiple_files.yml +0 -7
  114. data/spec/fixtures/no_config.yml +0 -2
  115. data/spec/fixtures/no_scope.yml +0 -4
  116. data/spec/fixtures/simple_scope.yml +0 -5
  117. data/spec/js/currency.spec.js +0 -62
  118. data/spec/js/current_locale.spec.js +0 -19
  119. data/spec/js/dates.spec.js +0 -276
  120. data/spec/js/defaults.spec.js +0 -31
  121. data/spec/js/extend.spec.js +0 -110
  122. data/spec/js/interpolation.spec.js +0 -124
  123. data/spec/js/jasmine/MIT.LICENSE +0 -20
  124. data/spec/js/jasmine/jasmine-html.js +0 -190
  125. data/spec/js/jasmine/jasmine.css +0 -166
  126. data/spec/js/jasmine/jasmine.js +0 -2476
  127. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  128. data/spec/js/locales.spec.js +0 -31
  129. data/spec/js/localization.spec.js +0 -78
  130. data/spec/js/numbers.spec.js +0 -174
  131. data/spec/js/placeholder.spec.js +0 -24
  132. data/spec/js/pluralization.spec.js +0 -219
  133. data/spec/js/prepare_options.spec.js +0 -41
  134. data/spec/js/require.js +0 -2083
  135. data/spec/js/specs.html +0 -49
  136. data/spec/js/specs_requirejs.html +0 -72
  137. data/spec/js/translate.spec.js +0 -304
  138. data/spec/js/translations.js +0 -188
  139. data/spec/js/utility_functions.spec.js +0 -20
  140. data/spec/ruby/i18n/js/fallback_locales_spec.rb +0 -84
  141. data/spec/ruby/i18n/js/segment_spec.rb +0 -219
  142. data/spec/ruby/i18n/js/utils_spec.rb +0 -106
  143. data/spec/ruby/i18n/js_spec.rb +0 -748
  144. data/spec/spec_helper.rb +0 -80
  145. 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
@@ -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