i18n-tasks 0.9.37 → 1.0.13

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -5
  3. data/Rakefile +1 -1
  4. data/config/locales/en.yml +12 -3
  5. data/config/locales/ru.yml +8 -0
  6. data/i18n-tasks.gemspec +14 -7
  7. data/lib/i18n/tasks/cli.rb +8 -7
  8. data/lib/i18n/tasks/command/commander.rb +1 -0
  9. data/lib/i18n/tasks/command/commands/missing.rb +17 -5
  10. data/lib/i18n/tasks/command/options/common.rb +0 -1
  11. data/lib/i18n/tasks/command/options/data.rb +1 -1
  12. data/lib/i18n/tasks/concurrent/cached_value.rb +0 -2
  13. data/lib/i18n/tasks/configuration.rb +13 -7
  14. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +15 -2
  15. data/lib/i18n/tasks/data/file_formats.rb +1 -1
  16. data/lib/i18n/tasks/data/router/pattern_router.rb +1 -1
  17. data/lib/i18n/tasks/data/tree/node.rb +1 -1
  18. data/lib/i18n/tasks/data/tree/nodes.rb +5 -7
  19. data/lib/i18n/tasks/data/tree/siblings.rb +1 -2
  20. data/lib/i18n/tasks/data/tree/traversal.rb +25 -11
  21. data/lib/i18n/tasks/html_keys.rb +2 -2
  22. data/lib/i18n/tasks/interpolations.rb +7 -3
  23. data/lib/i18n/tasks/key_pattern_matching.rb +2 -0
  24. data/lib/i18n/tasks/locale_pathname.rb +1 -1
  25. data/lib/i18n/tasks/plural_keys.rb +0 -6
  26. data/lib/i18n/tasks/references.rb +3 -3
  27. data/lib/i18n/tasks/reports/base.rb +2 -2
  28. data/lib/i18n/tasks/reports/terminal.rb +3 -3
  29. data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +118 -0
  30. data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +91 -0
  31. data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +69 -0
  32. data/lib/i18n/tasks/scanners/erb_ast_processor.rb +74 -0
  33. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
  34. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +85 -0
  35. data/lib/i18n/tasks/scanners/pattern_mapper.rb +1 -1
  36. data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -2
  37. data/lib/i18n/tasks/scanners/relative_keys.rb +2 -2
  38. data/lib/i18n/tasks/scanners/results/occurrence.rb +17 -1
  39. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +9 -34
  40. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +91 -156
  41. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +2 -2
  42. data/lib/i18n/tasks/split_key.rb +1 -1
  43. data/lib/i18n/tasks/translation.rb +4 -1
  44. data/lib/i18n/tasks/translators/base_translator.rb +17 -12
  45. data/lib/i18n/tasks/translators/deepl_translator.rb +34 -11
  46. data/lib/i18n/tasks/translators/google_translator.rb +1 -1
  47. data/lib/i18n/tasks/translators/openai_translator.rb +100 -0
  48. data/lib/i18n/tasks/used_keys.rb +9 -6
  49. data/lib/i18n/tasks/version.rb +1 -1
  50. data/lib/i18n/tasks.rb +11 -0
  51. data/templates/config/i18n-tasks.yml +17 -4
  52. data/templates/minitest/i18n_test.rb +6 -6
  53. metadata +74 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cad835aebcfc5621cb07d3a5ef4134a01cfed6286411776a9a5e04cfefe5862
4
- data.tar.gz: 3b9f5a35077463778d1b940bd01738e49225fe7775d02858da5bdd9a9ded8308
3
+ metadata.gz: 12e19e4d7fe61bd20e000b0b6e4db5bdf233443e8e8cf965b62e634bdfa3e5a2
4
+ data.tar.gz: 3dab05338d079c4defad590466ab2985faf43245d616f86af4c212003831a6e4
5
5
  SHA512:
6
- metadata.gz: 8ed7363e9cfd2e8cf179aa0f28c0f00b926b4a53ba95f19ca61d8a08901322c561e84ebab51616f592400dac985f32c82b0a08911ec5f22223fe22e7858dd6af
7
- data.tar.gz: 04b44a9180dc9b1e9d739eb5e5881877f68321bbd7efa1028a2f2d89fd3acf1cf742d587aab566a8cbd43813619693132ec803221d6c7ad31cd703ba516622f3
6
+ metadata.gz: 2cf99aec1c6d5738c4a71964a7ce67e70c8ade3150042c4e381fef19b9eaa82cf6638979bf97c6e9941f119304973f9e6f3fd31b3bab0fd635520344448b5587
7
+ data.tar.gz: 2de5ab47951585b298e43bece202f3ad885ae8eb03edb2b6e860b30e3172ea213947509b9543bbf31df06ef03079ff1e7cfd1b7a32cd964428d9f9612fd7e171
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # i18n-tasks [![Build Status][badge-ci]][ci] [![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
+ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua/)
4
+
3
5
  i18n-tasks helps you find and manage missing and unused translations.
