i18n-tasks 0.9.28 → 0.9.33

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: daf654e97c52cba1413c10e69c12510ed9ce438e2fabf089bdb05b38ac944c5c
4
- data.tar.gz: 7d3d264ca3a630497da4c7c5fc48907cf22c55bd0f16ac51bffed22febb540f4
3
+ metadata.gz: 9ac2026537e1679b2e306eddee2c97ca569d6c498d1a9850e512702626ab9c19
4
+ data.tar.gz: 49a908c2b2a9144919076a5c3948fa537486739dc9b02968366c7ec0d3ccf62b
5
5
  SHA512:
6
- metadata.gz: 2fd90385cd85755fcb9ef3f53a1a21f09a6ac217d8f9153871ade1cd38a155dbd2eb585a2ac0b059938b07b50578949c2291012fb97b4c1169a470b1ee59d84c
7
- data.tar.gz: de5ee6c4256599189f4934b97595364d39b242e4645ac017c59ea2f2a172296628340b53bbcd2b54df0657bbddd92fe0e8bee69f980e915b1a286a025eff8b78
6
+ metadata.gz: 639a786f1b1dfa5ee217e658975ac8b01b0966a4afdea49b69fdb4d1d4ff8cf9e48aab98eae0c6475a5c71fa4e8caccee88f1239205317550a03505a015fc932
7
+ data.tar.gz: d5a7ed2e9e1f4d2bf0474eb821e00713eff0fe486ef3cc92da12bc6e227bfecc987c5d2e70538e2c4436aba8958a763c8640d46c68d5ff2fdbf18e364d3d6cbc
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i18n-tasks [![Build Status][badge-travis]][travis] [![Coverage Status][badge-coverage]][coverage] [![Gemnasium][badge-gemnasium]][gemnasium] [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/glebm/i18n-tasks?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
1
+ # i18n-tasks [![Build Status][badge-travis]][travis] [![Coverage Status][badge-coverage]][coverage] [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/glebm/i18n-tasks?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2
2
 
3
3
  i18n-tasks helps you find and manage missing and unused translations.
4
4
 
@@ -22,7 +22,7 @@ i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (def
22
22
  Add i18n-tasks to the Gemfile:
23
23
 
24
24
  ```ruby
25
- gem 'i18n-tasks', '~> 0.9.28'
25
+ gem 'i18n-tasks', '~> 0.9.33'
26
26
  ```
27
27
 
28
28
  Copy the default [configuration file](#configuration):
@@ -45,7 +45,7 @@ $ cp $(i18n-tasks gem-path)/templates/minitest/i18n_test.rb test/
45
45
 
46
46
  ## Usage
47
47
 
48
- Run `i18n-tasks` to get the list of all the tasks with short descriptions.
48
+ Run `bundle exec i18n-tasks` to get the list of all the tasks with short descriptions.
49
49
 
50
50
  ### Check health
51
51
 
@@ -104,6 +104,17 @@ $ i18n-tasks translate-missing --backend=deepl
104
104
  $ i18n-tasks translate-missing --backend=deepl --from=en fr nl
105
105
  ```
106
106
 
107
+ ### Yandex Translate missing keys
108
+
109
+ Translate missing values with Yandex Translate ([more below on the API key](#yandex-translation-config)).
110
+
111
+ ```console
112
+ $ i18n-tasks translate-missing --backend=yandex
113
+
114
+ # accepts from and locales options:
115
+ $ i18n-tasks translate-missing --from=en es fr
116
+ ```
117
+
107
118
  ### Find usages
108
119
 
109
120
  See where the keys are used with `i18n-tasks find`:
@@ -324,6 +335,16 @@ A special syntax similar to file glob patterns is used throughout i18n-tasks to
324
335
  | `:` | matches a single key |
325
336
  | `{a, b.c}` | match any in set, can use `:` and `*`, match is captured |
326
337
 
338
+ Example of usage:
339
+
340
+ ```sh
341
+ $ bundle exec i18n-tasks mv "{:}.contents.{*}_body" "\1.attributes.\2.body"
342
+
343
+ car.contents.name_body ⮕ car.attributes.name.body
344
+ car.contents.description_body ⮕ car.attributes.description.body
345
+ truck.contents.name_body ⮕ truck.attributes.name.body
346
+ truck.contents.description_body ⮕ truck.attributes.description.body
347
+ ```
327
348
 
328
349
  #### Custom adapters
329
350
 
@@ -387,6 +408,17 @@ translation:
387
408
  deepl_api_key: <Deep Pro API key>
388
409
  ```
389
410
 
411
+ <a name="yandex-translation-config"></a>
412
+ ### Yandex Translate
413
+
414
+ `i18n-tasks translate-missing` requires a Yandex API key, get it at [Yandex](https://tech.yandex.com/translate).
415
+
416
+ ```yaml
417
+ # config/i18n-tasks.yml
418
+ translation:
419
+ yandex_api_key: <Yandex API key>
420
+ ```
421
+
390
422
  ## Interactive console
391
423
 
392
424
  `i18n-tasks irb` starts an IRB session in i18n-tasks context. Type `guide` for more information.
@@ -405,8 +437,6 @@ Custom tasks can be added easily, see the examples [on the wiki](https://github.
405
437
  [badge-travis]: https://img.shields.io/travis/glebm/i18n-tasks.svg
406
438
  [coverage]: https://codeclimate.com/github/glebm/i18n-tasks
407
439
  [badge-coverage]: https://api.codeclimate.com/v1/badges/5d173e90ada8df07cedc/test_coverage
408
- [gemnasium]: https://gemnasium.com/glebm/i18n-tasks
409
- [badge-gemnasium]: https://gemnasium.com/glebm/i18n-tasks.svg
410
440
  [config]: https://github.com/glebm/i18n-tasks/blob/master/templates/config/i18n-tasks.yml
411
441
  [wiki]: https://github.com/glebm/i18n-tasks/wiki "i18n-tasks wiki"
412
442
  [i18n-gem]: https://github.com/svenfuchs/i18n "svenfuchs/i18n on Github"
@@ -120,3 +120,9 @@ en:
120
120
  none: Every translation is in use.
121
121
  usages:
122
122
  none: No key usages found.
123
+ yandex_translate:
124
+ errors:
125
+ no_api_key: >-
126
+ Set Yandex API key via YANDEX_API_KEY environment variable or translation.yandex_api_key
127
+ in config/i18n-tasks.yml. Get the key at https://tech.yandex.com/translate.
128
+ no_results: Yandex returned no results.
@@ -122,3 +122,9 @@ ru:
122
122
  none: Все переводы используются.
123
123
  usages:
124
124
  none: Не найдено использований.
125
+ yandex_translate:
126
+ errors:
127
+ no_api_key: |-
128
+ Установить ключ API Яндекса с помощью переменной среды YANDEX_API_KEY или translation.yandex_api_key
129
+ в config / i18n-tasks.yml. Получите ключ по адресу https://tech.yandex.com/translate/.
130
+ no_results: Яндекс не дал результатов.
@@ -19,13 +19,13 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
19
19
  TEXT
20
20
  s.post_install_message = <<~TEXT
21
21
  # Install default configuration:
22
- cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
22
+ cp $(bundle exec i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
23
23
  # Add an RSpec for missing and unused keys:
24
- cp $(i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
24
+ cp $(bundle exec i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
25
25
  TEXT
26
26
  s.homepage = 'https://github.com/glebm/i18n-tasks'
27
27
  s.metadata = { 'issue_tracker' => 'https://github.com/glebm/i18n-tasks' } if s.respond_to?(:metadata=)
28
- s.required_ruby_version = '~> 2.3' if s.respond_to?(:required_ruby_version=)
28
+ s.required_ruby_version = '>= 2.3', '< 4.0' if s.respond_to?(:required_ruby_version=)
29
29
 
30
30
  s.files = `git ls-files`.split($/)
31
31
  s.files -= s.files.grep(%r{^(doc/|\.|spec/)}) + %w[CHANGES.md config/i18n-tasks.yml Gemfile]
@@ -43,12 +43,15 @@ TEXT
43
43
  s.add_dependency 'rainbow', '>= 2.2.2', '< 4.0'
44
44
  s.add_dependency 'terminal-table', '>= 1.5.1'
45
45
  s.add_development_dependency 'axlsx', '~> 2.0'
46
- s.add_development_dependency 'bundler', '~> 1.3'
47
- s.add_development_dependency 'deepl-rb', '>= 2.1.0'
48
- s.add_development_dependency 'easy_translate', '>= 0.5.1'
46
+ s.add_development_dependency 'bundler', '~> 2.0', '>= 2.0.1'
49
47
  s.add_development_dependency 'rake'
50
48
  s.add_development_dependency 'rspec', '~> 3.3'
51
49
  s.add_development_dependency 'rubocop', '~> 0.53.0'
52
50
  s.add_development_dependency 'simplecov'
53
51
  s.add_development_dependency 'yard'
52
+
53
+ # Translation backends
54
+ s.add_development_dependency 'deepl-rb', '>= 2.1.0'
55
+ s.add_development_dependency 'easy_translate', '>= 0.5.1' # Google Translate
56
+ s.add_development_dependency 'yandex-translator', '>= 0.3.3'
54
57
  end
@@ -68,8 +68,3 @@ require 'i18n'
68
68
  Dir[File.join(I18n::Tasks.gem_path, 'config', 'locales', '*.yml')].each do |locale_file|
69
69
  I18n.config.load_path << locale_file
70
70
  end
71
-
72
- # Load pluralization data
73
- require 'rails-i18n'
74
- I18n.enforce_available_locales = false
75
- RailsI18n::Railtie.add('rails/pluralization/*.rb')
@@ -22,7 +22,7 @@ module I18n::Tasks
22
22
  if opts.empty? || method(name).arity.zero?
23
23
  send name
24
24
  else
25
- send name, opts
25
+ send name, **opts
26
26
  end
27
27
  end
28
28
 
@@ -17,10 +17,10 @@ module I18n::Tasks
17
17
  fail CommandError, t('i18n_tasks.health.no_keys_detected') if stats[:key_count].zero?
18
18
  terminal_report.forest_stats forest, stats
19
19
  [
20
- missing(opt),
21
- unused(opt),
22
- check_consistent_interpolations(opt),
23
- check_normalized(opt)
20
+ missing(**opt),
21
+ unused(**opt),
22
+ check_consistent_interpolations(**opt),
23
+ check_normalized(**opt)
24
24
  ].detect { |result| result == :exit_1 }
25
25
  end
26
26
  end
@@ -12,7 +12,7 @@ module I18n::Tasks
12
12
  args: %i[locales out_format]
13
13
 
14
14
  def check_consistent_interpolations(opt = {})
15
- forest = i18n.inconsistent_interpolations(opt.slice(:locales, :base_locale))
15
+ forest = i18n.inconsistent_interpolations(**opt.slice(:locales, :base_locale))
16
16
  print_forest forest, opt, :inconsistent_interpolations
17
17
  :exit_1 unless forest.empty?
18
18
  end
@@ -28,7 +28,7 @@ module I18n::Tasks
28
28
  args: %i[locales out_format missing_types]
29
29
 
30
30
  def missing(opt = {})
31
- forest = i18n.missing_keys(opt.slice(:locales, :base_locale, :types))
31
+ forest = i18n.missing_keys(**opt.slice(:locales, :base_locale, :types))
32
32
  print_forest forest, opt, :missing_keys
33
33
  :exit_1 unless forest.empty?
34
34
  end
@@ -32,7 +32,7 @@ module I18n::Tasks
32
32
  args: %i[locales out_format strict]
33
33
 
34
34
  def unused(opt = {})
35
- forest = i18n.unused_keys(opt.slice(:locales, :strict))
35
+ forest = i18n.unused_keys(**opt.slice(:locales, :strict))
36
36
  print_forest forest, opt, :unused_keys
37
37
  :exit_1 unless forest.empty?
38
38
  end
@@ -40,10 +40,16 @@ module I18n::Tasks
40
40
  cmd :remove_unused,
41
41
  pos: '[locale ...]',
42
42
  desc: t('i18n_tasks.cmd.desc.remove_unused'),
43
- args: %i[locales out_format strict keep_order confirm]
43
+ args: %i[locales out_format strict keep_order confirm pattern]
44
+
45
+ def remove_unused(opt = {}) # rubocop:disable Metrics/AbcSize
46
+ unused_keys = i18n.unused_keys(**opt.slice(:locales, :strict))
47
+
48
+ if opt[:pattern]
49
+ pattern_re = i18n.compile_key_pattern(opt[:pattern])
50
+ unused_keys = unused_keys.select_keys { |full_key, _node| full_key =~ pattern_re }
51
+ end
44
52
 
45
- def remove_unused(opt = {})
46
- unused_keys = i18n.unused_keys(opt.slice(:locales, :strict))
47
53
  if unused_keys.present?
48
54
  terminal_report.unused_keys(unused_keys)
49
55
  confirm_remove_unused!(unused_keys, opt)
@@ -42,8 +42,8 @@ module I18n::Tasks
42
42
  end
43
43
 
44
44
  # late-bound I18n.t for module bodies
45
- def t(*args)
46
- proc { I18n.t(*args) }
45
+ def t(*args, **opts)
46
+ proc { I18n.t(*args, **opts) }
47
47
  end
48
48
 
49
49
  # if class is a module, merge DSL definitions when it is included
@@ -59,6 +59,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
59
59
  conf = (config[:translation] || {}).with_indifferent_access
60
60
  conf[:google_translate_api_key] = ENV['GOOGLE_TRANSLATE_API_KEY'] if ENV.key?('GOOGLE_TRANSLATE_API_KEY')
61
61
  conf[:deepl_api_key] = ENV['DEEPL_AUTH_KEY'] if ENV.key?('DEEPL_AUTH_KEY')
62
+ conf[:yandex_api_key] = ENV['YANDEX_API_KEY'] if ENV.key?('YANDEX_API_KEY')
62
63
  conf
63
64
  end
64
65
  end
@@ -9,7 +9,7 @@ module I18n::Tasks
9
9
  # @return [Hash] locale tree
10
10
  def parse(str, options)
11
11
  if YAML.method(:load).arity.abs == 2
12
- YAML.load(str, options || {})
12
+ YAML.load(str, **(options || {}))
13
13
  else
14
14
  # older jruby and rbx 2.2.7 do not accept options
15
15
  YAML.load(str)
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'i18n/tasks/data/tree/traversal'
4
4
  require 'i18n/tasks/data/tree/siblings'
5
- require 'i18n/tasks/rainbow_utils'
6
5
  module I18n::Tasks::Data::Tree
7
6
  class Node # rubocop:disable Metrics/ClassLength
8
7
  include Enumerable
@@ -28,7 +27,7 @@ module I18n::Tasks::Data::Tree
28
27
  end
29
28
 
30
29
  def derive(new_attr = {})
31
- self.class.new(attributes.merge(new_attr))
30
+ self.class.new(**attributes.merge(new_attr))
32
31
  end
33
32
 
34
33
  def children=(children)
@@ -164,7 +163,7 @@ module I18n::Tasks::Data::Tree
164
163
 
165
164
  def inspect(level = 0)
166
165
  label = if key.nil?
167
- I18n::Tasks::RainbowUtils.faint_color('∅')
166
+ Rainbow('∅').faint
168
167
  else
169
168
  [Rainbow(key).color(1 + level % 15),
170
169
  (": #{format_value_for_inspect(value)}" if leaf?),
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'i18n/tasks/data/tree/traversal'
4
- require 'i18n/tasks/rainbow_utils'
5
4
  module I18n::Tasks::Data::Tree
6
5
  # A list of nodes
7
6
  class Nodes
@@ -48,7 +47,7 @@ module I18n::Tasks::Data::Tree
48
47
  if present?
49
48
  map(&:inspect) * "\n"
50
49
  else
51
- I18n::Tasks::RainbowUtils.faint_color('{∅}')
50
+ Rainbow('{∅}').faint
52
51
  end
53
52
  end
54
53
 
@@ -11,6 +11,7 @@ module I18n::Tasks::Data::Tree
11
11
  # siblings' keys are unique
12
12
  class Siblings < Nodes # rubocop:disable Metrics/ClassLength
13
13
  include ::I18n::Tasks::SplitKey
14
+ include ::I18n::Tasks::PluralKeys
14
15
 
15
16
  attr_reader :parent, :key_to_node
16
17
 
@@ -100,12 +101,12 @@ module I18n::Tasks::Data::Tree
100
101
  key: key_part,
101
102
  parent: parent,
102
103
  children: [],
103
- warn_about_add_children_to_leaf: @warn_add_children_to_leaf
104
+ warn_about_add_children_to_leaf: @warn_about_add_children_to_leaf
104
105
  )
105
106
  append! child
106
107
  end
107
108
  unless child.children
108
- warn_add_children_to_leaf child if @warn_about_add_children_to_leaf
109
+ conditionally_warn_add_children_to_leaf(child, [])
109
110
  child.children = []
110
111
  end
111
112
  child.children.set rest, node
@@ -187,7 +188,7 @@ module I18n::Tasks::Data::Tree
187
188
  if our.children
188
189
  our.children.merge!(node.children)
189
190
  else
190
- warn_add_children_to_leaf our
191
+ conditionally_warn_add_children_to_leaf(our, node.children)
191
192
  our.children = node.children
192
193
  end
193
194
  elsif on_leaves_merge
@@ -228,6 +229,13 @@ module I18n::Tasks::Data::Tree
228
229
  end
229
230
  end
230
231
 
232
+ def conditionally_warn_add_children_to_leaf(node, children)
233
+ return unless @warn_about_add_children_to_leaf
234
+ return if plural_forms?(children)
235
+
236
+ warn_add_children_to_leaf(node)
237
+ end
238
+
231
239
  def warn_add_children_to_leaf(node)
232
240
  ::I18n::Tasks::Logging.log_warn "'#{node.full_key}' was a leaf, now has children (value <- scope conflict)"
233
241
  end
@@ -265,7 +273,7 @@ module I18n::Tasks::Data::Tree
265
273
  build_forest(opts) do |forest|
266
274
  key_attrs.each do |(full_key, attr)|
267
275
  fail "Invalid key #{full_key.inspect}" if full_key.end_with?('.')
268
- node = ::I18n::Tasks::Data::Tree::Node.new(attr.merge(key: split_key(full_key).last))
276
+ node = ::I18n::Tasks::Data::Tree::Node.new(**attr.merge(key: split_key(full_key).last))
269
277
  yield(full_key, node) if block
270
278
  forest[full_key] = node
271
279
  end
@@ -60,14 +60,12 @@ module I18n::Tasks
60
60
  self
61
61
  end
62
62
 
63
- def key_names(opts = {})
64
- opts[:root] = false unless opts.key?(:root)
65
- keys(opts).map { |key, _node| key }
63
+ def key_names(root: false)
64
+ keys(root: root).map { |key, _node| key }
66
65
  end
67
66
 
68
- def key_values(opts = {})
69
- opts[:root] = false unless opts.key?(:root)
70
- keys(opts).map { |key, node| [key, node.value] }
67
+ def key_values(root: false)
68
+ keys(root: root).map { |key, node| [key, node.value] }
71
69
  end
72
70
 
73
71
  def root_key_values(sort = false)
@@ -146,12 +144,12 @@ module I18n::Tasks
146
144
  # @return [Siblings]
147
145
  def intersect_keys(other_tree, key_opts = {}, &block)
148
146
  if block
149
- select_keys(key_opts) do |key, node|
147
+ select_keys(**key_opts.slice(:root)) do |key, node|
150
148
  other_node = other_tree[key]
151
149
  other_node && yield(key, node, other_node)
152
150
  end
153
151
  else
154
- select_keys(key_opts) { |key, _node| other_tree[key] }
152
+ select_keys(**key_opts.slice(:root)) { |key, _node| other_tree[key] }
155
153
  end
156
154
  end
157
155
 
@@ -58,8 +58,8 @@ module I18n::Tasks
58
58
 
59
59
  def missing_plural_forest(locales, _base = base_locale)
60
60
  locales.each_with_object(empty_forest) do |locale, forest|
61
- next unless I18n.exists?(:'i18n.plural.keys', locale)
62
- required_keys = Set.new(I18n.t(:'i18n.plural.keys', locale: locale, resolve: false))
61
+ required_keys = required_plural_keys_for_locale(locale)
62
+ next if required_keys.empty?
63
63
  tree = empty_forest
64
64
  plural_nodes data[locale] do |node|
65
65
  children = node.children
@@ -76,6 +76,19 @@ module I18n::Tasks
76
76
  end
77
77
  end
78
78
 
79
+ def required_plural_keys_for_locale(locale)
80
+ @plural_keys_for_locale ||= {}
81
+ return @plural_keys_for_locale[locale] if @plural_keys_for_locale.key?(locale)
82
+
83
+ @plural_keys_for_locale[locale] = plural_keys_for_locale(locale)
84
+ end
85
+
86
+ # Loads rails-i18n pluralization config for the given locale.
87
+ def load_rails_i18n_pluralization!(locale)
88
+ path = File.join(Gem::Specification.find_by_name('rails-i18n').gem_dir, 'rails', 'pluralization', "#{locale}.rb")
89
+ eval(File.read(path), binding, path) # rubocop:disable Security/Eval
90
+ end
91
+
79
92
  # keys present in compared_to, but not in locale
80
93
  def missing_diff_tree(locale, compared_to = base_locale)
81
94
  data[compared_to].select_keys do |key, _node|
@@ -106,7 +119,7 @@ module I18n::Tasks
106
119
  end
107
120
 
108
121
  def locale_key_missing?(locale, key)
109
- !key_value?(key, locale) && !external_key?(key) && !ignore_key?(key, :missing)
122
+ !key_value?(key, locale) && !external_key?(key, locale) && !ignore_key?(key, :missing)
110
123
  end
111
124
 
112
125
  # @param [::I18n::Tasks::Data::Tree::Siblings] forest
@@ -137,5 +150,32 @@ module I18n::Tasks
137
150
  end
138
151
  forest
139
152
  end
153
+
154
+ private
155
+
156
+ def plural_keys_for_locale(locale)
157
+ configuration = load_rails_i18n_pluralization!(locale)
158
+ if configuration[locale.to_sym].nil?
159
+ alternate_locale = alternate_locale_from(locale)
160
+ return Set.new if configuration[alternate_locale.to_sym].nil?
161
+
162
+ return set_from_rails_i18n_pluralization(configuration, alternate_locale)
163
+ end
164
+ set_from_rails_i18n_pluralization(configuration, locale)
165
+ rescue SystemCallError, IOError
166
+ Set.new
167
+ end
168
+
169
+ def alternate_locale_from(locale)
170
+ re = /(\w{2})-*(\w{2,3})*/
171
+ match = locale.match(re)
172
+ language_code = match[1]
173
+ country_code = match[2]
174
+ "#{language_code}-#{country_code.upcase}"
175
+ end
176
+
177
+ def set_from_rails_i18n_pluralization(configuration, locale)
178
+ Set.new(configuration[locale.to_sym][:i18n][:plural][:keys])
179
+ end
140
180
  end
141
181
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  require 'set'
4
4
  module I18n::Tasks::PluralKeys
5
- PLURAL_KEY_SUFFIXES = Set.new %w[zero one two few many other]
6
- PLURAL_KEY_RE = /\.(?:#{PLURAL_KEY_SUFFIXES.to_a * '|'})$/
5
+ # Ref: http://cldr.unicode.org/index/cldr-spec/plural-rules
6
+ CLDR_CATEGORY_KEYS = %w[zero one two few many other].freeze
7
+ PLURAL_KEY_SUFFIXES = Set.new CLDR_CATEGORY_KEYS
8
+ PLURAL_KEY_RE = /\.(?:#{CLDR_CATEGORY_KEYS * '|'})$/
7
9
 
8
10
  def collapse_plural_nodes!(tree)
9
11
  tree.leaves.map(&:parent).compact.uniq.each do |node|
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'i18n/tasks/reports/base'
4
- require 'i18n/tasks/rainbow_utils'
5
4
  require 'terminal-table'
6
5
  module I18n
7
6
  module Tasks
@@ -76,11 +75,11 @@ module I18n
76
75
 
77
76
  def forest_stats(forest, stats = task.forest_stats(forest))
78
77
  text = if stats[:locale_count] == 1
79
- I18n.t('i18n_tasks.data_stats.text_single_locale', stats)
78
+ I18n.t('i18n_tasks.data_stats.text_single_locale', **stats)
80
79
  else
81
- I18n.t('i18n_tasks.data_stats.text', stats)
80
+ I18n.t('i18n_tasks.data_stats.text', **stats)
82
81
  end
83
- title = Rainbow(I18n.t('i18n_tasks.data_stats.title', stats.slice(:locales))).bright
82
+ title = Rainbow(I18n.t('i18n_tasks.data_stats.title', **stats.slice(:locales))).bright
84
83
  print_info "#{Rainbow(title).cyan} #{Rainbow(text).cyan}"
85
84
  end
86
85
 
@@ -170,7 +169,7 @@ module I18n
170
169
  end
171
170
 
172
171
  def print_title(title)
173
- log_stderr "#{Rainbow(title.strip).bright} #{I18n::Tasks::RainbowUtils.faint_color('|')} " \
172
+ log_stderr "#{Rainbow(title.strip).bright} #{Rainbow('|').faint} " \
174
173
  "#{"i18n-tasks v#{I18n::Tasks::VERSION}"}"
175
174
  end
176
175
 
@@ -12,13 +12,15 @@ module I18n::Tasks::Scanners
12
12
  include OccurrenceFromPosition
13
13
  include RubyKeyLiterals
14
14
 
15
- TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'\-]I18n\.|I18n\.)t(?:ranslate)?/
15
+ TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'\-]I18n\.|I18n\.)t(?:!|ranslate!?)?/
16
16
  IGNORE_LINES = {
17
- 'opal' => /^\s*#(?!\si18n-tasks-use)/,
18
- 'haml' => /^\s*-\s*#(?!\si18n-tasks-use)/,
19
- 'slim' => %r{^\s*(?:-#|/)(?!\si18n-tasks-use)},
20
17
  'coffee' => /^\s*#(?!\si18n-tasks-use)/,
21
- 'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/
18
+ 'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
19
+ 'es6' => %r{^\s*//(?!\si18n-tasks-use)},
20
+ 'haml' => /^\s*-\s*#(?!\si18n-tasks-use)/,
21
+ 'js' => %r{^\s*//(?!\si18n-tasks-use)},
22
+ 'opal' => /^\s*#(?!\si18n-tasks-use)/,
23
+ 'slim' => %r{^\s*(?:-#|/)(?!\si18n-tasks-use)}
22
24
  }.freeze
23
25
 
24
26
  def initialize(**args)
@@ -15,10 +15,10 @@ module I18n::Tasks::Scanners
15
15
  include AST::Sexp
16
16
 
17
17
  MAGIC_COMMENT_PREFIX = /\A.\s*i18n-tasks-use\s+/
18
- RECEIVER_MESSAGES = [nil, AST::Node.new(:const, [nil, :I18n])].product(%i[t translate])
18
+ RECEIVER_MESSAGES = [nil, AST::Node.new(:const, [nil, :I18n])].product(%i[t t! translate translate!])
19
19
 
20
20
  def initialize(**args)
21
- super(args)
21
+ super(**args)
22
22
  @parser = ::Parser::CurrentRuby.new
23
23
  @magic_comment_parser = ::Parser::CurrentRuby.new
24
24
  @call_finder = RubyAstCallFinder.new(
@@ -2,12 +2,13 @@
2
2
 
3
3
  require 'i18n/tasks/translators/deepl_translator.rb'
4
4
  require 'i18n/tasks/translators/google_translator.rb'
5
+ require 'i18n/tasks/translators/yandex_translator.rb'
5
6
 
6
7
  module I18n::Tasks
7
8
  module Translation
8
9
  # @param [I18n::Tasks::Tree::Siblings] forest to translate to the locales of its root nodes
9
10
  # @param [String] from locale
10
- # @param [:deepl, :google] backend
11
+ # @param [:deepl, :google, :yandex] backend
11
12
  # @return [I18n::Tasks::Tree::Siblings] translated forest
12
13
  def translate_forest(forest, from:, backend: :google)
13
14
  case backend
@@ -15,6 +16,8 @@ module I18n::Tasks
15
16
  Translators::DeeplTranslator.new(self).translate_forest(forest, from)
16
17
  when :google
17
18
  Translators::GoogleTranslator.new(self).translate_forest(forest, from)
19
+ when :yandex
20
+ Translators::YandexTranslator.new(self).translate_forest(forest, from)
18
21
  else
19
22
  fail CommandError, "invalid backend: #{backend}"
20
23
  end
@@ -17,7 +17,12 @@ module I18n::Tasks::Translators
17
17
  protected
18
18
 
19
19
  def translate_values(list, from:, to:, **options)
20
- DeepL.translate(list, to_deepl_compatible_locale(from), to_deepl_compatible_locale(to), options).map(&:text)
20
+ result = DeepL.translate(list, to_deepl_compatible_locale(from), to_deepl_compatible_locale(to), options)
21
+ if result.is_a?(DeepL::Resources::Text)
22
+ [result.text]
23
+ else
24
+ result.map(&:text)
25
+ end
21
26
  end
22
27
 
23
28
  def options_for_translate_values(**options)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/translators/base_translator'
4
+
5
+ module I18n::Tasks::Translators
6
+ class YandexTranslator < BaseTranslator
7
+ def initialize(*)
8
+ begin
9
+ require 'yandex-translator'
10
+ rescue LoadError
11
+ raise ::I18n::Tasks::CommandError, "Add gem 'yandex-translator' to your Gemfile to use this command"
12
+ end
13
+ super
14
+ end
15
+
16
+ protected
17
+
18
+ def translate_values(list, **options)
19
+ list.map { |item| translator.translate(item, options) }
20
+ end
21
+
22
+ def options_for_translate_values(from:, to:, **options)
23
+ options.merge(
24
+ from: to_yandex_compatible_locale(from),
25
+ to: to_yandex_compatible_locale(to)
26
+ )
27
+ end
28
+
29
+ def options_for_html
30
+ { format: 'html' }
31
+ end
32
+
33
+ def options_for_plain
34
+ { format: 'plain' }
35
+ end
36
+
37
+ def no_results_error_message
38
+ I18n.t('i18n_tasks.yandex_translate.errors.no_results')
39
+ end
40
+
41
+ private
42
+
43
+ # Convert 'es-ES' to 'es'
44
+ def to_yandex_compatible_locale(locale)
45
+ return locale unless locale.include?('-')
46
+ locale.split('-', 2).first
47
+ end
48
+
49
+ def translator
50
+ @translator ||= Yandex::Translator.new(api_key)
51
+ end
52
+
53
+ def api_key
54
+ @api_key ||= begin
55
+ key = @i18n_tasks.translation_config[:yandex_api_key]
56
+ fail ::I18n::Tasks::CommandError, I18n.t('i18n_tasks.yandex_translate.errors.no_api_key') if key.blank?
57
+ key
58
+ end
59
+ end
60
+ end
61
+ end
@@ -25,7 +25,8 @@ module I18n::Tasks
25
25
  }.freeze
26
26
 
27
27
  ALWAYS_EXCLUDE = %w[*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less
28
- *.yml *.json *.zip *.tar.gz *.swf *.flv].freeze
28
+ *.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus
29
+ *.webp].freeze
29
30
 
30
31
  # Find all keys in the source and return a forest with the keys in absolute form and their occurrences.
31
32
  #
@@ -2,6 +2,6 @@
2
2
 
3
3
  module I18n
4
4
  module Tasks
5
- VERSION = '0.9.28'
5
+ VERSION = '0.9.33'
6
6
  end
7
7
  end
@@ -32,7 +32,7 @@ data:
32
32
  # This data is not considered unused and is never written to.
33
33
  external:
34
34
  ## Example (replace %#= with %=):
35
- # - "<%#= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml"
35
+ # - "<%#= %x[bundle info vagrant --path].chomp %>/templates/locales/%{locale}.yml"
36
36
 
37
37
  ## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
38
38
  # router: conservative_router
@@ -23,7 +23,14 @@ class I18nTest < ActiveSupport::TestCase
23
23
  non_normalized = @i18n.non_normalized_paths
24
24
  error_message = "The following files need to be normalized:\n" \
25
25
  "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \
26
- 'Please run `i18n-tasks normalize` to fix'
26
+ "Please run `i18n-tasks normalize' to fix"
27
27
  assert_empty non_normalized, error_message
28
28
  end
29
+
30
+ def test_no_inconsistent_interpolations
31
+ inconsistent_interpolations = @i18n.inconsistent_interpolations
32
+ error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
33
+ "Please run `i18n-tasks check-consistent-interpolations' to show them"
34
+ assert_empty inconsistent_interpolations, error_message
35
+ end
29
36
  end
@@ -2,10 +2,11 @@
2
2
 
3
3
  require 'i18n/tasks'
4
4
 
5
- RSpec.describe 'I18n' do
5
+ RSpec.describe I18n do
6
6
  let(:i18n) { I18n::Tasks::BaseTask.new }
7
7
  let(:missing_keys) { i18n.missing_keys }
8
8
  let(:unused_keys) { i18n.unused_keys }
9
+ let(:inconsistent_interpolations) { i18n.inconsistent_interpolations }
9
10
 
10
11
  it 'does not have missing keys' do
11
12
  expect(missing_keys).to be_empty,
@@ -21,7 +22,13 @@ RSpec.describe 'I18n' do
21
22
  non_normalized = i18n.non_normalized_paths
22
23
  error_message = "The following files need to be normalized:\n" \
23
24
  "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \
24
- 'Please run `i18n-tasks normalize` to fix'
25
+ "Please run `i18n-tasks normalize' to fix"
25
26
  expect(non_normalized).to be_empty, error_message
26
27
  end
28
+
29
+ it 'does not have inconsistent interpolations' do
30
+ error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
31
+ "Run `i18n-tasks check-consistent-interpolations' to show them"
32
+ expect(inconsistent_interpolations).to be_empty, error_message
33
+ end
27
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.28
4
+ version: 0.9.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - glebm
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-29 00:00:00.000000000 Z
11
+ date: 2020-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -162,42 +162,20 @@ dependencies:
162
162
  requirements:
163
163
  - - "~>"
164
164
  - !ruby/object:Gem::Version
165
- version: '1.3'
166
- type: :development
167
- prerelease: false
168
- version_requirements: !ruby/object:Gem::Requirement
169
- requirements:
170
- - - "~>"
171
- - !ruby/object:Gem::Version
172
- version: '1.3'
173
- - !ruby/object:Gem::Dependency
174
- name: deepl-rb
175
- requirement: !ruby/object:Gem::Requirement
176
- requirements:
165
+ version: '2.0'
177
166
  - - ">="
178
167
  - !ruby/object:Gem::Version
179
- version: 2.1.0
168
+ version: 2.0.1
180
169
  type: :development
181
170
  prerelease: false
182
171
  version_requirements: !ruby/object:Gem::Requirement
183
172
  requirements:
184
- - - ">="
185
- - !ruby/object:Gem::Version
186
- version: 2.1.0
187
- - !ruby/object:Gem::Dependency
188
- name: easy_translate
189
- requirement: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - ">="
173
+ - - "~>"
192
174
  - !ruby/object:Gem::Version
193
- version: 0.5.1
194
- type: :development
195
- prerelease: false
196
- version_requirements: !ruby/object:Gem::Requirement
197
- requirements:
175
+ version: '2.0'
198
176
  - - ">="
199
177
  - !ruby/object:Gem::Version
200
- version: 0.5.1
178
+ version: 2.0.1
201
179
  - !ruby/object:Gem::Dependency
202
180
  name: rake
203
181
  requirement: !ruby/object:Gem::Requirement
@@ -268,6 +246,48 @@ dependencies:
268
246
  - - ">="
269
247
  - !ruby/object:Gem::Version
270
248
  version: '0'
249
+ - !ruby/object:Gem::Dependency
250
+ name: deepl-rb
251
+ requirement: !ruby/object:Gem::Requirement
252
+ requirements:
253
+ - - ">="
254
+ - !ruby/object:Gem::Version
255
+ version: 2.1.0
256
+ type: :development
257
+ prerelease: false
258
+ version_requirements: !ruby/object:Gem::Requirement
259
+ requirements:
260
+ - - ">="
261
+ - !ruby/object:Gem::Version
262
+ version: 2.1.0
263
+ - !ruby/object:Gem::Dependency
264
+ name: easy_translate
265
+ requirement: !ruby/object:Gem::Requirement
266
+ requirements:
267
+ - - ">="
268
+ - !ruby/object:Gem::Version
269
+ version: 0.5.1
270
+ type: :development
271
+ prerelease: false
272
+ version_requirements: !ruby/object:Gem::Requirement
273
+ requirements:
274
+ - - ">="
275
+ - !ruby/object:Gem::Version
276
+ version: 0.5.1
277
+ - !ruby/object:Gem::Dependency
278
+ name: yandex-translator
279
+ requirement: !ruby/object:Gem::Requirement
280
+ requirements:
281
+ - - ">="
282
+ - !ruby/object:Gem::Version
283
+ version: 0.3.3
284
+ type: :development
285
+ prerelease: false
286
+ version_requirements: !ruby/object:Gem::Requirement
287
+ requirements:
288
+ - - ">="
289
+ - !ruby/object:Gem::Version
290
+ version: 0.3.3
271
291
  description: |
272
292
  i18n-tasks helps you find and manage missing and unused translations.
273
293
 
@@ -334,7 +354,6 @@ files:
334
354
  - lib/i18n/tasks/logging.rb
335
355
  - lib/i18n/tasks/missing_keys.rb
336
356
  - lib/i18n/tasks/plural_keys.rb
337
- - lib/i18n/tasks/rainbow_utils.rb
338
357
  - lib/i18n/tasks/references.rb
339
358
  - lib/i18n/tasks/reports/base.rb
340
359
  - lib/i18n/tasks/reports/terminal.rb
@@ -363,6 +382,7 @@ files:
363
382
  - lib/i18n/tasks/translators/base_translator.rb
364
383
  - lib/i18n/tasks/translators/deepl_translator.rb
365
384
  - lib/i18n/tasks/translators/google_translator.rb
385
+ - lib/i18n/tasks/translators/yandex_translator.rb
366
386
  - lib/i18n/tasks/unused_keys.rb
367
387
  - lib/i18n/tasks/used_keys.rb
368
388
  - lib/i18n/tasks/version.rb
@@ -376,25 +396,27 @@ metadata:
376
396
  issue_tracker: https://github.com/glebm/i18n-tasks
377
397
  post_install_message: |
378
398
  # Install default configuration:
379
- cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
399
+ cp $(bundle exec i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
380
400
  # Add an RSpec for missing and unused keys:
381
- cp $(i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
401
+ cp $(bundle exec i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
382
402
  rdoc_options: []
383
403
  require_paths:
384
404
  - lib
385
405
  required_ruby_version: !ruby/object:Gem::Requirement
386
406
  requirements:
387
- - - "~>"
407
+ - - ">="
388
408
  - !ruby/object:Gem::Version
389
409
  version: '2.3'
410
+ - - "<"
411
+ - !ruby/object:Gem::Version
412
+ version: '4.0'
390
413
  required_rubygems_version: !ruby/object:Gem::Requirement
391
414
  requirements:
392
415
  - - ">="
393
416
  - !ruby/object:Gem::Version
394
417
  version: '0'
395
418
  requirements: []
396
- rubyforge_project:
397
- rubygems_version: 2.7.7
419
+ rubygems_version: 3.1.2
398
420
  signing_key:
399
421
  specification_version: 4
400
422
  summary: Manage localization and translation with the awesome power of static analysis
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module I18n
4
- module Tasks
5
- module RainbowUtils
6
- # TODO: This method can be removed after below PR is released.
7
- # https://github.com/sickill/rainbow/pull/53
8
- def self.faint_color(str)
9
- presenter = Rainbow(str)
10
- return presenter unless Rainbow.enabled
11
- Rainbow::Presenter.new(Rainbow::StringUtils.wrap_with_sgr(presenter, [2]))
12
- end
13
- end
14
- end
15
- end