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.
- checksums.yaml +4 -4
- data/README.md +43 -5
- data/Rakefile +1 -1
- data/config/locales/en.yml +12 -3
- data/config/locales/ru.yml +8 -0
- data/i18n-tasks.gemspec +14 -7
- data/lib/i18n/tasks/cli.rb +8 -7
- data/lib/i18n/tasks/command/commander.rb +1 -0
- data/lib/i18n/tasks/command/commands/missing.rb +17 -5
- data/lib/i18n/tasks/command/options/common.rb +0 -1
- data/lib/i18n/tasks/command/options/data.rb +1 -1
- data/lib/i18n/tasks/concurrent/cached_value.rb +0 -2
- data/lib/i18n/tasks/configuration.rb +13 -7
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +15 -2
- data/lib/i18n/tasks/data/file_formats.rb +1 -1
- data/lib/i18n/tasks/data/router/pattern_router.rb +1 -1
- data/lib/i18n/tasks/data/tree/node.rb +1 -1
- data/lib/i18n/tasks/data/tree/nodes.rb +5 -7
- data/lib/i18n/tasks/data/tree/siblings.rb +1 -2
- data/lib/i18n/tasks/data/tree/traversal.rb +25 -11
- data/lib/i18n/tasks/html_keys.rb +2 -2
- data/lib/i18n/tasks/interpolations.rb +7 -3
- data/lib/i18n/tasks/key_pattern_matching.rb +2 -0
- data/lib/i18n/tasks/locale_pathname.rb +1 -1
- data/lib/i18n/tasks/plural_keys.rb +0 -6
- data/lib/i18n/tasks/references.rb +3 -3
- data/lib/i18n/tasks/reports/base.rb +2 -2
- data/lib/i18n/tasks/reports/terminal.rb +3 -3
- data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +118 -0
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +91 -0
- data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +69 -0
- data/lib/i18n/tasks/scanners/erb_ast_processor.rb +74 -0
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +85 -0
- data/lib/i18n/tasks/scanners/pattern_mapper.rb +1 -1
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -2
- data/lib/i18n/tasks/scanners/relative_keys.rb +2 -2
- data/lib/i18n/tasks/scanners/results/occurrence.rb +17 -1
- data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +9 -34
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +91 -156
- data/lib/i18n/tasks/scanners/ruby_key_literals.rb +2 -2
- data/lib/i18n/tasks/split_key.rb +1 -1
- data/lib/i18n/tasks/translation.rb +4 -1
- data/lib/i18n/tasks/translators/base_translator.rb +17 -12
- data/lib/i18n/tasks/translators/deepl_translator.rb +34 -11
- data/lib/i18n/tasks/translators/google_translator.rb +1 -1
- data/lib/i18n/tasks/translators/openai_translator.rb +100 -0
- data/lib/i18n/tasks/used_keys.rb +9 -6
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/i18n/tasks.rb +11 -0
- data/templates/config/i18n-tasks.yml +17 -4
- data/templates/minitest/i18n_test.rb +6 -6
- metadata +74 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12e19e4d7fe61bd20e000b0b6e4db5bdf233443e8e8cf965b62e634bdfa3e5a2
|
4
|
+
data.tar.gz: 3dab05338d079c4defad590466ab2985faf43245d616f86af4c212003831a6e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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/
|
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/
|
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
data/config/locales/en.yml
CHANGED
@@ -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}."
|
data/config/locales/ru.yml
CHANGED
@@ -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|
|
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 = {
|
28
|
-
|
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.
|
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.
|
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
|
|
data/lib/i18n/tasks/cli.rb
CHANGED
@@ -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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
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|
|
@@ -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
|
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
|
@@ -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] ||=
|
48
|
-
|
49
|
-
|
50
|
-
|
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.
|
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
|
@@ -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
|
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] ||=
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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|
|
data/lib/i18n/tasks/html_keys.rb
CHANGED
@@ -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
|
-
|
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(
|
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(
|
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*/, '|')})" }
|