4
6
 
5
7
  <img width="539" height="331" src="https://i.imgur.com/XZBd8l7.png">
@@ -22,7 +24,7 @@ i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (def
22
24
  Add i18n-tasks to the Gemfile:
23
25
 
24
26
  ```ruby
25
- gem 'i18n-tasks', '~> 0.9.37'
27
+ gem 'i18n-tasks', '~> 1.0.13'
26
28
  ```
27
29
 
28
30
  Copy the default [configuration file](#configuration):
@@ -115,6 +117,17 @@ $ i18n-tasks translate-missing --backend=yandex
115
117
  $ i18n-tasks translate-missing --from=en es fr
116
118
  ```
117
119
 
120
+ ### OpenAI Translate missing keys
121
+
122
+ Translate missing values with OpenAI ([more below on the API key](#openai-translation-config)).
123
+
124
+ ```console
125
+ $ i18n-tasks translate-missing --backend=openai
126
+
127
+ # accepts from and locales options:
128
+ $ i18n-tasks translate-missing --from=en es fr
129
+ ```
130
+
118
131
  ### Find usages
119
132
 
120
133
  See where the keys are used with `i18n-tasks find`:
@@ -221,7 +234,7 @@ See the full list of tasks with `i18n-tasks --help`.
221
234
 
222
235
  ### Features and limitations
223
236
 
224
- `i18n-tasks` uses an AST scanner for `.rb` files, and a regexp-based scanner for other files, such as `.haml`.
237
+ `i18n-tasks` uses an AST scanner for `.rb` and `.html.erb` files, and a regexp-based scanner for other files, such as `.haml`.
225
238
 
226
239
  #### Relative keys
227
240
 
@@ -333,6 +346,7 @@ A special syntax similar to file glob patterns is used throughout i18n-tasks to
333
346
  |:------------:|:----------------------------------------------------------|
334
347
  | `*` | matches everything |
335
348
  | `:` | matches a single key |
349
+ | `*:` | matches part of a single key |
336
350
  | `{a, b.c}` | match any in set, can use `:` and `*`, match is captured |
337
351
 
338
352
  Example of usage:
@@ -353,7 +367,7 @@ If you have implemented a custom adapter please share it on [the wiki][wiki].
353
367
 
354
368
  ### Usage search
355
369
 
356
- i18n-tasks uses an AST scanner for `.rb` files, and a regexp scanner for all other files.
370
+ i18n-tasks uses an AST scanner for `.rb` and `.html.erb` files, and a regexp scanner for all other files.
357
371
  New scanners can be added easily: please refer to [this example](https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example).
358
372
 
359
373
  See the `search` section in the [config file][config] for all available configuration options.
@@ -421,6 +435,17 @@ translation:
421
435
  yandex_api_key: <Yandex API key>
422
436
  ```
423
437
 
438
+ <a name="openai-translation-config"></a>
439
+ ### OpenAI Translate
440
+
441
+ `i18n-tasks translate-missing` requires a OpenAI API key, get it at [OpenAI](https://openai.com/).
442
+
443
+ ```yaml
444
+ # config/i18n-tasks.yml
445
+ translation:
446
+ openai_api_key: <OpenAI API key>
447
+ ```
448
+
424
449
  ## Interactive console
425
450
 
426
451
  `i18n-tasks irb` starts an IRB session in i18n-tasks context. Type `guide` for more information.
@@ -434,15 +459,28 @@ See [i18n-tasks wiki: CSV import and export tasks](https://github.com/glebm/i18n
434
459
  Tasks that come with the gem are defined in [lib/i18n/tasks/command/commands](lib/i18n/tasks/command/commands).
435
460
  Custom tasks can be added easily, see the examples [on the wiki](https://github.com/glebm/i18n-tasks/wiki#custom-tasks).
436
461
 
462
+ # Development
463
+
464
+ - Install dependencies using `bundle install`
465
+ - Run tests using `bundle exec rspec`
466
+ - Install [Overcommit](https://github.com/sds/overcommit) by running `overcommit --install`
467
+
468
+ ## Skip Overcommit-hooks
469
+
470
+ - `SKIP=RuboCop git commit`
471
+ - `OVERCOMMIT_DISABLE=1 git commit`
472
+
473
+
437
474
  [MIT license]: /LICENSE.txt
438
475
  [ci]: https://github.com/glebm/i18n-tasks/actions/workflows/tests.yml
439
476
  [badge-ci]: https://github.com/glebm/i18n-tasks/actions/workflows/tests.yml/badge.svg
440
477
  [coverage]: https://codeclimate.com/github/glebm/i18n-tasks
441
478
  [badge-coverage]: https://api.codeclimate.com/v1/badges/5d173e90ada8df07cedc/test_coverage
442
- [config]: https://github.com/glebm/i18n-tasks/blob/master/templates/config/i18n-tasks.yml
479
+ [config]: https://github.com/glebm/i18n-tasks/blob/main/templates/config/i18n-tasks.yml
443
480
  [wiki]: https://github.com/glebm/i18n-tasks/wiki "i18n-tasks wiki"
444
481
  [i18n-gem]: https://github.com/svenfuchs/i18n "svenfuchs/i18n on Github"
445
482
  [screenshot-i18n-tasks]: https://i.imgur.com/XZBd8l7.png "i18n-tasks screenshot"
446
483
  [screenshot-find]: https://i.imgur.com/VxBrSfY.png "i18n-tasks find output screenshot"
447
- [adapter-example]: https://github.com/glebm/i18n-tasks/blob/master/lib/i18n/tasks/data/file_system_base.rb
484
+ [adapter-example]: https://github.com/glebm/i18n-tasks/blob/main/lib/i18n/tasks/data/file_system_base.rb
448
485
  [custom-scanner-docs]: https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example
486
+ [overcommit]: https://github.com/sds/overcommit#installation
data/Rakefile CHANGED
@@ -9,5 +9,5 @@ task :irb do
9
9
  # $: << File.expand_path('lib', __FILE__)
10
10
  require 'i18n/tasks'
11
11
  require 'i18n/tasks/commands'
12
- ::I18n::Tasks::Commands.new(::I18n::Tasks::BaseTask.new).irb
12
+ I18n::Tasks::Commands.new(I18n::Tasks::BaseTask.new).irb
13
13
  end
@@ -33,7 +33,7 @@ en:
33
33
  Value. Interpolates: %{value}, %{human_key}, %{key}, %{default}, %{value_or_human_key},
34
34
  %{value_or_default_or_human_key}
35
35
  desc:
36
- add_missing: add missing keys to locale data
36
+ add_missing: add missing keys to locale data, optionally match a pattern
37
37
  check_consistent_interpolations: verify that all translations use correct interpolation variables
38
38
  check_normalized: verify that all translation data is normalized
39
39
  config: display i18n-tasks configuration
@@ -46,12 +46,13 @@ en:
46
46
  gem_path: show path to the gem
47
47
  health: is everything OK?
48
48
  irb: start REPL session within i18n-tasks context
49
- missing: show missing translations
49
+ missing: show missing translations, optionally match a pattern
50
50
  mv: rename/merge the keys in locale data that match the given pattern
51
51
  normalize: 'normalize translation data: sort and move to the right files'
52
52
  remove_unused: remove unused keys
53
53
  rm: remove the keys in locale data that match the given pattern
54
- translate_missing: translate missing keys with Google Translate or DeepL Pro
54
+ translate_missing: translate missing keys with Google Translate or DeepL Pro, optionally match
55
+ a pattern
55
56
  tree_convert: convert tree between formats
56
57
  tree_filter: filter tree by key pattern
57
58
  tree_merge: merge trees
@@ -95,6 +96,8 @@ en:
95
96
  Setup DeepL Pro API key via DEEPL_AUTH_KEY environment variable or translation.deepl_api_key
96
97
  in config/i18n-tasks.yml. Get the key at https://www.deepl.com/pro.
97
98
  no_results: DeepL returned no results.
99
+ specific_target_missing: You must supply a specific variant for the given target language
100
+ e.g. en-us instead of en.
98
101
  google_translate:
99
102
  errors:
100
103
  no_api_key: >-
@@ -109,6 +112,12 @@ en:
109
112
  missing:
110
113
  details_title: Value in other locales or source
111
114
  none: No translations are missing.
115
+ openai_translate:
116
+ errors:
117
+ no_api_key: >-
118
+ Set OpenAI API key via OPENAI_API_KEY environment variable or translation.openai_api_key
119
+ in config/i18n-tasks.yml. Get the key at https://openai.com/.
120
+ no_results: OpenAI returned no results.
112
121
  remove_unused:
113
122
  confirm:
114
123
  one: "%{count} translation will be removed from %{locales}."
@@ -94,6 +94,8 @@ ru:
94
94
  Задайте ключ API DeepL через переменную окружения DEEPL_AUTH_KEY или translation.deepl_api_key
95
95
  Получите ключ через https://www.deepl.com/pro.
96
96
  no_results: DeepL не дал результатов.
97
+ specific_target_missing: You must supply a specific variant for the given target language
98
+ e.g. en-us instead of en.
97
99
  google_translate:
98
100
  errors:
99
101
  no_api_key: >-
@@ -109,6 +111,12 @@ ru:
109
111
  missing:
110
112
  details_title: На других языках или в коде
111
113
  none: Всё переведено.
114
+ openai_translate:
115
+ errors:
116
+ no_api_key: |-
117
+ Установить ключ API Яндекса с помощью переменной среды OPENAI_API_KEY или translation.openai_api_key
118
+ в config / i18n-tasks.yml. Получите ключ по адресу https://openai.com/.
119
+ no_results: Яндекс не дал результатов.
112
120
  remove_unused:
113
121
  confirm:
114
122
  few: Переводы (%{count}) будут удалены из %{locales}.
data/i18n-tasks.gemspec CHANGED
@@ -4,7 +4,7 @@ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'i18n/tasks/version'
6
6
 
7
- Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
7
+ Gem::Specification.new do |s|
8
8
  s.name = 'i18n-tasks'
9
9
  s.version = I18n::Tasks::VERSION
10
10
  s.authors = ['glebm']
@@ -22,31 +22,38 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
22
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
24
  cp $(bundle exec i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/
25
+ # Or for minitest:
26
+ cp $(bundle exec i18n-tasks gem-path)/templates/minitest/i18n_test.rb test/
25
27
  TEXT
26
28
  s.homepage = 'https://github.com/glebm/i18n-tasks'
27
- s.metadata = { 'issue_tracker' => 'https://github.com/glebm/i18n-tasks' } if s.respond_to?(:metadata=)
28
- s.required_ruby_version = '>= 2.6', '< 4.0' if s.respond_to?(:required_ruby_version=)
29
+ s.metadata = {
30
+ 'issue_tracker' => 'https://github.com/glebm/i18n-tasks',
31
+ 'rubygems_mfa_required' => 'true'
32
+ }
33
+ s.required_ruby_version = '>= 2.6', '< 4.0'
29
34
 
30
35
  s.files = `git ls-files`.split($/)
31
36
  s.files -= s.files.grep(%r{^(doc/|\.|spec/)}) + %w[CHANGES.md config/i18n-tasks.yml Gemfile]
32
37
  s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } - %w[i18n-tasks.cmd]
33
- s.test_files = s.files.grep(%r{^(test|spec|features)/})
34
38
  s.require_paths = ['lib']
35
39
 
36
40
  s.add_dependency 'activesupport', '>= 4.0.2'
37
41
  s.add_dependency 'ast', '>= 2.1.0'
42
+ s.add_dependency 'better_html', '>= 1.0', '< 3.0'
38
43
  s.add_dependency 'erubi'
39
44
  s.add_dependency 'highline', '>= 2.0.0'
40
45
  s.add_dependency 'i18n'
41
- s.add_dependency 'parser', '>= 2.2.3.0'
46
+ s.add_dependency 'parser', '>= 3.2.2.1'
42
47
  s.add_dependency 'rails-i18n'
43
48
  s.add_dependency 'rainbow', '>= 2.2.2', '< 4.0'
44
49
  s.add_dependency 'terminal-table', '>= 1.5.1'
45
- s.add_development_dependency 'axlsx', '~> 2.0'
46
50
  s.add_development_dependency 'bundler', '~> 2.0', '>= 2.0.1'
51
+ s.add_development_dependency 'overcommit', '~> 0.58.0'
47
52
  s.add_development_dependency 'rake'
48
53
  s.add_development_dependency 'rspec', '~> 3.3'
49
- s.add_development_dependency 'rubocop', '~> 1.6.1'
54
+ s.add_development_dependency 'rubocop', '~> 1.50.1'
55
+ s.add_development_dependency 'rubocop-rake', '~> 0.6.0'
56
+ s.add_development_dependency 'rubocop-rspec', '~> 2.19.0'
50
57
  s.add_development_dependency 'simplecov'
51
58
  s.add_development_dependency 'yard'
52
59
 
@@ -35,13 +35,14 @@ class I18n::Tasks::CLI
35
35
 
36
36
  def run(argv)
37
37
  argv.each_with_index do |arg, i|
38
- if ["--config", "-c"].include?(arg)
39
- if File.exist?(argv[i+1])
40
- @config_file = argv[i+1]
41
- break
42
- else
43
- error "Config file doesn't exist: #{argv[i+1]}", 128
44
- end
38
+ next unless ['--config', '-c'].include?(arg)
39
+
40
+ _, config_file = argv.slice!(i, 2)
41
+ if File.exist?(config_file)
42
+ @config_file = config_file
43
+ break
44
+ else
45
+ error "Config file doesn't exist: #{config_file}", 128
45
46
  end
46
47
  end
47
48
 
@@ -16,6 +16,7 @@ module I18n::Tasks
16
16
  end
17
17
 
18
18
  def run(name, opts = {})
19
+ log_stderr "#{Rainbow('#StandWith').bg(:blue)}#{Rainbow('Ukraine').bg(:yellow)}"
19
20
  name = name.to_sym
20
21
  public_name = name.to_s.tr '_', '-'
21
22
  log_verbose "task: #{public_name}(#{opts.map { |k, v| "#{k}: #{v.inspect}" } * ', '})"
@@ -25,10 +25,14 @@ module I18n::Tasks
25
25
  cmd :missing,
26
26
  pos: '[locale ...]',
27
27
  desc: t('i18n_tasks.cmd.desc.missing'),
28
- args: %i[locales out_format missing_types]
28
+ args: %i[locales out_format missing_types pattern]
29
29
 
30
30
  def missing(opt = {})
31
31
  forest = i18n.missing_keys(**opt.slice(:locales, :base_locale, :types))
32
+ if opt[:pattern]
33
+ pattern_re = i18n.compile_key_pattern(opt[:pattern])
34
+ forest.select_keys! { |full_key, _node| full_key =~ pattern_re }
35
+ end
32
36
  print_forest forest, opt, :missing_keys
33
37
  :exit1 unless forest.empty?
34
38
  end
@@ -36,10 +40,14 @@ module I18n::Tasks
36
40
  cmd :translate_missing,
37
41
  pos: '[locale ...]',
38
42
  desc: t('i18n_tasks.cmd.desc.translate_missing'),
39
- args: [:locales, :locale_to_translate_from, arg(:out_format).from(1), :translation_backend]
43
+ args: [:locales, :locale_to_translate_from, arg(:out_format).from(1), :translation_backend, :pattern]
40
44
 
41
45
  def translate_missing(opt = {})
42
- missing = i18n.missing_diff_forest opt[:locales], opt[:from]
46
+ missing = i18n.missing_diff_forest opt[:locales], opt[:from]
47
+ if opt[:pattern]
48
+ pattern_re = i18n.compile_key_pattern(opt[:pattern])
49
+ missing.select_keys! { |full_key, _node| full_key =~ pattern_re }
50
+ end
43
51
  translated = i18n.translate_forest missing, from: opt[:from], backend: opt[:backend].to_sym
44
52
  i18n.data.merge! translated
45
53
  log_stderr t('i18n_tasks.translate_missing.translated', count: translated.leaves.count)
@@ -49,17 +57,21 @@ module I18n::Tasks
49
57
  cmd :add_missing,
50
58
  pos: '[locale ...]',
51
59
  desc: t('i18n_tasks.cmd.desc.add_missing'),
52
- args: [:locales, :out_format, arg(:value) + [{ default: '%{value_or_default_or_human_key}' }],
60
+ args: [:locales, :out_format, :pattern, arg(:value) + [{ default: '%{value_or_default_or_human_key}' }],
53
61
  ['--nil-value', 'Set value to nil. Takes precedence over the value argument.']]
54
62
 
55
63
  # Merge base locale first, as this may affect the value for the other locales
56
- def add_missing(opt = {})
64
+ def add_missing(opt = {}) # rubocop:disable Metrics/AbcSize
57
65
  [
58
66
  [i18n.base_locale] & opt[:locales],
59
67
  opt[:locales] - [i18n.base_locale]
60
68
  ].reject(&:empty?).each_with_object(i18n.empty_forest) do |locales, added|
61
69
  forest = i18n.missing_keys(locales: locales, **opt.slice(:types, :base_locale))
62
70
  .set_each_value!(opt[:'nil-value'] ? nil : opt[:value])
71
+ if opt[:pattern]
72
+ pattern_re = i18n.compile_key_pattern(opt[:pattern])
73
+ forest.select_keys! { |full_key, _node| full_key =~ pattern_re }
74
+ end
63
75
  i18n.data.merge! forest
64
76
  added.merge! forest
65
77
  end.tap do |added|
@@ -33,7 +33,6 @@ module I18n::Tasks
33
33
  '--config FILE',
34
34
  t('i18n_tasks.cmd.args.desc.config')
35
35
 
36
-
37
36
  def arg_or_pos!(key, opts)
38
37
  opts[key] ||= opts[:arguments].try(:shift)
39
38
  end
@@ -86,7 +86,7 @@ module I18n::Tasks
86
86
  when 'keys'
87
87
  puts forest.key_names(root: true)
88
88
  when 'key-values'
89
- puts forest.key_values(root: true).map { |kv| kv.join("\t") }
89
+ puts(forest.key_values(root: true).map { |kv| kv.join("\t") })
90
90
  when *DATA_FORMATS
91
91
  puts i18n.data.adapter_dump forest.to_hash(true), format
92
92
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/concurrent/cached_value'
4
-
5
3
  module I18n::Tasks::Concurrent
6
4
  # A thread-safe memoized value.
7
5
  # The given computation is guaranteed to be invoked at most once.
@@ -30,6 +30,13 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
30
30
  warn_deprecated 'Please move relative_roots under search in config/i18n-tasks.yml.'
31
31
  c[:search][:relative_roots] = c.delete(:relative_roots)
32
32
  end
33
+
34
+ if c.dig(:search, :exclude_method_name_paths)
35
+ warn_deprecated(
36
+ 'Please rename exclude_method_name_paths to relative_exclude_method_name_paths in config/i18n-tasks.yml.'
37
+ )
38
+ c[:search][:relative_exclude_method_name_paths] = c[:search].delete(:exclude_method_name_paths)
39
+ end
33
40
  end
34
41
  else
35
42
  {}.with_indifferent_access
@@ -44,12 +51,10 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
44
51
  # data config
45
52
  # @return [Hash<adapter: String, options: Hash>]
46
53
  def data_config
47
- @config_sections[:data] ||= begin
48
- {
49
- adapter: data.class.name,
50
- config: data.config
51
- }
52
- end
54
+ @config_sections[:data] ||= {
55
+ adapter: data.class.name,
56
+ config: data.config
57
+ }
53
58
  end
54
59
 
55
60
  # translation config
@@ -61,6 +66,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
61
66
  conf[:deepl_api_key] = ENV['DEEPL_AUTH_KEY'] if ENV.key?('DEEPL_AUTH_KEY')
62
67
  conf[:deepl_host] = ENV['DEEPL_HOST'] if ENV.key?('DEEPL_HOST')
63
68
  conf[:deepl_version] = ENV['DEEPL_VERSION'] if ENV.key?('DEEPL_VERSION')
69
+ conf[:openai_api_key] = ENV['OPENAI_API_KEY'] if ENV.key?('OPENAI_API_KEY')
64
70
  conf[:yandex_api_key] = ENV['YANDEX_API_KEY'] if ENV.key?('YANDEX_API_KEY')
65
71
  conf
66
72
  end
@@ -82,7 +88,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
82
88
  valid_locales = Dir[File.join(I18n::Tasks.gem_path, 'config', 'locales', '*.yml')]
83
89
  .map { |f| File.basename(f, '.yml') }
84
90
  unless valid_locales.include?(internal_locale)
85
- log_warn "invalid internal_locale #{internal_locale.inspect}. "\
91
+ log_warn "invalid internal_locale #{internal_locale.inspect}. " \
86
92
  "Available internal locales: #{valid_locales * ', '}."
87
93
  internal_locale = DEFAULTS[:internal_locale].to_s
88
94
  end
@@ -5,11 +5,14 @@ module I18n::Tasks
5
5
  module Data
6
6
  module Adapter
7
7
  module YamlAdapter
8
+ EMOJI_REGEX = /\\u[\da-f]{8}/i.freeze
9
+ TRAILING_SPACE_REGEX = / $/.freeze
10
+
8
11
  class << self
9
12
  # @return [Hash] locale tree
10
13
  def parse(str, options)
11
14
  if YAML.method(:load).arity.abs == 2
12
- YAML.load(str, **(options || {}))
15
+ YAML.safe_load(str, **(options || {}), permitted_classes: [Symbol], aliases: true)
13
16
  else
14
17
  # older jruby and rbx 2.2.7 do not accept options
15
18
  YAML.load(str)
@@ -18,7 +21,17 @@ module I18n::Tasks
18
21
 
19
22
  # @return [String]
20
23
  def dump(tree, options)
21
- tree.to_yaml(options || {})
24
+ strip_trailing_spaces(restore_emojis(tree.to_yaml(options || {})))
25
+ end
26
+
27
+ # @return [String]
28
+ def restore_emojis(yaml)
29
+ yaml.gsub(EMOJI_REGEX) { |m| [m[-8..].to_i(16)].pack('U') }
30
+ end
31
+
32
+ # @return [String]
33
+ def strip_trailing_spaces(yaml)
34
+ yaml.gsub(TRAILING_SPACE_REGEX, '')
22
35
  end
23
36
  end
24
37
  end
@@ -55,7 +55,7 @@ module I18n
55
55
  return if File.file?(path) && content == read_file(path)
56
56
 
57
57
  ::FileUtils.mkpath(File.dirname(path))
58
- ::File.open(path, 'w') { |f| f.write content }
58
+ ::File.write(path, content)
59
59
  end
60
60
 
61
61
  def normalized?(path, tree)
@@ -38,7 +38,7 @@ module I18n::Tasks
38
38
  if pattern
39
39
  key_match = $~
40
40
  path = format(path, locale: locale)
41
- path.gsub!(/\\\d+/) { |m| key_match[m[1..-1].to_i] }
41
+ path.gsub!(/\\\d+/) { |m| key_match[m[1..].to_i] }
42
42
  (out[path] ||= Set.new) << "#{locale}.#{key}"
43
43
  else
44
44
  fail CommandError, "Cannot route key #{key}. Routes are #{@routes_config.inspect}"
@@ -168,7 +168,7 @@ module I18n::Tasks::Data::Tree
168
168
  label = if key.nil?
169
169
  Rainbow('∅').faint
170
170
  else
171
- [Rainbow(key).color(1 + level % 15),
171
+ [Rainbow(key).color(1 + (level % 15)),
172
172
  (": #{format_value_for_inspect(value)}" if leaf?),
173
173
  (" #{data}" if data?)].compact.join
174
174
  end
@@ -31,13 +31,11 @@ module I18n::Tasks::Data::Tree
31
31
  end
32
32
 
33
33
  def to_hash(sort = false)
34
- (@hash ||= {})[sort] ||= begin
35
- if sort
36
- sort_by(&:key)
37
- else
38
- self
39
- end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!)
40
- end
34
+ (@hash ||= {})[sort] ||= if sort
35
+ sort_by(&:key)
36
+ else
37
+ self
38
+ end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!)
41
39
  end
42
40
 
43
41
  delegate :to_json, to: :to_hash
@@ -3,7 +3,6 @@
3
3
  require 'set'
4
4
  require 'i18n/tasks/split_key'
5
5
  require 'i18n/tasks/data/tree/nodes'
6
- require 'i18n/tasks/data/tree/node'
7
6
 
8
7
  module I18n::Tasks::Data::Tree
9
8
  # Siblings represents a subtree sharing a common parent
@@ -50,7 +49,7 @@ module I18n::Tasks::Data::Tree
50
49
  next
51
50
  end
52
51
  match = $~
53
- new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..-1].to_i] }
52
+ new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..].to_i] }
54
53
  old_key_to_new_key[full_key] = new_key
