i18n-tasks 0.9.37 → 1.0.13

Sign up to get free protection for your applications and to get access to all the features.
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