i18n-tasks 1.0.12 → 1.0.14
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.
- checksums.yaml +4 -4
- data/README.md +85 -2
- data/Rakefile +1 -1
- data/config/locales/en.yml +11 -4
- data/config/locales/ru.yml +11 -4
- data/i18n-tasks.gemspec +5 -5
- data/lib/i18n/tasks/cli.rb +8 -8
- data/lib/i18n/tasks/command/commands/data.rb +14 -0
- data/lib/i18n/tasks/command/commands/missing.rb +2 -2
- data/lib/i18n/tasks/command/options/common.rb +1 -1
- data/lib/i18n/tasks/configuration.rb +3 -1
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +7 -1
- data/lib/i18n/tasks/data/file_system_base.rb +5 -0
- data/lib/i18n/tasks/data/router/isolating_router.rb +146 -0
- data/lib/i18n/tasks/data/tree/siblings.rb +2 -2
- data/lib/i18n/tasks/data/tree/traversal.rb +2 -2
- data/lib/i18n/tasks/interpolations.rb +1 -1
- data/lib/i18n/tasks/key_pattern_matching.rb +5 -3
- data/lib/i18n/tasks/references.rb +3 -3
- data/lib/i18n/tasks/reports/base.rb +1 -1
- data/lib/i18n/tasks/reports/terminal.rb +7 -1
- data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +26 -0
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +4 -4
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +55 -25
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +1 -1
- data/lib/i18n/tasks/scanners/relative_keys.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +3 -1
- data/lib/i18n/tasks/split_key.rb +30 -47
- data/lib/i18n/tasks/translation.rb +4 -1
- data/lib/i18n/tasks/translators/base_translator.rb +22 -11
- data/lib/i18n/tasks/translators/deepl_translator.rb +58 -9
- data/lib/i18n/tasks/translators/google_translator.rb +28 -13
- data/lib/i18n/tasks/translators/openai_translator.rb +118 -0
- data/lib/i18n/tasks/used_keys.rb +5 -5
- data/lib/i18n/tasks/version.rb +1 -1
- data/templates/config/i18n-tasks.yml +28 -3
- data/templates/minitest/i18n_test.rb +6 -6
- metadata +40 -30
- data/lib/i18n/tasks/scanners/erb_ast_processor.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd61ceb254dc1c44768bd40672b3f31635c9d42fb237950cbdfe64fef642a850
|
4
|
+
data.tar.gz: ae6981dd739636ddb5f1bae1ca9e5cf05ba991fd1345a5488e1cfa8561833877
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1312910139411cc4695bea0f0de8624d49958efac33c8c7b7bd99fb3f351d408d0a6980789ec81a43452bc29a15292f6de9719ace0e3b902866d63c02f5cab79
|
7
|
+
data.tar.gz: e782f1bc1f1ad68cc33583d014c879b3d121ba2e0990db8cc109f864aeb0e98f0a97030300294e31e9041899b131b2b677fe6917f705d09bda6bf07aed588b42
|
data/README.md
CHANGED
@@ -24,7 +24,7 @@ i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (def
|
|
24
24
|
Add i18n-tasks to the Gemfile:
|
25
25
|
|
26
26
|
```ruby
|
27
|
-
gem 'i18n-tasks', '~> 1.0.
|
27
|
+
gem 'i18n-tasks', '~> 1.0.14', group: :development
|
28
28
|
```
|
29
29
|
|
30
30
|
Copy the default [configuration file](#configuration):
|
@@ -117,6 +117,17 @@ $ i18n-tasks translate-missing --backend=yandex
|
|
117
117
|
$ i18n-tasks translate-missing --from=en es fr
|
118
118
|
```
|
119
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
|
+
|
120
131
|
### Find usages
|
121
132
|
|
122
133
|
See where the keys are used with `i18n-tasks find`:
|
@@ -327,6 +338,33 @@ data:
|
|
327
338
|
If you want to have i18n-tasks reorganize your existing keys using `data.write`, either set the router to
|
328
339
|
`pattern_router` as above, or run `i18n-tasks normalize -p` (forcing the use of the pattern router for that run).
|
329
340
|
|
341
|
+
##### Isolating router
|
342
|
+
|
343
|
+
Isolating router assumes each YAML file is independent and can contain similar keys.
|
344
|
+
|
345
|
+
As a result, the translations are written to an alternate target file for each source file
|
346
|
+
(only the `%{locale}` part is changed to match target locale). Thus, it is not necessary to
|
347
|
+
specify any `write` configuration (in fact, it would be completely ignored).
|
348
|
+
|
349
|
+
This can be useful for example when using [ViewComponent sidecars](https://viewcomponent.org/guide/translations.html)
|
350
|
+
(ViewComponent assigns an implicit scope to each sidecar YAML file but `i18n-tasks` is not aware of
|
351
|
+
that logic, resulting in collisions):
|
352
|
+
|
353
|
+
* `app/components/movies_component.en.yml`:
|
354
|
+
```yaml
|
355
|
+
en:
|
356
|
+
title: Movies
|
357
|
+
```
|
358
|
+
|
359
|
+
* `app/components/games_component.en.yml`
|
360
|
+
```yaml
|
361
|
+
en:
|
362
|
+
title: Games
|
363
|
+
```
|
364
|
+
|
365
|
+
This router has a limitation, though: it does not support detecting missing keys from code usage
|
366
|
+
(since it is not aware of the implicit scope logic).
|
367
|
+
|
330
368
|
##### Key pattern syntax
|
331
369
|
|
332
370
|
A special syntax similar to file glob patterns is used throughout i18n-tasks to match translation keys:
|
@@ -335,6 +373,7 @@ A special syntax similar to file glob patterns is used throughout i18n-tasks to
|
|
335
373
|
|:------------:|:----------------------------------------------------------|
|
336
374
|
| `*` | matches everything |
|
337
375
|
| `:` | matches a single key |
|
376
|
+
| `*:` | matches part of a single key |
|
338
377
|
| `{a, b.c}` | match any in set, can use `:` and `*`, match is captured |
|
339
378
|
|
340
379
|
Example of usage:
|
@@ -399,6 +438,12 @@ translation:
|
|
399
438
|
google_translate_api_key: <Google Translate API key>
|
400
439
|
```
|
401
440
|
|
441
|
+
or via environment variable:
|
442
|
+
|
443
|
+
```bash
|
444
|
+
GOOGLE_TRANSLATE_API_KEY=<Google Translate API key>
|
445
|
+
```
|
446
|
+
|
402
447
|
<a name="deepl-translation-config"></a>
|
403
448
|
### DeepL Pro Translate
|
404
449
|
|
@@ -410,6 +455,19 @@ translation:
|
|
410
455
|
deepl_api_key: <DeepL Pro API key>
|
411
456
|
deepl_host: <optional>
|
412
457
|
deepl_version: <optional>
|
458
|
+
deepl_glossary_ids:
|
459
|
+
- f28106eb-0e06-489e-82c6-8215d6f95089
|
460
|
+
- 2c6415be-1852-4f54-9e1b-d800463496b4
|
461
|
+
deepl_options:
|
462
|
+
formality: prefer_less
|
463
|
+
```
|
464
|
+
|
465
|
+
or via environment variables:
|
466
|
+
|
467
|
+
```bash
|
468
|
+
DEEPL_API_KEY=<DeepL Pro API key>
|
469
|
+
DEEPL_HOST=<optional>
|
470
|
+
DEEPL_VERSION=<optional>
|
413
471
|
```
|
414
472
|
|
415
473
|
<a name="yandex-translation-config"></a>
|
@@ -423,6 +481,31 @@ translation:
|
|
423
481
|
yandex_api_key: <Yandex API key>
|
424
482
|
```
|
425
483
|
|
484
|
+
or via environment variable:
|
485
|
+
|
486
|
+
```bash
|
487
|
+
YANDEX_API_KEY=<Yandex API key>
|
488
|
+
```
|
489
|
+
|
490
|
+
<a name="openai-translation-config"></a>
|
491
|
+
### OpenAI Translate
|
492
|
+
|
493
|
+
`i18n-tasks translate-missing` requires a OpenAI API key, get it at [OpenAI](https://openai.com/).
|
494
|
+
|
495
|
+
```yaml
|
496
|
+
# config/i18n-tasks.yml
|
497
|
+
translation:
|
498
|
+
openai_api_key: <OpenAI API key>
|
499
|
+
openai_model: <optional>
|
500
|
+
```
|
501
|
+
|
502
|
+
or via environment variable:
|
503
|
+
|
504
|
+
```bash
|
505
|
+
OPENAI_API_KEY=<OpenAI API key>
|
506
|
+
OPENAI_MODEL=<optional>
|
507
|
+
```
|
508
|
+
|
426
509
|
## Interactive console
|
427
510
|
|
428
511
|
`i18n-tasks irb` starts an IRB session in i18n-tasks context. Type `guide` for more information.
|
@@ -440,7 +523,7 @@ Custom tasks can be added easily, see the examples [on the wiki](https://github.
|
|
440
523
|
|
441
524
|
- Install dependencies using `bundle install`
|
442
525
|
- Run tests using `bundle exec rspec`
|
443
|
-
- Install [Overcommit](overcommit) by running `overcommit --install`
|
526
|
+
- Install [Overcommit](https://github.com/sds/overcommit) by running `overcommit --install`
|
444
527
|
|
445
528
|
## Skip Overcommit-hooks
|
446
529
|
|
data/Rakefile
CHANGED
data/config/locales/en.yml
CHANGED
@@ -16,12 +16,10 @@ en:
|
|
16
16
|
data_format: 'Data format: %{valid_text}.'
|
17
17
|
keep_order: Keep the order of the keys
|
18
18
|
key_pattern: Filter by key pattern (e.g. 'common.*')
|
19
|
-
key_pattern_to_rename: Full key (pattern) to rename. Required
|
20
19
|
locale: :i18n_tasks.common.locale
|
21
20
|
locale_to_translate_from: Locale to translate from
|
22
21
|
locales_filter: 'Locale(s) to process. Special: base'
|
23
22
|
missing_types: 'Filter by types: %{valid}'
|
24
|
-
new_key_name: New name, interpolates original name as %{key}. Required
|
25
23
|
nostdin: Do not read from stdin
|
26
24
|
out_format: 'Output format: %{valid_text}'
|
27
25
|
pattern_router: 'Use pattern router: keys moved per config data.write'
|
@@ -30,13 +28,14 @@ en:
|
|
30
28
|
the config setting if set.
|
31
29
|
translation_backend: Translation backend (google or deepl)
|
32
30
|
value: >-
|
33
|
-
Value. Interpolates:
|
34
|
-
|
31
|
+
Value. Interpolates: %%{value}, %%{human_key}, %%{key}, %%{default}, %%{value_or_human_key},
|
32
|
+
%%{value_or_default_or_human_key}
|
35
33
|
desc:
|
36
34
|
add_missing: add missing keys to locale data, optionally match a pattern
|
37
35
|
check_consistent_interpolations: verify that all translations use correct interpolation variables
|
38
36
|
check_normalized: verify that all translation data is normalized
|
39
37
|
config: display i18n-tasks configuration
|
38
|
+
cp: copy the keys in locale data that match the given pattern
|
40
39
|
data: show locale data
|
41
40
|
data_merge: merge locale data with trees
|
42
41
|
data_remove: remove keys present in tree from data
|
@@ -96,6 +95,8 @@ en:
|
|
96
95
|
Setup DeepL Pro API key via DEEPL_AUTH_KEY environment variable or translation.deepl_api_key
|
97
96
|
in config/i18n-tasks.yml. Get the key at https://www.deepl.com/pro.
|
98
97
|
no_results: DeepL returned no results.
|
98
|
+
specific_target_missing: You must supply a specific variant for the given target language
|
99
|
+
e.g. en-us instead of en.
|
99
100
|
google_translate:
|
100
101
|
errors:
|
101
102
|
no_api_key: >-
|
@@ -110,6 +111,12 @@ en:
|
|
110
111
|
missing:
|
111
112
|
details_title: Value in other locales or source
|
112
113
|
none: No translations are missing.
|
114
|
+
openai_translate:
|
115
|
+
errors:
|
116
|
+
no_api_key: >-
|
117
|
+
Set OpenAI API key via OPENAI_API_KEY environment variable or translation.openai_api_key
|
118
|
+
in config/i18n-tasks.yml. Get the key at https://openai.com/.
|
119
|
+
no_results: OpenAI returned no results.
|
113
120
|
remove_unused:
|
114
121
|
confirm:
|
115
122
|
one: "%{count} translation will be removed from %{locales}."
|
data/config/locales/ru.yml
CHANGED
@@ -13,28 +13,27 @@ ru:
|
|
13
13
|
data_format: 'Формат данных: %{valid_text}.'
|
14
14
|
keep_order: Keep the order of the keys
|
15
15
|
key_pattern: Маска ключа (например, common.*)
|
16
|
-
key_pattern_to_rename: Полный ключ (шаблон) для переименования. Необходимый параметр.
|
17
16
|
locale: 'Язык. По умолчанию: base'
|
18
17
|
locale_to_translate_from: 'Язык, с которого переводить (по умолчанию: base)'
|
19
18
|
locales_filter: >-
|
20
19
|
Список языков для обработки, разделенный запятыми (,). По умолчанию: все. Специальное
|
21
20
|
значение: base.
|
22
21
|
missing_types: 'Типы недостающих переводов: %{valid}. По умолчанию: все'
|
23
|
-
new_key_name: Новое имя, интерполирует оригинальное название как %{key}. Необходимый параметр.
|
24
22
|
nostdin: Не читать дерево из стандартного ввода
|
25
23
|
out_format: 'Формат вывода: %{valid_text}.'
|
26
24
|
pattern_router: 'Использовать pattern_router: ключи распределятся по файлам согласно data.write'
|
27
25
|
strict: Не угадывать динамические использования ключей, например `t("category.#{category.key}")`
|
28
26
|
translation_backend: Движок перевода (google или deepl)
|
29
27
|
value: >-
|
30
|
-
Значение, интерполируется с
|
31
|
-
|
28
|
+
Значение, интерполируется с %%{value}, %%{human_key}, %%{key}, %%{default}, %%{value_or_human_key},
|
29
|
+
%%{value_or_default_or_human_key}
|
32
30
|
desc:
|
33
31
|
add_missing: добавить недостающие ключи к переводам
|
34
32
|
check_consistent_interpolations: убедитесь, что во всех переводах используются правильные
|
35
33
|
интерполяционные переменные
|
36
34
|
check_normalized: проверить, что все файлы переводов нормализованы
|
37
35
|
config: показать конфигурацию
|
36
|
+
cp: скопируйте ключи в данных локали, соответствующие заданному шаблону
|
38
37
|
data: показать данные переводов
|
39
38
|
data_merge: добавить дерево к переводам
|
40
39
|
data_remove: удалить ключи, которые есть в дереве, из данных
|
@@ -94,6 +93,8 @@ ru:
|
|
94
93
|
Задайте ключ API DeepL через переменную окружения DEEPL_AUTH_KEY или translation.deepl_api_key
|
95
94
|
Получите ключ через https://www.deepl.com/pro.
|
96
95
|
no_results: DeepL не дал результатов.
|
96
|
+
specific_target_missing: You must supply a specific variant for the given target language
|
97
|
+
e.g. en-us instead of en.
|
97
98
|
google_translate:
|
98
99
|
errors:
|
99
100
|
no_api_key: >-
|
@@ -109,6 +110,12 @@ ru:
|
|
109
110
|
missing:
|
110
111
|
details_title: На других языках или в коде
|
111
112
|
none: Всё переведено.
|
113
|
+
openai_translate:
|
114
|
+
errors:
|
115
|
+
no_api_key: |-
|
116
|
+
Установить ключ API Яндекса с помощью переменной среды OPENAI_API_KEY или translation.openai_api_key
|
117
|
+
в config / i18n-tasks.yml. Получите ключ по адресу https://openai.com/.
|
118
|
+
no_results: Яндекс не дал результатов.
|
112
119
|
remove_unused:
|
113
120
|
confirm:
|
114
121
|
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']
|
@@ -35,16 +35,14 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
|
|
35
35
|
s.files = `git ls-files`.split($/)
|
36
36
|
s.files -= s.files.grep(%r{^(doc/|\.|spec/)}) + %w[CHANGES.md config/i18n-tasks.yml Gemfile]
|
37
37
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } - %w[i18n-tasks.cmd]
|
38
|
-
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
39
38
|
s.require_paths = ['lib']
|
40
39
|
|
41
40
|
s.add_dependency 'activesupport', '>= 4.0.2'
|
42
41
|
s.add_dependency 'ast', '>= 2.1.0'
|
43
|
-
s.add_dependency 'better_html', '>= 1.0', '< 3.0'
|
44
42
|
s.add_dependency 'erubi'
|
45
43
|
s.add_dependency 'highline', '>= 2.0.0'
|
46
44
|
s.add_dependency 'i18n'
|
47
|
-
s.add_dependency 'parser', '>= 2.2.
|
45
|
+
s.add_dependency 'parser', '>= 3.2.2.1'
|
48
46
|
s.add_dependency 'rails-i18n'
|
49
47
|
s.add_dependency 'rainbow', '>= 2.2.2', '< 4.0'
|
50
48
|
s.add_dependency 'terminal-table', '>= 1.5.1'
|
@@ -52,7 +50,9 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
|
|
52
50
|
s.add_development_dependency 'overcommit', '~> 0.58.0'
|
53
51
|
s.add_development_dependency 'rake'
|
54
52
|
s.add_development_dependency 'rspec', '~> 3.3'
|
55
|
-
s.add_development_dependency 'rubocop', '~> 1.
|
53
|
+
s.add_development_dependency 'rubocop', '~> 1.50.1'
|
54
|
+
s.add_development_dependency 'rubocop-rake', '~> 0.6.0'
|
55
|
+
s.add_development_dependency 'rubocop-rspec', '~> 2.19.0'
|
56
56
|
s.add_development_dependency 'simplecov'
|
57
57
|
s.add_development_dependency 'yard'
|
58
58
|
|
data/lib/i18n/tasks/cli.rb
CHANGED
@@ -35,14 +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
|
-
|
45
|
-
|
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
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
@@ -46,6 +46,20 @@ module I18n::Tasks
|
|
46
46
|
terminal_report.mv_results results
|
47
47
|
end
|
48
48
|
|
49
|
+
cmd :cp,
|
50
|
+
pos: 'FROM_KEY_PATTERN TO_KEY_PATTERN',
|
51
|
+
desc: t('i18n_tasks.cmd.desc.cp')
|
52
|
+
def cp(opt = {})
|
53
|
+
fail CommandError, 'requires FROM_KEY_PATTERN and TO_KEY_PATTERN' if opt[:arguments].size < 2
|
54
|
+
|
55
|
+
from_pattern = opt[:arguments].shift
|
56
|
+
to_pattern = opt[:arguments].shift
|
57
|
+
forest = i18n.data_forest
|
58
|
+
results = forest.mv_key!(compile_key_pattern(from_pattern), to_pattern, root: false, retain: true)
|
59
|
+
i18n.data.write forest
|
60
|
+
terminal_report.cp_results results
|
61
|
+
end
|
62
|
+
|
49
63
|
cmd :rm,
|
50
64
|
pos: 'KEY_PATTERN [KEY_PATTERN...]',
|
51
65
|
desc: t('i18n_tasks.cmd.desc.rm')
|
@@ -43,7 +43,7 @@ module I18n::Tasks
|
|
43
43
|
args: [:locales, :locale_to_translate_from, arg(:out_format).from(1), :translation_backend, :pattern]
|
44
44
|
|
45
45
|
def translate_missing(opt = {})
|
46
|
-
missing
|
46
|
+
missing = i18n.missing_diff_forest opt[:locales], opt[:from]
|
47
47
|
if opt[:pattern]
|
48
48
|
pattern_re = i18n.compile_key_pattern(opt[:pattern])
|
49
49
|
missing.select_keys! { |full_key, _node| full_key =~ pattern_re }
|
@@ -61,7 +61,7 @@ module I18n::Tasks
|
|
61
61
|
['--nil-value', 'Set value to nil. Takes precedence over the value argument.']]
|
62
62
|
|
63
63
|
# Merge base locale first, as this may affect the value for the other locales
|
64
|
-
def add_missing(opt = {})
|
64
|
+
def add_missing(opt = {}) # rubocop:disable Metrics/AbcSize
|
65
65
|
[
|
66
66
|
[i18n.base_locale] & opt[:locales],
|
67
67
|
opt[:locales] - [i18n.base_locale]
|
@@ -26,7 +26,7 @@ module I18n::Tasks
|
|
26
26
|
arg :value,
|
27
27
|
'-v',
|
28
28
|
'--value VALUE',
|
29
|
-
t('i18n_tasks.cmd.args.desc.value')
|
29
|
+
t('i18n_tasks.cmd.args.desc.value', dummy: 'value') # Dummy value is workaround for https://github.com/ruby-i18n/i18n/issues/689
|
30
30
|
|
31
31
|
arg :config,
|
32
32
|
'-c',
|
@@ -66,6 +66,8 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
|
|
66
66
|
conf[:deepl_api_key] = ENV['DEEPL_AUTH_KEY'] if ENV.key?('DEEPL_AUTH_KEY')
|
67
67
|
conf[:deepl_host] = ENV['DEEPL_HOST'] if ENV.key?('DEEPL_HOST')
|
68
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')
|
70
|
+
conf[:openai_model] = ENV['OPENAI_MODEL'] if ENV.key?('OPENAI_MODEL')
|
69
71
|
conf[:yandex_api_key] = ENV['YANDEX_API_KEY'] if ENV.key?('YANDEX_API_KEY')
|
70
72
|
conf
|
71
73
|
end
|
@@ -87,7 +89,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
|
|
87
89
|
valid_locales = Dir[File.join(I18n::Tasks.gem_path, 'config', 'locales', '*.yml')]
|
88
90
|
.map { |f| File.basename(f, '.yml') }
|
89
91
|
unless valid_locales.include?(internal_locale)
|
90
|
-
log_warn "invalid internal_locale #{internal_locale.inspect}. "\
|
92
|
+
log_warn "invalid internal_locale #{internal_locale.inspect}. " \
|
91
93
|
"Available internal locales: #{valid_locales * ', '}."
|
92
94
|
internal_locale = DEFAULTS[:internal_locale].to_s
|
93
95
|
end
|
@@ -6,6 +6,7 @@ module I18n::Tasks
|
|
6
6
|
module Adapter
|
7
7
|
module YamlAdapter
|
8
8
|
EMOJI_REGEX = /\\u[\da-f]{8}/i.freeze
|
9
|
+
TRAILING_SPACE_REGEX = / $/.freeze
|
9
10
|
|
10
11
|
class << self
|
11
12
|
# @return [Hash] locale tree
|
@@ -20,13 +21,18 @@ module I18n::Tasks
|
|
20
21
|
|
21
22
|
# @return [String]
|
22
23
|
def dump(tree, options)
|
23
|
-
restore_emojis(tree.to_yaml(options || {}))
|
24
|
+
strip_trailing_spaces(restore_emojis(tree.to_yaml(options || {})))
|
24
25
|
end
|
25
26
|
|
26
27
|
# @return [String]
|
27
28
|
def restore_emojis(yaml)
|
28
29
|
yaml.gsub(EMOJI_REGEX) { |m| [m[-8..].to_i(16)].pack('U') }
|
29
30
|
end
|
31
|
+
|
32
|
+
# @return [String]
|
33
|
+
def strip_trailing_spaces(yaml)
|
34
|
+
yaml.gsub(TRAILING_SPACE_REGEX, '')
|
35
|
+
end
|
30
36
|
end
|
31
37
|
end
|
32
38
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'i18n/tasks/data/tree/node'
|
4
4
|
require 'i18n/tasks/data/router/pattern_router'
|
5
5
|
require 'i18n/tasks/data/router/conservative_router'
|
6
|
+
require 'i18n/tasks/data/router/isolating_router'
|
6
7
|
require 'i18n/tasks/data/file_formats'
|
7
8
|
require 'i18n/tasks/key_pattern_matching'
|
8
9
|
|
@@ -151,6 +152,7 @@ module I18n::Tasks
|
|
151
152
|
|
152
153
|
ROUTER_NAME_ALIASES = {
|
153
154
|
'conservative_router' => 'I18n::Tasks::Data::Router::ConservativeRouter',
|
155
|
+
'isolating_router' => 'I18n::Tasks::Data::Router::IsolatingRouter',
|
154
156
|
'pattern_router' => 'I18n::Tasks::Data::Router::PatternRouter'
|
155
157
|
}.freeze
|
156
158
|
def router
|
@@ -170,6 +172,9 @@ module I18n::Tasks
|
|
170
172
|
end.map do |path|
|
171
173
|
[path.freeze, load_file(path) || {}]
|
172
174
|
end.map do |path, data|
|
175
|
+
if router.is_a?(I18n::Tasks::Data::Router::IsolatingRouter)
|
176
|
+
data.transform_values! { |tree| { "<#{router.alternate_path_for(path, base_locale)}>" => tree } }
|
177
|
+
end
|
173
178
|
filter_nil_keys! path, data
|
174
179
|
Data::Tree::Siblings.from_nested_hash(data).tap do |s|
|
175
180
|
s.leaves { |x| x.data.update(path: path, locale: locale) }
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/tasks/key_pattern_matching'
|
4
|
+
require 'i18n/tasks/data/tree/node'
|
5
|
+
|
6
|
+
module I18n::Tasks
|
7
|
+
module Data::Router
|
8
|
+
# Route based on source file path
|
9
|
+
class IsolatingRouter
|
10
|
+
include ::I18n::Tasks::KeyPatternMatching
|
11
|
+
|
12
|
+
attr_reader :config_read_patterns, :base_locale
|
13
|
+
|
14
|
+
def initialize(_adapter, data_config)
|
15
|
+
@base_locale = data_config[:base_locale]
|
16
|
+
@config_read_patterns = Array.wrap(data_config[:read])
|
17
|
+
end
|
18
|
+
|
19
|
+
# Route keys to destinations
|
20
|
+
# @param forest [I18n::Tasks::Data::Tree::Siblings] forest roots are locales.
|
21
|
+
# @yieldparam [String] dest_path
|
22
|
+
# @yieldparam [I18n::Tasks::Data::Tree::Siblings] tree_slice
|
23
|
+
# @return [Hash] mapping of destination => [ [key, value], ... ]
|
24
|
+
def route(locale, forest, &block)
|
25
|
+
return to_enum(:route, locale, forest) unless block
|
26
|
+
|
27
|
+
locale = locale.to_s
|
28
|
+
out = {}
|
29
|
+
|
30
|
+
forest.keys do |key_namespaced_with_source_path, _node|
|
31
|
+
source_path, key = key_namespaced_with_source_path.match(/\A<([^>]*)>\.(.*)/).captures
|
32
|
+
target_path = alternate_path_for(source_path, locale)
|
33
|
+
next unless source_path && key && target_path
|
34
|
+
|
35
|
+
(out[target_path] ||= Set.new) << "#{locale}.#{key}"
|
36
|
+
end
|
37
|
+
|
38
|
+
out.each do |target_path, keys|
|
39
|
+
file_namespace_subtree = I18n::Tasks::Data::Tree::Siblings.new(
|
40
|
+
nodes: forest.get("#{locale}.<#{alternate_path_for(target_path, base_locale)}>")
|
41
|
+
)
|
42
|
+
file_namespace_subtree.set_root_key!(locale)
|
43
|
+
|
44
|
+
block.yield(
|
45
|
+
target_path,
|
46
|
+
file_namespace_subtree.select_keys { |key, _| keys.include?(key) }
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def alternate_path_for(source_path, locale)
|
52
|
+
source_path = source_path.dup
|
53
|
+
|
54
|
+
config_read_patterns.each do |pattern|
|
55
|
+
regexp = Glob.new(format(pattern, locale: '(*)')).to_regexp
|
56
|
+
next unless source_path.match?(regexp)
|
57
|
+
|
58
|
+
source_path.match(regexp) do |match_data|
|
59
|
+
(1..match_data.size - 1).reverse_each do |capture_index|
|
60
|
+
capture_begin, capture_end = match_data.offset(capture_index)
|
61
|
+
source_path.slice!(Range.new(capture_begin, capture_end, true))
|
62
|
+
source_path.insert(capture_begin, locale.to_s)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
return source_path
|
67
|
+
end
|
68
|
+
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# based on https://github.com/alexch/rerun/blob/36f2d237985b670752abbe4a7f6814893cdde96f/lib/rerun/glob.rb
|
73
|
+
class Glob
|
74
|
+
NO_LEADING_DOT = '(?=[^\.])'
|
75
|
+
START_OF_FILENAME = '(?:\A|\/)'
|
76
|
+
END_OF_STRING = '\z'
|
77
|
+
|
78
|
+
def initialize(pattern)
|
79
|
+
@pattern = pattern
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_regexp_string # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
83
|
+
chars = smoosh(@pattern.chars)
|
84
|
+
|
85
|
+
curlies = 0
|
86
|
+
escaping = false
|
87
|
+
|
88
|
+
string = chars.map do |char|
|
89
|
+
if escaping
|
90
|
+
escaping = false
|
91
|
+
next char
|
92
|
+
end
|
93
|
+
|
94
|
+
case char
|
95
|
+
when '**' then '(?:[^/]+/)*'
|
96
|
+
when '*' then '.*'
|
97
|
+
when '?' then '.'
|
98
|
+
when '.' then '\.'
|
99
|
+
when '{'
|
100
|
+
curlies += 1
|
101
|
+
'('
|
102
|
+
when '}'
|
103
|
+
if curlies.positive?
|
104
|
+
curlies -= 1
|
105
|
+
')'
|
106
|
+
else
|
107
|
+
char
|
108
|
+
end
|
109
|
+
when ','
|
110
|
+
if curlies.positive?
|
111
|
+
'|'
|
112
|
+
else
|
113
|
+
char
|
114
|
+
end
|
115
|
+
when '\\'
|
116
|
+
escaping = true
|
117
|
+
'\\'
|
118
|
+
else char
|
119
|
+
end
|
120
|
+
end.join
|
121
|
+
|
122
|
+
START_OF_FILENAME + string + END_OF_STRING
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_regexp
|
126
|
+
Regexp.new(to_regexp_string)
|
127
|
+
end
|
128
|
+
|
129
|
+
def smoosh(chars)
|
130
|
+
out = []
|
131
|
+
until chars.empty?
|
132
|
+
char = chars.shift
|
133
|
+
if char == '*' && chars.first == '*'
|
134
|
+
chars.shift
|
135
|
+
chars.shift if chars.first == '/'
|
136
|
+
out.push('**')
|
137
|
+
else
|
138
|
+
out.push(char)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
out
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -36,7 +36,7 @@ module I18n::Tasks::Data::Tree
|
|
36
36
|
# @param to_pattern [Regexp]
|
37
37
|
# @param root [Boolean]
|
38
38
|
# @return {old key => new key}
|
39
|
-
def mv_key!(from_pattern, to_pattern, root: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
39
|
+
def mv_key!(from_pattern, to_pattern, root: false, retain: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
40
40
|
moved_forest = Siblings.new
|
41
41
|
moved_nodes = []
|
42
42
|
old_key_to_new_key = {}
|
@@ -69,7 +69,7 @@ module I18n::Tasks::Data::Tree
|
|
69
69
|
node.value = new_target.to_sym
|
70
70
|
end
|
71
71
|
end
|
72
|
-
remove_nodes_and_emptied_ancestors!
|
72
|
+
remove_nodes_and_emptied_ancestors!(moved_nodes) unless retain
|
73
73
|
merge! moved_forest
|
74
74
|
old_key_to_new_key
|
75
75
|
end
|
@@ -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?
|
@@ -5,7 +5,7 @@ module I18n::Tasks
|
|
5
5
|
class << self
|
6
6
|
attr_accessor :variable_regex
|
7
7
|
end
|
8
|
-
@variable_regex =
|
8
|
+
@variable_regex = /(?<!%)%{[^}]+}/.freeze
|
9
9
|
|
10
10
|
def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disable Metrics/AbcSize
|
11
11
|
locales ||= self.locales
|
@@ -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)
|
@@ -30,9 +31,10 @@ module I18n::Tasks::KeyPatternMatching
|
|
30
31
|
|
31
32
|
def key_pattern_re_body(key_pattern)
|
32
33
|
key_pattern
|
33
|
-
.gsub(
|
34
|
-
.gsub(
|
35
|
-
.gsub(
|
34
|
+
.gsub('.', '\.')
|
35
|
+
.gsub('*:', '[^.]+?')
|
36
|
+
.gsub('*', '.*')
|
37
|
+
.gsub(':', '(?<=^|\.)[^.]+?(?=\.|$)')
|
36
38
|
.gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, '|')})" }
|
37
39
|
end
|
38
40
|
end
|