55
54
  moved_forest.merge!(Siblings.new.tap do |forest|
56
55
  forest[[(node.root.try(:key) unless root), new_key].compact.join('.')] =
@@ -161,12 +161,12 @@ module I18n::Tasks
161
161
  end
162
162
 
163
163
  def grep_keys(match, opts = {})
164
- select_keys(opts) do |full_key, _node|
164
+ select_keys(**opts) do |full_key, _node|
165
165
  match === full_key # rubocop:disable Style/CaseEquality
166
166
  end
167
167
  end
168
168
 
169
- def set_each_value!(val_pattern, key_pattern = nil, &value_proc)
169
+ def set_each_value!(val_pattern, key_pattern = nil, &value_proc) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
170
170
  value_proc ||= proc do |node|
171
171
  node_value = node.value
172
172
  next node_value if node.reference?
@@ -174,15 +174,29 @@ module I18n::Tasks
174
174
  human_key = ActiveSupport::Inflector.humanize(node.key.to_s)
175
175
  full_key = node.full_key
176
176
  default = (node.data[:occurrences] || []).detect { |o| o.default_arg.presence }.try(:default_arg)
177
- StringInterpolation.interpolate_soft(
178
- val_pattern,
179
- value: node_value,
180
- human_key: human_key,
181
- key: full_key,
182
- default: default,
183
- value_or_human_key: node_value.presence || human_key,
184
- value_or_default_or_human_key: node_value.presence || default || human_key
185
- )
177
+ if default.is_a?(Hash)
178
+ default.each_with_object({}) do |(k, v), h|
179
+ h[k] = StringInterpolation.interpolate_soft(
180
+ val_pattern,
181
+ value: node_value,
182
+ human_key: human_key,
183
+ key: full_key,
184
+ default: v,
185
+ value_or_human_key: node_value.presence || human_key,
186
+ value_or_default_or_human_key: node_value.presence || v || human_key
187
+ )
188
+ end
189
+ else
190
+ StringInterpolation.interpolate_soft(
191
+ val_pattern,
192
+ value: node_value,
193
+ human_key: human_key,
194
+ key: full_key,
195
+ default: default,
196
+ value_or_human_key: node_value.presence || human_key,
197
+ value_or_default_or_human_key: node_value.presence || default || human_key
198
+ )
199
+ end
186
200
  end
187
201
  pattern_re = I18n::Tasks::KeyPatternMatching.compile_key_pattern(key_pattern) if key_pattern.present?
188
202
  keys.each do |key, node|
@@ -7,8 +7,8 @@ module I18n::Tasks
7
7
 
8
8
  def html_key?(full_key, locale)
9
9
  !!(full_key =~ HTML_KEY_PATTERN ||
10
- full_key =~ MAYBE_PLURAL_HTML_KEY_PATTERN &&
11
- depluralize_key(split_key(full_key, 2)[1], locale) =~ HTML_KEY_PATTERN)
10
+ (full_key =~ MAYBE_PLURAL_HTML_KEY_PATTERN &&
11
+ depluralize_key(split_key(full_key, 2)[1], locale) =~ HTML_KEY_PATTERN))
12
12
  end
