i18n-tasks 1.0.13 → 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 +61 -1
- data/config/locales/en.yml +3 -4
- data/config/locales/ru.yml +3 -4
- data/i18n-tasks.gemspec +0 -1
- data/lib/i18n/tasks/command/commands/data.rb +14 -0
- data/lib/i18n/tasks/command/options/common.rb +1 -1
- data/lib/i18n/tasks/configuration.rb +1 -0
- 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/interpolations.rb +1 -1
- data/lib/i18n/tasks/key_pattern_matching.rb +4 -4
- data/lib/i18n/tasks/reports/terminal.rb +6 -0
- data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +26 -0
- 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 +2 -1
- data/lib/i18n/tasks/split_key.rb +30 -47
- data/lib/i18n/tasks/translators/base_translator.rb +6 -0
- data/lib/i18n/tasks/translators/deepl_translator.rb +27 -1
- data/lib/i18n/tasks/translators/google_translator.rb +27 -12
- data/lib/i18n/tasks/translators/openai_translator.rb +25 -7
- data/lib/i18n/tasks/used_keys.rb +2 -2
- data/lib/i18n/tasks/version.rb +1 -1
- data/templates/config/i18n-tasks.yml +25 -2
- metadata +5 -24
- 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):
|
@@ -338,6 +338,33 @@ data:
|
|
338
338
|
If you want to have i18n-tasks reorganize your existing keys using `data.write`, either set the router to
|
339
339
|
`pattern_router` as above, or run `i18n-tasks normalize -p` (forcing the use of the pattern router for that run).
|
340
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
|
+
|
341
368
|
##### Key pattern syntax
|
342
369
|
|
343
370
|
A special syntax similar to file glob patterns is used throughout i18n-tasks to match translation keys:
|
@@ -411,6 +438,12 @@ translation:
|
|
411
438
|
google_translate_api_key: <Google Translate API key>
|
412
439
|
```
|
413
440
|
|
441
|
+
or via environment variable:
|
442
|
+
|
443
|
+
```bash
|
444
|
+
GOOGLE_TRANSLATE_API_KEY=<Google Translate API key>
|
445
|
+
```
|
446
|
+
|
414
447
|
<a name="deepl-translation-config"></a>
|
415
448
|
### DeepL Pro Translate
|
416
449
|
|
@@ -422,6 +455,19 @@ translation:
|
|
422
455
|
deepl_api_key: <DeepL Pro API key>
|
423
456
|
deepl_host: <optional>
|
424
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>
|
425
471
|
```
|
426
472
|
|
427
473
|
<a name="yandex-translation-config"></a>
|
@@ -435,6 +481,12 @@ translation:
|
|
435
481
|
yandex_api_key: <Yandex API key>
|
436
482
|
```
|
437
483
|
|
484
|
+
or via environment variable:
|
485
|
+
|
486
|
+
```bash
|
487
|
+
YANDEX_API_KEY=<Yandex API key>
|
488
|
+
```
|
489
|
+
|
438
490
|
<a name="openai-translation-config"></a>
|
439
491
|
### OpenAI Translate
|
440
492
|
|
@@ -444,6 +496,14 @@ translation:
|
|
444
496
|
# config/i18n-tasks.yml
|
445
497
|
translation:
|
446
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>
|
447
507
|
```
|
448
508
|
|
449
509
|
## Interactive console
|
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
|
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: удалить ключи, которые есть в дереве, из данных
|
data/i18n-tasks.gemspec
CHANGED
@@ -39,7 +39,6 @@ Gem::Specification.new do |s|
|
|
39
39
|
|
40
40
|
s.add_dependency 'activesupport', '>= 4.0.2'
|
41
41
|
s.add_dependency 'ast', '>= 2.1.0'
|
42
|
-
s.add_dependency 'better_html', '>= 1.0', '< 3.0'
|
43
42
|
s.add_dependency 'erubi'
|
44
43
|
s.add_dependency 'highline', '>= 2.0.0'
|
45
44
|
s.add_dependency 'i18n'
|
@@ -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')
|
@@ -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',
|
@@ -67,6 +67,7 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
|
|
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
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')
|
70
71
|
conf[:yandex_api_key] = ENV['YANDEX_API_KEY'] if ENV.key?('YANDEX_API_KEY')
|
71
72
|
conf
|
72
73
|
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
|
@@ -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
|
@@ -31,10 +31,10 @@ module I18n::Tasks::KeyPatternMatching
|
|
31
31
|
|
32
32
|
def key_pattern_re_body(key_pattern)
|
33
33
|
key_pattern
|
34
|
-
.gsub(
|
35
|
-
.gsub(
|
36
|
-
.gsub(
|
37
|
-
.gsub(
|
34
|
+
.gsub('.', '\.')
|
35
|
+
.gsub('*:', '[^.]+?')
|
36
|
+
.gsub('*', '.*')
|
37
|
+
.gsub(':', '(?<=^|\.)[^.]+?(?=\.|$)')
|
38
38
|
.gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, '|')})" }
|
39
39
|
end
|
40
40
|
end
|
@@ -93,6 +93,12 @@ module I18n
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
+
def cp_results(results)
|
97
|
+
results.each do |(from, to)|
|
98
|
+
print_info "#{Rainbow(from).cyan} #{Rainbow('+').yellow.bright} #{Rainbow(to).green}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
96
102
|
def check_normalized_results(non_normalized)
|
97
103
|
if non_normalized.empty?
|
98
104
|
print_success 'All data is normalized'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/tasks/scanners/ast_matchers/base_matcher'
|
4
|
+
require 'i18n/tasks/scanners/results/occurrence'
|
5
|
+
|
6
|
+
module I18n::Tasks::Scanners::AstMatchers
|
7
|
+
class DefaultI18nSubjectMatcher < BaseMatcher
|
8
|
+
def convert_to_key_occurrences(send_node, method_name, location: send_node.loc)
|
9
|
+
children = Array(send_node&.children)
|
10
|
+
return unless children[1] == :default_i18n_subject
|
11
|
+
|
12
|
+
key = @scanner.absolute_key(
|
13
|
+
'.subject',
|
14
|
+
location.expression.source_buffer.name,
|
15
|
+
calling_method: method_name
|
16
|
+
)
|
17
|
+
[
|
18
|
+
key,
|
19
|
+
I18n::Tasks::Scanners::Results::Occurrence.from_range(
|
20
|
+
raw_key: key,
|
21
|
+
range: location.expression
|
22
|
+
)
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'i18n/tasks/scanners/ruby_ast_scanner'
|
4
|
-
require 'i18n/tasks/scanners/
|
5
|
-
require 'better_html/errors'
|
6
|
-
require 'better_html/parser'
|
4
|
+
require 'i18n/tasks/scanners/local_ruby_parser'
|
7
5
|
|
8
6
|
module I18n::Tasks::Scanners
|
9
7
|
# Scan for I18n.translate calls in ERB-file better-html and ASTs
|
10
8
|
class ErbAstScanner < RubyAstScanner
|
9
|
+
DEFAULT_REGEXP = /<%(={1,2}|-|\#|%)?(.*?)([-=])?%>/m.freeze
|
10
|
+
|
11
11
|
def initialize(**args)
|
12
12
|
super(**args)
|
13
|
-
@
|
13
|
+
@ruby_parser = LocalRubyParser.new(ignore_blocks: true)
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
@@ -20,29 +20,59 @@ module I18n::Tasks::Scanners
|
|
20
20
|
# @param path Path to file to parse
|
21
21
|
# @return [{Parser::AST::Node}, [Parser::Source::Comment]]
|
22
22
|
def path_to_ast_and_comments(path)
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
comments = []
|
24
|
+
buffer = make_buffer(path)
|
25
|
+
|
26
|
+
children = []
|
27
|
+
buffer
|
28
|
+
.source
|
29
|
+
.scan(DEFAULT_REGEXP) do |indicator, code, tailch, _rspace|
|
30
|
+
match = Regexp.last_match
|
31
|
+
character = indicator ? indicator[0] : nil
|
32
|
+
|
33
|
+
start = match.begin(0) + 2 + (character&.size || 0)
|
34
|
+
stop = match.end(0) - 2 - (tailch&.size || 0)
|
35
|
+
|
36
|
+
case character
|
37
|
+
when '=', nil, '-'
|
38
|
+
parsed, parsed_comments = handle_code(buffer, code, start, stop)
|
39
|
+
comments.concat(parsed_comments)
|
40
|
+
children << parsed unless parsed.nil?
|
41
|
+
when '#', '#-'
|
42
|
+
comments << handle_comment(buffer, start, stop)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
[root_node(children, buffer), comments]
|
26
47
|
end
|
27
48
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
49
|
+
def handle_code(buffer, code, start, stop)
|
50
|
+
range = ::Parser::Source::Range.new(buffer, start, stop)
|
51
|
+
location =
|
52
|
+
Parser::Source::Map::Definition.new(
|
53
|
+
range.begin,
|
54
|
+
range.begin,
|
55
|
+
range.begin,
|
56
|
+
range.end
|
57
|
+
)
|
58
|
+
@ruby_parser.parse(code, location: location)
|
59
|
+
end
|
60
|
+
|
61
|
+
def handle_comment(buffer, start, stop)
|
62
|
+
range = ::Parser::Source::Range.new(buffer, start, stop)
|
63
|
+
::Parser::Source::Comment.new(range)
|
64
|
+
end
|
65
|
+
|
66
|
+
def root_node(children, buffer)
|
67
|
+
range = ::Parser::Source::Range.new(buffer, 0, buffer.source.size)
|
68
|
+
location =
|
69
|
+
Parser::Source::Map::Definition.new(
|
70
|
+
range.begin,
|
71
|
+
range.begin,
|
72
|
+
range.begin,
|
73
|
+
range.end
|
74
|
+
)
|
75
|
+
::Parser::AST::Node.new(:erb, children, location: location)
|
46
76
|
end
|
47
77
|
end
|
48
78
|
end
|
@@ -12,7 +12,7 @@ module I18n::Tasks::Scanners
|
|
12
12
|
include OccurrenceFromPosition
|
13
13
|
include RubyKeyLiterals
|
14
14
|
|
15
|
-
TRANSLATE_CALL_RE = /(?<=^|[^\
|
15
|
+
TRANSLATE_CALL_RE = /(?<=^|[^\p{L}'\-.]|[^\p{L}'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
|
16
16
|
IGNORE_LINES = {
|
17
17
|
'coffee' => /^\s*#(?!\si18n-tasks-use)/,
|
18
18
|
'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'i18n/tasks/scanners/file_scanner'
|
4
4
|
require 'i18n/tasks/scanners/relative_keys'
|
5
5
|
require 'i18n/tasks/scanners/ruby_ast_call_finder'
|
6
|
+
require 'i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher'
|
6
7
|
require 'i18n/tasks/scanners/ast_matchers/message_receivers_matcher'
|
7
8
|
require 'i18n/tasks/scanners/ast_matchers/rails_model_matcher'
|
8
9
|
require 'parser/current'
|
@@ -80,7 +81,7 @@ module I18n::Tasks::Scanners
|
|
80
81
|
results = []
|
81
82
|
|
82
83
|
# method_name is not available at this stage
|
83
|
-
calls.each do |send_node, _method_name|
|
84
|
+
calls.each do |(send_node, _method_name)|
|
84
85
|
@matchers.each do |matcher|
|
85
86
|
result = matcher.convert_to_key_occurrences(
|
86
87
|
send_node,
|
data/lib/i18n/tasks/split_key.rb
CHANGED
@@ -5,6 +5,11 @@ module I18n
|
|
5
5
|
module SplitKey
|
6
6
|
module_function
|
7
7
|
|
8
|
+
PARENTHESIS_PAIRS = %w({} [] () <>).freeze
|
9
|
+
START_KEYS = PARENTHESIS_PAIRS.to_set { |pair| pair[0] }.freeze
|
10
|
+
END_KEYS = PARENTHESIS_PAIRS.to_h { |pair| [pair[0], pair[1]] }.freeze
|
11
|
+
private_constant :PARENTHESIS_PAIRS, :START_KEYS, :END_KEYS
|
12
|
+
|
8
13
|
# split a key by dots (.)
|
9
14
|
# dots inside braces or parenthesis are not split on
|
10
15
|
#
|
@@ -12,61 +17,39 @@ module I18n
|
|
12
17
|
# split_key 'a.#{b.c}' # => ['a', '#{b.c}']
|
13
18
|
# split_key 'a.b.c', 2 # => ['a', 'b.c']
|
14
19
|
def split_key(key, max = Float::INFINITY)
|
15
|
-
parts = []
|
16
|
-
pos = 0
|
17
20
|
return [key] if max == 1
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
parts = []
|
23
|
+
current_parenthesis_end_char = nil
|
24
|
+
part = ''
|
25
|
+
key.each_char.with_index do |char, index|
|
26
|
+
if current_parenthesis_end_char
|
27
|
+
part += char
|
28
|
+
current_parenthesis_end_char = nil if char == current_parenthesis_end_char
|
29
|
+
elsif START_KEYS.include?(char)
|
30
|
+
part += char
|
31
|
+
current_parenthesis_end_char = END_KEYS[char]
|
32
|
+
elsif char == '.'
|
33
|
+
parts << part
|
34
|
+
if parts.size + 1 == max
|
35
|
+
remaining = key[(index + 1)..]
|
36
|
+
parts << remaining unless remaining.empty?
|
37
|
+
return parts
|
38
|
+
end
|
39
|
+
part = ''
|
40
|
+
else
|
41
|
+
part += char
|
25
42
|
end
|
26
43
|
end
|
27
|
-
parts
|
28
|
-
end
|
29
|
-
|
30
|
-
def last_key_part(key)
|
31
|
-
last = nil
|
32
|
-
key_parts(key) { |part| last = part }
|
33
|
-
last
|
34
|
-
end
|
35
44
|
|
36
|
-
|
37
|
-
# dots inside braces or parenthesis are not split on
|
38
|
-
def key_parts(key, &block)
|
39
|
-
return enum_for(:key_parts, key) unless block
|
45
|
+
return parts if part.empty?
|
40
46
|
|
41
|
-
|
42
|
-
counts = PARENS_ZEROS # dup'd later if key contains parenthesis
|
43
|
-
delim = '.'
|
44
|
-
from = to = 0
|
45
|
-
key.each_char do |char|
|
46
|
-
if char == delim && PARENS_ZEROS == counts
|
47
|
-
block.yield key[from...to]
|
48
|
-
from = to = (to + 1)
|
49
|
-
else
|
50
|
-
nest_i, nest_inc = nesting[char]
|
51
|
-
if nest_i
|
52
|
-
counts = counts.dup if counts.frozen?
|
53
|
-
counts[nest_i] += nest_inc
|
54
|
-
end
|
55
|
-
to += 1
|
56
|
-
end
|
57
|
-
end
|
58
|
-
block.yield(key[from...to]) if from < to && to <= key.length
|
59
|
-
true
|
47
|
+
current_parenthesis_end_char ? parts.concat(part.split('.')) : parts << part
|
60
48
|
end
|
61
49
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
h[s[1].freeze] = [i, -1].freeze
|
66
|
-
end.freeze
|
67
|
-
PARENS_ZEROS = Array.new(PARENS.size, 0).freeze
|
68
|
-
private_constant :PARENS
|
69
|
-
private_constant :PARENS_ZEROS
|
50
|
+
def last_key_part(key)
|
51
|
+
split_key(key).last
|
52
|
+
end
|
70
53
|
end
|
71
54
|
end
|
72
55
|
end
|
@@ -70,6 +70,9 @@ module I18n::Tasks
|
|
70
70
|
when Array
|
71
71
|
# dump recursively
|
72
72
|
value.map { |v| dump_value(v, opts) }
|
73
|
+
when Hash
|
74
|
+
# dump recursively
|
75
|
+
value.values.map { |v| dump_value(v, opts) }
|
73
76
|
when String
|
74
77
|
value = CGI.escapeHTML(value) if opts[:html_escape]
|
75
78
|
replace_interpolations value unless value.empty?
|
@@ -85,6 +88,9 @@ module I18n::Tasks
|
|
85
88
|
when Array
|
86
89
|
# implode array
|
87
90
|
untranslated.map { |from| parse_value(from, each_translated, opts) }
|
91
|
+
when Hash
|
92
|
+
# implode hash
|
93
|
+
untranslated.transform_values { |value| parse_value(value, each_translated, opts) }
|
88
94
|
when String
|
89
95
|
if untranslated.empty?
|
90
96
|
untranslated
|
@@ -24,7 +24,12 @@ module I18n::Tasks::Translators
|
|
24
24
|
def translate_values(list, from:, to:, **options)
|
25
25
|
results = []
|
26
26
|
list.each_slice(BATCH_SIZE) do |parts|
|
27
|
-
res = DeepL.translate(
|
27
|
+
res = DeepL.translate(
|
28
|
+
parts,
|
29
|
+
to_deepl_source_locale(from),
|
30
|
+
to_deepl_target_locale(to),
|
31
|
+
options_with_glossary(options, from, to)
|
32
|
+
)
|
28
33
|
if res.is_a?(DeepL::Resources::Text)
|
29
34
|
results << res.text
|
30
35
|
else
|
@@ -100,5 +105,26 @@ module I18n::Tasks::Translators
|
|
100
105
|
config.version = version unless version.blank?
|
101
106
|
end
|
102
107
|
end
|
108
|
+
|
109
|
+
def options_with_glossary(options, from, to)
|
110
|
+
glossary = find_glossary(from, to)
|
111
|
+
glossary ? { glossary_id: glossary.id }.merge(options) : options
|
112
|
+
end
|
113
|
+
|
114
|
+
def all_ready_glossaries
|
115
|
+
@all_ready_glossaries ||= DeepL.glossaries.list
|
116
|
+
end
|
117
|
+
|
118
|
+
def find_glossary(from, to)
|
119
|
+
config_glossary_ids = @i18n_tasks.translation_config[:deepl_glossary_ids]
|
120
|
+
return unless config_glossary_ids
|
121
|
+
|
122
|
+
all_ready_glossaries.find do |glossary|
|
123
|
+
glossary.ready \
|
124
|
+
&& glossary.source_lang == from \
|
125
|
+
&& glossary.target_lang == to \
|
126
|
+
&& config_glossary_ids.include?(glossary.id)
|
127
|
+
end
|
128
|
+
end
|
103
129
|
end
|
104
130
|
end
|
@@ -4,6 +4,7 @@ require 'i18n/tasks/translators/base_translator'
|
|
4
4
|
|
5
5
|
module I18n::Tasks::Translators
|
6
6
|
class GoogleTranslator < BaseTranslator
|
7
|
+
NEWLINE_PLACEHOLDER = '<br id=i18n />'
|
7
8
|
def initialize(*)
|
8
9
|
begin
|
9
10
|
require 'easy_translate'
|
@@ -16,14 +17,21 @@ module I18n::Tasks::Translators
|
|
16
17
|
protected
|
17
18
|
|
18
19
|
def translate_values(list, **options)
|
19
|
-
|
20
|
+
restore_newlines(
|
21
|
+
EasyTranslate.translate(
|
22
|
+
replace_newlines_with_placeholder(list, options[:html]),
|
23
|
+
options,
|
24
|
+
format: :text
|
25
|
+
),
|
26
|
+
options[:html]
|
27
|
+
)
|
20
28
|
end
|
21
29
|
|
22
30
|
def options_for_translate_values(from:, to:, **options)
|
23
31
|
options.merge(
|
24
32
|
api_key: api_key,
|
25
|
-
from:
|
26
|
-
to:
|
33
|
+
from: from,
|
34
|
+
to: to
|
27
35
|
)
|
28
36
|
end
|
29
37
|
|
@@ -41,15 +49,6 @@ module I18n::Tasks::Translators
|
|
41
49
|
|
42
50
|
private
|
43
51
|
|
44
|
-
SUPPORTED_LOCALES_WITH_REGION = %w[zh-CN zh-TW].freeze
|
45
|
-
|
46
|
-
# Convert 'es-ES' to 'es'
|
47
|
-
def to_google_translate_compatible_locale(locale)
|
48
|
-
return locale unless locale.include?('-') && !SUPPORTED_LOCALES_WITH_REGION.include?(locale)
|
49
|
-
|
50
|
-
locale.split('-', 2).first
|
51
|
-
end
|
52
|
-
|
53
52
|
def api_key
|
54
53
|
@api_key ||= begin
|
55
54
|
key = @i18n_tasks.translation_config[:google_translate_api_key]
|
@@ -65,5 +64,21 @@ module I18n::Tasks::Translators
|
|
65
64
|
key
|
66
65
|
end
|
67
66
|
end
|
67
|
+
|
68
|
+
def replace_newlines_with_placeholder(list, html)
|
69
|
+
return list unless html
|
70
|
+
|
71
|
+
list.map do |value|
|
72
|
+
value.gsub("\n", NEWLINE_PLACEHOLDER)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def restore_newlines(translations, html)
|
77
|
+
return translations unless html
|
78
|
+
|
79
|
+
translations.map do |translation|
|
80
|
+
translation.gsub("#{NEWLINE_PLACEHOLDER} ", "\n")
|
81
|
+
end
|
82
|
+
end
|
68
83
|
end
|
69
84
|
end
|
@@ -1,11 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'i18n/tasks/translators/base_translator'
|
4
|
+
require 'active_support/core_ext/string/filters'
|
4
5
|
|
5
6
|
module I18n::Tasks::Translators
|
6
7
|
class OpenAiTranslator < BaseTranslator
|
7
8
|
# max allowed texts per request
|
8
9
|
BATCH_SIZE = 50
|
10
|
+
DEFAULT_SYSTEM_PROMPT = <<~PROMPT.squish
|
11
|
+
You are a professional translator that translates content from the %{from} locale
|
12
|
+
to the %{to} locale in an i18n locale array.
|
13
|
+
|
14
|
+
The array has a structured format and contains multiple strings. Your task is to translate
|
15
|
+
each of these strings and create a new array with the translated strings.
|
16
|
+
|
17
|
+
HTML markups (enclosed in < and > characters) must not be changed under any circumstance.
|
18
|
+
Variables (starting with %%{ and ending with }) must not be changed under any circumstance.
|
19
|
+
|
20
|
+
Keep in mind the context of all the strings for a more accurate translation.
|
21
|
+
PROMPT
|
9
22
|
|
10
23
|
def initialize(*)
|
11
24
|
begin
|
@@ -50,6 +63,14 @@ module I18n::Tasks::Translators
|
|
50
63
|
end
|
51
64
|
end
|
52
65
|
|
66
|
+
def model
|
67
|
+
@model ||= @i18n_tasks.translation_config[:openai_model].presence || 'gpt-3.5-turbo'
|
68
|
+
end
|
69
|
+
|
70
|
+
def system_prompt
|
71
|
+
@system_prompt ||= @i18n_tasks.translation_config[:openai_system_prompt].presence || DEFAULT_SYSTEM_PROMPT
|
72
|
+
end
|
73
|
+
|
53
74
|
def translate_values(list, from:, to:)
|
54
75
|
results = []
|
55
76
|
|
@@ -62,14 +83,11 @@ module I18n::Tasks::Translators
|
|
62
83
|
results.flatten
|
63
84
|
end
|
64
85
|
|
65
|
-
def translate(values, from, to)
|
86
|
+
def translate(values, from, to)
|
66
87
|
messages = [
|
67
88
|
{
|
68
89
|
role: 'system',
|
69
|
-
content:
|
70
|
-
locale array. The array has a structured format and contains multiple strings. Your task is to translate
|
71
|
-
each of these strings and create a new array with the translated strings. Keep in mind the context of all
|
72
|
-
the strings for a more accurate translation.\n"
|
90
|
+
content: format(system_prompt, from: from, to: to)
|
73
91
|
},
|
74
92
|
{
|
75
93
|
role: 'user',
|
@@ -83,9 +101,9 @@ module I18n::Tasks::Translators
|
|
83
101
|
|
84
102
|
response = translator.chat(
|
85
103
|
parameters: {
|
86
|
-
model:
|
104
|
+
model: model,
|
87
105
|
messages: messages,
|
88
|
-
temperature: 0.
|
106
|
+
temperature: 0.0
|
89
107
|
}
|
90
108
|
)
|
91
109
|
|
data/lib/i18n/tasks/used_keys.rb
CHANGED
@@ -21,8 +21,8 @@ module I18n::Tasks
|
|
21
21
|
relative_roots: %w[app/controllers app/helpers app/mailers app/presenters app/views].freeze,
|
22
22
|
scanners: [
|
23
23
|
['::I18n::Tasks::Scanners::RubyAstScanner', { only: %w[*.rb] }],
|
24
|
-
['::I18n::Tasks::Scanners::ErbAstScanner', { only: %w[*.erb] }],
|
25
|
-
['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.erb *.rb] }]
|
24
|
+
['::I18n::Tasks::Scanners::ErbAstScanner', { only: %w[*.html.erb] }],
|
25
|
+
['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.html.erb *.rb] }]
|
26
26
|
],
|
27
27
|
ast_matchers: [],
|
28
28
|
strict: true
|
data/lib/i18n/tasks/version.rb
CHANGED
@@ -13,7 +13,7 @@ data:
|
|
13
13
|
## Provide a custom adapter:
|
14
14
|
# adapter: I18n::Tasks::Data::FileSystem
|
15
15
|
|
16
|
-
# Locale files or `
|
16
|
+
# Locale files or `Dir.glob` patterns where translations are read from:
|
17
17
|
read:
|
18
18
|
## Default:
|
19
19
|
# - config/locales/%{locale}.yml
|
@@ -92,9 +92,13 @@ search:
|
|
92
92
|
## - RailsModelMatcher
|
93
93
|
## Matches ActiveRecord translations like
|
94
94
|
## User.human_attribute_name(:email) and User.model_name.human
|
95
|
+
## - DefaultI18nSubjectMatcher
|
96
|
+
## Matches ActionMailer's default_i18n_subject method
|
95
97
|
##
|
96
98
|
## To implement your own, please see `I18n::Tasks::Scanners::AstMatchers::BaseMatcher`.
|
97
|
-
#
|
99
|
+
# ast_matchers:
|
100
|
+
# - 'I18n::Tasks::Scanners::AstMatchers::RailsModelMatcher'
|
101
|
+
# - 'I18n::Tasks::Scanners::AstMatchers::DefaultI18nSubjectMatcher'
|
98
102
|
|
99
103
|
## Multiple scanners can be used. Their results are merged.
|
100
104
|
## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
|
@@ -110,9 +114,28 @@ search:
|
|
110
114
|
# deepl_api_key: "48E92789-57A3-466A-9959-1A1A1A1A1A1A"
|
111
115
|
# # deepl_host: "https://api.deepl.com"
|
112
116
|
# # deepl_version: "v2"
|
117
|
+
# # deepl_glossary_ids:
|
118
|
+
# # - f28106eb-0e06-489e-82c6-8215d6f95089
|
119
|
+
# # - 2c6415be-1852-4f54-9e1b-d800463496b4
|
113
120
|
# # add additional options to the DeepL.translate call: https://www.deepl.com/docs-api/translate-text/translate-text/
|
114
121
|
# deepl_options:
|
115
122
|
# formality: prefer_less
|
123
|
+
# # OpenAI
|
124
|
+
# openai_api_key: "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
125
|
+
# # openai_model: "gpt-3.5-turbo" # see https://platform.openai.com/docs/models
|
126
|
+
# # may contain `%{from}` and `%{to}`, which will be replaced by source and target locale codes, respectively (using `Kernel.format`)
|
127
|
+
# # openai_system_prompt: >-
|
128
|
+
# # You are a professional translator that translates content from the %{from} locale
|
129
|
+
# # to the %{to} locale in an i18n locale array.
|
130
|
+
# #
|
131
|
+
# # The array has a structured format and contains multiple strings. Your task is to translate
|
132
|
+
# # each of these strings and create a new array with the translated strings.
|
133
|
+
# #
|
134
|
+
# # HTML markups (enclosed in < and > characters) must not be changed under any circumstance.
|
135
|
+
# # Variables (starting with %%{ and ending with }) must not be changed under any circumstance.
|
136
|
+
# #
|
137
|
+
# # Keep in mind the context of all the strings for a more accurate translation.
|
138
|
+
|
116
139
|
## Do not consider these keys missing:
|
117
140
|
# ignore_missing:
|
118
141
|
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: i18n-tasks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- glebm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -38,26 +38,6 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 2.1.0
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: better_html
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.0'
|
48
|
-
- - "<"
|
49
|
-
- !ruby/object:Gem::Version
|
50
|
-
version: '3.0'
|
51
|
-
type: :runtime
|
52
|
-
prerelease: false
|
53
|
-
version_requirements: !ruby/object:Gem::Requirement
|
54
|
-
requirements:
|
55
|
-
- - ">="
|
56
|
-
- !ruby/object:Gem::Version
|
57
|
-
version: '1.0'
|
58
|
-
- - "<"
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '3.0'
|
61
41
|
- !ruby/object:Gem::Dependency
|
62
42
|
name: erubi
|
63
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -388,6 +368,7 @@ files:
|
|
388
368
|
- lib/i18n/tasks/data/file_system.rb
|
389
369
|
- lib/i18n/tasks/data/file_system_base.rb
|
390
370
|
- lib/i18n/tasks/data/router/conservative_router.rb
|
371
|
+
- lib/i18n/tasks/data/router/isolating_router.rb
|
391
372
|
- lib/i18n/tasks/data/router/pattern_router.rb
|
392
373
|
- lib/i18n/tasks/data/tree/node.rb
|
393
374
|
- lib/i18n/tasks/data/tree/nodes.rb
|
@@ -406,9 +387,9 @@ files:
|
|
406
387
|
- lib/i18n/tasks/reports/base.rb
|
407
388
|
- lib/i18n/tasks/reports/terminal.rb
|
408
389
|
- lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb
|
390
|
+
- lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb
|
409
391
|
- lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb
|
410
392
|
- lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb
|
411
|
-
- lib/i18n/tasks/scanners/erb_ast_processor.rb
|
412
393
|
- lib/i18n/tasks/scanners/erb_ast_scanner.rb
|
413
394
|
- lib/i18n/tasks/scanners/file_scanner.rb
|
414
395
|
- lib/i18n/tasks/scanners/files/caching_file_finder.rb
|
@@ -474,7 +455,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
474
455
|
- !ruby/object:Gem::Version
|
475
456
|
version: '0'
|
476
457
|
requirements: []
|
477
|
-
rubygems_version: 3.
|
458
|
+
rubygems_version: 3.5.3
|
478
459
|
signing_key:
|
479
460
|
specification_version: 4
|
480
461
|
summary: Manage localization and translation with the awesome power of static analysis
|
@@ -1,74 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'ast'
|
4
|
-
require 'set'
|
5
|
-
require 'i18n/tasks/scanners/local_ruby_parser'
|
6
|
-
|
7
|
-
module I18n::Tasks::Scanners
|
8
|
-
class ErbAstProcessor
|
9
|
-
include AST::Processor::Mixin
|
10
|
-
def initialize
|
11
|
-
super()
|
12
|
-
@ruby_parser = LocalRubyParser.new(ignore_blocks: true)
|
13
|
-
@comments = []
|
14
|
-
end
|
15
|
-
|
16
|
-
def process_and_extract_comments(ast)
|
17
|
-
result = process(ast)
|
18
|
-
[result, @comments]
|
19
|
-
end
|
20
|
-
|
21
|
-
def on_code(node)
|
22
|
-
parsed, comments = @ruby_parser.parse(
|
23
|
-
node.children[0],
|
24
|
-
location: node.location
|
25
|
-
)
|
26
|
-
@comments.concat(comments)
|
27
|
-
|
28
|
-
unless parsed.nil?
|
29
|
-
parsed = parsed.updated(
|
30
|
-
nil,
|
31
|
-
parsed.children.map { |child| node?(child) ? process(child) : child }
|
32
|
-
)
|
33
|
-
node = node.updated(:send, parsed)
|
34
|
-
end
|
35
|
-
node
|
36
|
-
end
|
37
|
-
|
38
|
-
# @param node [::Parser::AST::Node]
|
39
|
-
# @return [::Parser::AST::Node]
|
40
|
-
def handler_missing(node)
|
41
|
-
node = handle_comment(node)
|
42
|
-
return if node.nil?
|
43
|
-
|
44
|
-
node.updated(
|
45
|
-
nil,
|
46
|
-
node.children.map { |child| node?(child) ? process(child) : child }
|
47
|
-
)
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
# Convert ERB-comments to ::Parser::Source::Comment and skip processing node
|
53
|
-
#
|
54
|
-
# @param node Parser::AST::Node Potential comment node
|
55
|
-
# @return Parser::AST::Node or nil
|
56
|
-
def handle_comment(node)
|
57
|
-
if node.type == :erb && node.children.size == 4 &&
|
58
|
-
node.children[0]&.type == :indicator && node.children[0].children[0] == '#' &&
|
59
|
-
node.children[2]&.type == :code
|
60
|
-
|
61
|
-
# Do not continue parsing this node
|
62
|
-
comment = node.children[2]
|
63
|
-
@comments << ::Parser::Source::Comment.new(comment.location.expression)
|
64
|
-
return
|
65
|
-
end
|
66
|
-
|
67
|
-
node
|
68
|
-
end
|
69
|
-
|
70
|
-
def node?(node)
|
71
|
-
node.is_a?(::Parser::AST::Node)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|