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