13
13
  end
14
14
  end
@@ -2,22 +2,26 @@
2
2
 
3
3
  module I18n::Tasks
4
4
  module Interpolations
5
- VARIABLE_REGEX = /%{[^}]+}/.freeze
5
+ class << self
6
+ attr_accessor :variable_regex
7
+ end
8
+ @variable_regex = /%{[^}]+}/.freeze
6
9
 
7
10
  def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disable Metrics/AbcSize
8
11
  locales ||= self.locales
9
12
  base_locale ||= self.base_locale
10
13
  result = empty_forest
14
+ variable_regex = I18n::Tasks::Interpolations.variable_regex
11
15
 
12
16
  data[base_locale].key_values.each do |key, value|
13
17
  next if !value.is_a?(String) || ignore_key?(key, :inconsistent_interpolations)
14
18
 
15
- base_vars = Set.new(value.scan(VARIABLE_REGEX))
19
+ base_vars = Set.new(value.scan(variable_regex))
16
20
  (locales - [base_locale]).each do |current_locale|
17
21
  node = data[current_locale].first.children[key]
18
22
  next unless node&.value.is_a?(String)
19
23
 
20
- if base_vars != Set.new(node.value.scan(VARIABLE_REGEX))
24
+ if base_vars != Set.new(node.value.scan(variable_regex))
21
25
  result.merge!(node.walk_to_root.reduce(nil) { |c, p| [p.derive(children: c)] })
22
26
  end
23
27
  end
@@ -21,6 +21,7 @@ module I18n::Tasks::KeyPatternMatching
21
21
  # In patterns:
22
22
  # * is like .* in regexs
23
23
  # : matches a single key
24
+ # *: matches part of a single key, equivalent to `[^.]+?` regex
24
25
  # { a, b.c } match any in set, can use : and *, match is captured
25
26
  def compile_key_pattern(key_pattern)
26
27
  return key_pattern if key_pattern.is_a?(Regexp)
@@ -31,6 +32,7 @@ module I18n::Tasks::KeyPatternMatching
31
32
  def key_pattern_re_body(key_pattern)
32
33
  key_pattern
33
34
  .gsub(/\./, '\.')
35
+ .gsub(/\*:/, '[^.]+?')
34
36
  .gsub(/\*/, '.*')
35
37
  .gsub(/:/, '(?<=^|\.)[^.]+?(?=\.|$)')
36
38
  .gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, '|')})" }
@@ -10,7 +10,7 @@ module I18n::Tasks
10
10
  private
11
11
 
12
12
  def path_locale_re(locale)
13
- (@path_locale_res ||= {})[locale] ||= %r{(?<=^|[/.])#{locale}(?=[/.])}
13
+ (@path_locale_res ||= {})[locale] ||= %r{(?<=^|[/.-])#{locale}(?=[/.])}
14
14
  end
15
15
  end
16
16
  end