i18n-tasks 0.9.21 → 0.9.22
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 +5 -5
- data/README.md +27 -6
- data/config/locales/en.yml +9 -2
- data/config/locales/ru.yml +9 -2
- data/i18n-tasks.gemspec +4 -5
- data/lib/i18n/tasks/base_task.rb +2 -0
- data/lib/i18n/tasks/base_translator.rb +148 -0
- data/lib/i18n/tasks/command/commands/missing.rb +7 -2
- data/lib/i18n/tasks/command/options/locales.rb +8 -0
- data/lib/i18n/tasks/configuration.rb +2 -1
- data/lib/i18n/tasks/data/tree/traversal.rb +1 -1
- data/lib/i18n/tasks/deepl_translation.rb +67 -0
- data/lib/i18n/tasks/google_translation.rb +36 -94
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +1 -1
- data/lib/i18n/tasks/version.rb +1 -1
- data/templates/config/i18n-tasks.yml +6 -2
- metadata +27 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 84b3720d5321c4996a0cebe6b17a734445434d5aa549ebcc6771c09e1856bf6b
|
4
|
+
data.tar.gz: d966fc1e803e5cf34fe1dc61ddd37a10f56f5457b2749142511a38fb377e38ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f90d7edb7ca9f661d0d5f8d36f25a19c14d58fdac1a540389f1b3e19c01e12835df8d1478619b8a45bddb42fd4852c91ea59b4a92c0f66ed696432202b331a9
|
7
|
+
data.tar.gz: 1407da83477f63875c56708aef038ac96d005897e5457b990bcc195d8738dbe1a857490939c2e7530cdb41380374c5291a3b55892cd80442af6ad3eda695a197
|
data/README.md
CHANGED
@@ -7,7 +7,7 @@ i18n-tasks helps you find and manage missing and unused translations.
|
|
7
7
|
This gem analyses code statically for key usages, such as `I18n.t('some.key')`, in order to:
|
8
8
|
|
9
9
|
* Report keys that are missing or unused.
|
10
|
-
* Pre-fill missing keys, optionally from Google Translate.
|
10
|
+
* Pre-fill missing keys, optionally from Google Translate or DeepL Pro.
|
11
11
|
* Remove unused keys.
|
12
12
|
|
13
13
|
Thus addressing the two main problems of [i18n gem][i18n-gem] design:
|
@@ -22,7 +22,7 @@ i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (def
|
|
22
22
|
Add i18n-tasks to the Gemfile:
|
23
23
|
|
24
24
|
```ruby
|
25
|
-
gem 'i18n-tasks', '~> 0.9.
|
25
|
+
gem 'i18n-tasks', '~> 0.9.22'
|
26
26
|
```
|
27
27
|
|
28
28
|
Copy the default [configuration file](#configuration):
|
@@ -83,7 +83,7 @@ Usage: i18n-tasks add-missing [options] [locale ...]
|
|
83
83
|
|
84
84
|
### Google Translate missing keys
|
85
85
|
|
86
|
-
Translate missing values with Google Translate ([more below on the API key](#translation-config)).
|
86
|
+
Translate missing values with Google Translate ([more below on the API key](#google-translation-config)).
|
87
87
|
|
88
88
|
```console
|
89
89
|
$ i18n-tasks translate-missing
|
@@ -91,6 +91,16 @@ $ i18n-tasks translate-missing
|
|
91
91
|
$ i18n-tasks translate-missing --from base es fr
|
92
92
|
```
|
93
93
|
|
94
|
+
### DeepL Pro Translate missing keys
|
95
|
+
|
96
|
+
Translate missing values with DeepL Pro Translate ([more below on the API key](#deepl-translation-config)).
|
97
|
+
|
98
|
+
```console
|
99
|
+
$ i18n-tasks translate-missing
|
100
|
+
# accepts from and locales options:
|
101
|
+
$ i18n-tasks translate-missing --backend deepl --from en
|
102
|
+
```
|
103
|
+
|
94
104
|
### Find usages
|
95
105
|
|
96
106
|
See where the keys are used with `i18n-tasks find`:
|
@@ -295,7 +305,7 @@ data:
|
|
295
305
|
- 'config/locales/%{locale}.yml'
|
296
306
|
```
|
297
307
|
|
298
|
-
If you want to have i18n-tasks reorganize your existing keys using `data.write`, either set the router to
|
308
|
+
If you want to have i18n-tasks reorganize your existing keys using `data.write`, either set the router to
|
299
309
|
`pattern_router` as above, or run `i18n-tasks normalize -p` (forcing the use of the pattern router for that run).
|
300
310
|
|
301
311
|
##### Key pattern syntax
|
@@ -340,7 +350,7 @@ For more complex cases, you can implement a [custom scanner][custom-scanner-docs
|
|
340
350
|
|
341
351
|
See the [config file][config] to find out more.
|
342
352
|
|
343
|
-
<a name="translation-config"></a>
|
353
|
+
<a name="google-translation-config"></a>
|
344
354
|
### Google Translate
|
345
355
|
|
346
356
|
`i18n-tasks translate-missing` requires a Google Translate API key, get it at [Google API Console](https://code.google.com/apis/console).
|
@@ -357,7 +367,18 @@ Put the key in `GOOGLE_TRANSLATE_API_KEY` environment variable or in the config
|
|
357
367
|
```yaml
|
358
368
|
# config/i18n-tasks.yml
|
359
369
|
translation:
|
360
|
-
|
370
|
+
google_translate_api_key: <Google Translate API key>
|
371
|
+
```
|
372
|
+
|
373
|
+
<a name="deepl-translation-config"></a>
|
374
|
+
### DeepL Pro Translate
|
375
|
+
|
376
|
+
`i18n-tasks translate-missing` requires a DeepL Pro API key, get it at [DeepL](https://www.deepl.com/pro).
|
377
|
+
|
378
|
+
```yaml
|
379
|
+
# config/i18n-tasks.yml
|
380
|
+
translation:
|
381
|
+
deepl_api_key: <Deep Pro API key>
|
361
382
|
```
|
362
383
|
|
363
384
|
## Interactive console
|
data/config/locales/en.yml
CHANGED
@@ -26,6 +26,7 @@ en:
|
|
26
26
|
strict: >-
|
27
27
|
Avoid inferring dynamic key usages such as t("cats.#{cat}.name"). Takes precedence over
|
28
28
|
the config setting if set.
|
29
|
+
translation_backend: Translation backend (google or deepl)
|
29
30
|
value: >-
|
30
31
|
Value. Interpolates: %{value}, %{human_key}, %{key}, %{default}, %{value_or_human_key},
|
31
32
|
%{value_or_default_or_human_key}
|
@@ -47,7 +48,7 @@ en:
|
|
47
48
|
normalize: 'normalize translation data: sort and move to the right files'
|
48
49
|
remove_unused: remove unused keys
|
49
50
|
rm: remove the keys in locale data that match the given pattern
|
50
|
-
translate_missing: translate missing keys with Google Translate
|
51
|
+
translate_missing: translate missing keys with Google Translate or DeepL Pro
|
51
52
|
tree_convert: convert tree between formats
|
52
53
|
tree_filter: filter tree by key pattern
|
53
54
|
tree_merge: merge trees
|
@@ -89,10 +90,16 @@ en:
|
|
89
90
|
has %{key_count} keys in total. On average, values are %{value_chars_avg} characters long,
|
90
91
|
keys have %{key_segments_avg} segments.
|
91
92
|
title: Forest (%{locales})
|
93
|
+
deepl_translate:
|
94
|
+
errors:
|
95
|
+
no_api_key: >-
|
96
|
+
Setup DeepL Pro API key via DEEPL_AUTH_KEY environment variable or translation.deepl_api_key
|
97
|
+
in config/i18n-tasks.yml. Get the key at https://www.deepl.com/pro.
|
98
|
+
no_results: DeepL returned no results.
|
92
99
|
google_translate:
|
93
100
|
errors:
|
94
101
|
no_api_key: >-
|
95
|
-
Set Google API key via GOOGLE_TRANSLATE_API_KEY environment variable or translation.
|
102
|
+
Set Google API key via GOOGLE_TRANSLATE_API_KEY environment variable or translation.google_translate_api_key
|
96
103
|
in config/i18n-tasks.yml. Get the key at https://code.google.com/apis/console.
|
97
104
|
no_results: >-
|
98
105
|
Google Translate returned no results. Make sure billing information is set at https://code.google.com/apis/console.
|
data/config/locales/ru.yml
CHANGED
@@ -23,6 +23,7 @@ ru:
|
|
23
23
|
out_format: 'Формат вывода: %{valid_text}. %{default_text}.'
|
24
24
|
pattern_router: 'Использовать pattern_router: ключи распределятся по файлам согласно data.write'
|
25
25
|
strict: Не угадывать динамические использования ключей, например `t("category.#{category.key}")`
|
26
|
+
translation_backend: Движок перевода (google или deepl)
|
26
27
|
value: >-
|
27
28
|
Значение, интерполируется с %{value}, %{human_key}, %{key}, %{default}, %{value_or_human_key},
|
28
29
|
%{value_or_default_or_human_key}
|
@@ -44,7 +45,7 @@ ru:
|
|
44
45
|
normalize: нормализовать файлы переводов (сортировка и распределение)
|
45
46
|
remove_unused: удалить неиспользуемые ключи
|
46
47
|
rm: удалить ключи, которые соответствуют заданному шаблону
|
47
|
-
translate_missing: перевести недостающие переводы с Google Translate
|
48
|
+
translate_missing: перевести недостающие переводы с Google Translate / DeepL Pro
|
48
49
|
tree_convert: преобразовать дерево между форматами
|
49
50
|
tree_filter: фильтровать дерево по ключу
|
50
51
|
tree_merge: объединенить деревья
|
@@ -85,10 +86,16 @@ ru:
|
|
85
86
|
text_single_locale: >-
|
86
87
|
%{key_count} ключей. В среднем, длина строки: %{value_chars_avg}, сегменты ключей: %{key_segments_avg}.
|
87
88
|
title: 'Данные (%{locales}):'
|
89
|
+
deepl_translate:
|
90
|
+
errors:
|
91
|
+
no_api_key: >-
|
92
|
+
Задайте ключ API DeepL через переменную окружения DEEPL_AUTH_KEY или translation.deepl_api_key
|
93
|
+
Получите ключ через https://www.deepl.com/pro.
|
94
|
+
no_results: DeepL не дал результатов.
|
88
95
|
google_translate:
|
89
96
|
errors:
|
90
97
|
no_api_key: >-
|
91
|
-
Задайте ключ API Google через переменную окружения GOOGLE_TRANSLATE_API_KEY или translation.
|
98
|
+
Задайте ключ API Google через переменную окружения GOOGLE_TRANSLATE_API_KEY или translation.google_translate_api_key
|
92
99
|
в config/i18n-tasks.yml. Получите ключ через https://code.google.com/apis/console.
|
93
100
|
no_results: >-
|
94
101
|
Google Translate не дал результатов. Убедитесь в том, что платежная информация добавлена
|
data/i18n-tasks.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
lib = File.expand_path('
|
3
|
+
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
|
|
@@ -37,6 +37,7 @@ TEXT
|
|
37
37
|
|
38
38
|
s.add_dependency 'activesupport', '>= 4.0.2'
|
39
39
|
s.add_dependency 'ast', '>= 2.1.0'
|
40
|
+
s.add_dependency 'deepl-rb', '>= 2.1.0'
|
40
41
|
s.add_dependency 'easy_translate', '>= 0.5.1'
|
41
42
|
s.add_dependency 'erubi'
|
42
43
|
s.add_dependency 'highline', '>= 1.7.3'
|
@@ -48,9 +49,7 @@ TEXT
|
|
48
49
|
s.add_development_dependency 'bundler', '~> 1.3'
|
49
50
|
s.add_development_dependency 'rake'
|
50
51
|
s.add_development_dependency 'rspec', '~> 3.3'
|
51
|
-
s.add_development_dependency 'rubocop'
|
52
|
-
|
53
|
-
# https://github.com/codeclimate/ruby-test-reporter/pull/181
|
54
|
-
s.add_development_dependency 'simplecov', '~> 0.13.0'
|
52
|
+
s.add_development_dependency 'rubocop', '~> 0.53.0'
|
53
|
+
s.add_development_dependency 'simplecov'
|
55
54
|
s.add_development_dependency 'yard'
|
56
55
|
end
|
data/lib/i18n/tasks/base_task.rb
CHANGED
@@ -11,6 +11,7 @@ require 'i18n/tasks/used_keys'
|
|
11
11
|
require 'i18n/tasks/ignore_keys'
|
12
12
|
require 'i18n/tasks/missing_keys'
|
13
13
|
require 'i18n/tasks/unused_keys'
|
14
|
+
require 'i18n/tasks/deepl_translation'
|
14
15
|
require 'i18n/tasks/google_translation'
|
15
16
|
require 'i18n/tasks/locale_pathname'
|
16
17
|
require 'i18n/tasks/locale_list'
|
@@ -31,6 +32,7 @@ module I18n
|
|
31
32
|
include IgnoreKeys
|
32
33
|
include MissingKeys
|
33
34
|
include UnusedKeys
|
35
|
+
include DeeplTranslation
|
34
36
|
include GoogleTranslation
|
35
37
|
include Logging
|
36
38
|
include Configuration
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n::Tasks
|
4
|
+
class BaseTranslator
|
5
|
+
# @param [I18n::Tasks::BaseTask] i18n_tasks
|
6
|
+
def initialize(i18n_tasks)
|
7
|
+
@i18n_tasks = i18n_tasks
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param [I18n::Tasks::Tree::Siblings] forest to translate to the locales of its root nodes
|
11
|
+
# @param [String] from locale
|
12
|
+
# @return [I18n::Tasks::Tree::Siblings] translated forest
|
13
|
+
def translate_forest(forest, from)
|
14
|
+
forest.inject @i18n_tasks.empty_forest do |result, root|
|
15
|
+
translated = translate_pairs(root.key_values(root: true), to: root.key, from: from)
|
16
|
+
result.merge! Data::Tree::Siblings.from_flat_pairs(translated)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
# @param [Array<[String, Object]>] list of key-value pairs
|
23
|
+
# @return [Array<[String, Object]>] translated list
|
24
|
+
def translate_pairs(list, opts)
|
25
|
+
return [] if list.empty?
|
26
|
+
opts = opts.dup
|
27
|
+
key_pos = list.each_with_index.inject({}) { |idx, ((k, _v), i)| idx.update(k => i) }
|
28
|
+
# copy reference keys as is, instead of translating
|
29
|
+
reference_key_vals = list.select { |_k, v| v.is_a? Symbol } || []
|
30
|
+
list -= reference_key_vals
|
31
|
+
result = list.group_by { |k_v| @i18n_tasks.html_key? k_v[0], opts[:from] }.map do |is_html, list_slice|
|
32
|
+
fetch_translations list_slice, opts.merge(is_html ? options_for_html : options_for_plain)
|
33
|
+
end.reduce(:+) || []
|
34
|
+
result.concat(reference_key_vals)
|
35
|
+
result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] }
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [Array<[String, Object]>] list of key-value pairs
|
40
|
+
# @return [Array<[String, Object]>] translated list
|
41
|
+
def fetch_translations(list, opts)
|
42
|
+
from_values(list, translate_values(to_values(list), **options_for_translate_values(**opts))).tap do |result|
|
43
|
+
fail CommandError, no_results_error_message if result.blank?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param [Array<[String, Object]>] list of key-value pairs
|
48
|
+
# @return [Array<String>] values for translation extracted from list
|
49
|
+
def to_values(list)
|
50
|
+
list.map { |l| dump_value l[1] }.flatten.compact
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Array<[String, Object]>] list
|
54
|
+
# @param [Array<String>] translated_values
|
55
|
+
# @return [Array<[String, Object]>] translated key-value pairs
|
56
|
+
def from_values(list, translated_values)
|
57
|
+
keys = list.map(&:first)
|
58
|
+
untranslated_values = list.map(&:last)
|
59
|
+
keys.zip parse_value(untranslated_values, translated_values.to_enum)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Prepare value for translation.
|
63
|
+
# @return [String, Array<String, nil>, nil] value for Google Translate or nil for non-string values
|
64
|
+
def dump_value(value)
|
65
|
+
case value
|
66
|
+
when Array
|
67
|
+
# dump recursively
|
68
|
+
value.map { |v| dump_value v }
|
69
|
+
when String
|
70
|
+
replace_interpolations value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parse translated value from the each_translated enumerator
|
75
|
+
# @param [Object] untranslated
|
76
|
+
# @param [Enumerator] each_translated
|
77
|
+
# @return [Object] final translated value
|
78
|
+
def parse_value(untranslated, each_translated)
|
79
|
+
case untranslated
|
80
|
+
when Array
|
81
|
+
# implode array
|
82
|
+
untranslated.map { |from| parse_value(from, each_translated) }
|
83
|
+
when String
|
84
|
+
restore_interpolations untranslated, each_translated.next
|
85
|
+
else
|
86
|
+
untranslated
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
INTERPOLATION_KEY_RE = /%\{[^}]+}/
|
91
|
+
UNTRANSLATABLE_STRING = 'zxzxzx'
|
92
|
+
|
93
|
+
# @param [String] value
|
94
|
+
# @return [String] 'hello, %{name}' => 'hello, <round-trippable string>'
|
95
|
+
def replace_interpolations(value)
|
96
|
+
i = -1
|
97
|
+
value.gsub INTERPOLATION_KEY_RE do
|
98
|
+
i += 1
|
99
|
+
"#{UNTRANSLATABLE_STRING}#{i}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param [String] untranslated
|
104
|
+
# @param [String] translated
|
105
|
+
# @return [String] 'hello, <round-trippable string>' => 'hello, %{name}'
|
106
|
+
def restore_interpolations(untranslated, translated)
|
107
|
+
return translated if untranslated !~ INTERPOLATION_KEY_RE
|
108
|
+
values = untranslated.scan(INTERPOLATION_KEY_RE)
|
109
|
+
translated.gsub(/#{Regexp.escape(UNTRANSLATABLE_STRING)}\d+/i) do |m|
|
110
|
+
values[m[UNTRANSLATABLE_STRING.length..-1].to_i]
|
111
|
+
end
|
112
|
+
rescue StandardError => e
|
113
|
+
raise_interpolation_error(untranslated, translated, e)
|
114
|
+
end
|
115
|
+
|
116
|
+
def raise_interpolation_error(untranslated, translated, e)
|
117
|
+
fail CommandError.new(e, <<-TEXT.strip)
|
118
|
+
Error when restoring interpolations:
|
119
|
+
original: "#{untranslated}"
|
120
|
+
response: "#{translated}"
|
121
|
+
error: #{e.message} (#{e.class.name})
|
122
|
+
TEXT
|
123
|
+
end
|
124
|
+
|
125
|
+
# @param [Array<String>] list
|
126
|
+
# @param [Hash] options
|
127
|
+
# @return [Array<String>]
|
128
|
+
# @abstract
|
129
|
+
def translate_values(list, **options); end
|
130
|
+
|
131
|
+
# @param [Hash] options
|
132
|
+
# @return [Hash]
|
133
|
+
# @abstract
|
134
|
+
def options_for_translate_values(options); end
|
135
|
+
|
136
|
+
# @return [Hash]
|
137
|
+
# @abstract
|
138
|
+
def options_for_html; end
|
139
|
+
|
140
|
+
# @return [Hash]
|
141
|
+
# @abstract
|
142
|
+
def options_for_plain; end
|
143
|
+
|
144
|
+
# @return [String]
|
145
|
+
# @abstract
|
146
|
+
def no_results_error_message; end
|
147
|
+
end
|
148
|
+
end
|
@@ -36,11 +36,16 @@ module I18n::Tasks
|
|
36
36
|
cmd :translate_missing,
|
37
37
|
pos: '[locale ...]',
|
38
38
|
desc: t('i18n_tasks.cmd.desc.translate_missing'),
|
39
|
-
args: [:locales, :locale_to_translate_from, arg(:out_format).from(1)]
|
39
|
+
args: [:locales, :locale_to_translate_from, arg(:out_format).from(1), :translation_backend]
|
40
40
|
|
41
41
|
def translate_missing(opt = {})
|
42
42
|
missing = i18n.missing_diff_forest opt[:locales], opt[:from]
|
43
|
-
translated =
|
43
|
+
translated = case opt[:backend]
|
44
|
+
when 'deepl'
|
45
|
+
i18n.deepl_translate_forest missing, opt[:from]
|
46
|
+
when 'google'
|
47
|
+
i18n.google_translate_forest missing, opt[:from]
|
48
|
+
end
|
44
49
|
i18n.data.merge! translated
|
45
50
|
log_stderr t('i18n_tasks.translate_missing.translated', count: translated.leaves.count)
|
46
51
|
print_forest translated, opt
|
@@ -30,6 +30,14 @@ module I18n::Tasks
|
|
30
30
|
t('i18n_tasks.cmd.args.desc.locale_to_translate_from'),
|
31
31
|
parser: OptionParsers::Locale::Parser,
|
32
32
|
default: 'base'
|
33
|
+
|
34
|
+
TRANSLATION_BACKENDS = %w[google deepl].freeze
|
35
|
+
arg :translation_backend,
|
36
|
+
'-b',
|
37
|
+
'--backend BACKEND',
|
38
|
+
t('i18n_tasks.cmd.args.desc.translation_backend'),
|
39
|
+
parser: OptionParsers::Locale::Parser,
|
40
|
+
default: TRANSLATION_BACKENDS[0]
|
33
41
|
end
|
34
42
|
end
|
35
43
|
end
|
@@ -57,7 +57,8 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength
|
|
57
57
|
def translation_config
|
58
58
|
@config_sections[:translation] ||= begin
|
59
59
|
conf = (config[:translation] || {}).with_indifferent_access
|
60
|
-
conf[:
|
60
|
+
conf[:google_translate_api_key] = ENV['GOOGLE_TRANSLATE_API_KEY'] if ENV.key?('GOOGLE_TRANSLATE_API_KEY')
|
61
|
+
conf[:deepl_api_key] = ENV['DEEPL_AUTH_KEY'] if ENV.key?('DEEPL_AUTH_KEY')
|
61
62
|
conf
|
62
63
|
end
|
63
64
|
end
|
@@ -179,7 +179,7 @@ module I18n::Tasks
|
|
179
179
|
)
|
180
180
|
end
|
181
181
|
pattern_re = I18n::Tasks::KeyPatternMatching.compile_key_pattern(key_pattern) if key_pattern.present?
|
182
|
-
keys.each do |key, node|
|
182
|
+
keys.each do |key, node|
|
183
183
|
next if pattern_re && key !~ pattern_re
|
184
184
|
node.value = value_proc.call(node)
|
185
185
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deepl'
|
4
|
+
require 'i18n/tasks/html_keys'
|
5
|
+
require 'i18n/tasks/base_translator'
|
6
|
+
|
7
|
+
module I18n::Tasks
|
8
|
+
module DeeplTranslation
|
9
|
+
# @param [I18n::Tasks::Tree::Siblings] forest to translate to the locales of its root nodes
|
10
|
+
# @param [String] from locale
|
11
|
+
# @return [I18n::Tasks::Tree::Siblings] translated forest
|
12
|
+
def deepl_translate_forest(forest, from)
|
13
|
+
DeeplTranslator.new(self).translate_forest(forest, from)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class DeeplTranslator < BaseTranslator
|
18
|
+
def initialize(*)
|
19
|
+
super
|
20
|
+
configure_api_key!
|
21
|
+
end
|
22
|
+
|
23
|
+
def translate_values(list, from:, to:, **options)
|
24
|
+
DeepL.translate(list, from, to, options).map(&:text)
|
25
|
+
end
|
26
|
+
|
27
|
+
def options_for_translate_values(**options)
|
28
|
+
{ ignore_tags: %w[i18n] }.merge(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def options_for_html
|
32
|
+
{ tag_handling: 'xml' }
|
33
|
+
end
|
34
|
+
|
35
|
+
def options_for_plain
|
36
|
+
{ preserve_formatting: true }
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [String] value
|
40
|
+
# @return [String] 'hello, %{name}' => 'hello, <i18n>%{name}</i18n>'
|
41
|
+
def replace_interpolations(value)
|
42
|
+
value.gsub(INTERPOLATION_KEY_RE, '<i18n>\0</i18n>')
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param [String] untranslated
|
46
|
+
# @param [String] translated
|
47
|
+
# @return [String] 'hello, <i18n>%{name}</i18n>' => 'hello, %{name}'
|
48
|
+
def restore_interpolations(untranslated, translated)
|
49
|
+
return translated if untranslated !~ INTERPOLATION_KEY_RE
|
50
|
+
translated.gsub(%r{<\/?i18n>}, '')
|
51
|
+
rescue StandardError => e
|
52
|
+
raise_interpolation_error(untranslated, translated, e)
|
53
|
+
end
|
54
|
+
|
55
|
+
def no_results_error_message
|
56
|
+
I18n.t('i18n_tasks.deepl_translate.errors.no_results')
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def configure_api_key!
|
62
|
+
api_key = @i18n_tasks.translation_config[:deepl_api_key]
|
63
|
+
fail CommandError, I18n.t('i18n_tasks.deepl_translate.errors.no_api_key') if api_key.blank?
|
64
|
+
DeepL.configure { |config| config.auth_key = api_key }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'easy_translate'
|
4
4
|
require 'i18n/tasks/html_keys'
|
5
|
+
require 'i18n/tasks/base_translator'
|
5
6
|
|
6
7
|
module I18n::Tasks
|
7
8
|
module GoogleTranslation
|
@@ -9,117 +10,58 @@ module I18n::Tasks
|
|
9
10
|
# @param [String] from locale
|
10
11
|
# @return [I18n::Tasks::Tree::Siblings] translated forest
|
11
12
|
def google_translate_forest(forest, from)
|
12
|
-
forest
|
13
|
-
translated = google_translate_list(root.key_values(root: true), to: root.key, from: from)
|
14
|
-
result.merge! Data::Tree::Siblings.from_flat_pairs(translated)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# @param [Array<[String, Object]>] list of key-value pairs
|
19
|
-
# @return [Array<[String, Object]>] translated list
|
20
|
-
def google_translate_list(list, opts) # rubocop:disable Metrics/AbcSize
|
21
|
-
return [] if list.empty?
|
22
|
-
opts = opts.dup
|
23
|
-
opts[:key] ||= translation_config[:api_key]
|
24
|
-
validate_google_translate_api_key! opts[:key]
|
25
|
-
key_pos = list.each_with_index.inject({}) { |idx, ((k, _v), i)| idx.update(k => i) }
|
26
|
-
# copy reference keys as is, instead of translating
|
27
|
-
reference_key_vals = list.select { |_k, v| v.is_a? Symbol } || []
|
28
|
-
list -= reference_key_vals
|
29
|
-
result = list.group_by { |k_v| html_key? k_v[0], opts[:from] }.map do |is_html, list_slice|
|
30
|
-
fetch_google_translations list_slice, opts.merge(is_html ? { html: true } : { format: 'text' })
|
31
|
-
end.reduce(:+) || []
|
32
|
-
result.concat(reference_key_vals)
|
33
|
-
result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] }
|
34
|
-
result
|
35
|
-
end
|
36
|
-
|
37
|
-
# @param [Array<[String, Object]>] list of key-value pairs
|
38
|
-
# @return [Array<[String, Object]>] translated list
|
39
|
-
def fetch_google_translations(list, opts)
|
40
|
-
from_values(list, EasyTranslate.translate(to_values(list), opts)).tap do |result|
|
41
|
-
fail CommandError, I18n.t('i18n_tasks.google_translate.errors.no_results') if result.blank?
|
42
|
-
end
|
13
|
+
GoogleTranslator.new(self).translate_forest(forest, from)
|
43
14
|
end
|
15
|
+
end
|
44
16
|
|
45
|
-
|
17
|
+
class GoogleTranslator < BaseTranslator
|
18
|
+
SUPPORTED_LOCALES_WITH_REGION = %w[zh-CN zh-TW].freeze
|
46
19
|
|
47
|
-
def
|
48
|
-
|
20
|
+
def translate_values(list, **options)
|
21
|
+
EasyTranslate.translate(list, options)
|
49
22
|
end
|
50
23
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
24
|
+
def options_for_translate_values(from:, to:, **options)
|
25
|
+
options.merge(
|
26
|
+
api_key: api_key,
|
27
|
+
from: to_google_translate_compatible_locale(from),
|
28
|
+
to: to_google_translate_compatible_locale(to)
|
29
|
+
)
|
55
30
|
end
|
56
31
|
|
57
|
-
|
58
|
-
|
59
|
-
# @return [Array<[String, Object]>] translated key-value pairs
|
60
|
-
def from_values(list, translated_values)
|
61
|
-
keys = list.map(&:first)
|
62
|
-
untranslated_values = list.map(&:last)
|
63
|
-
keys.zip parse_value(untranslated_values, translated_values.to_enum)
|
32
|
+
def options_for_html
|
33
|
+
{ html: true }
|
64
34
|
end
|
65
35
|
|
66
|
-
|
67
|
-
|
68
|
-
def dump_value(value)
|
69
|
-
case value
|
70
|
-
when Array
|
71
|
-
# dump recursively
|
72
|
-
value.map { |v| dump_value v }
|
73
|
-
when String
|
74
|
-
replace_interpolations value
|
75
|
-
end
|
36
|
+
def options_for_plain
|
37
|
+
{ format: 'text' }
|
76
38
|
end
|
77
39
|
|
78
|
-
|
79
|
-
|
80
|
-
# @param [Enumerator] each_translated
|
81
|
-
# @return [Object] final translated value
|
82
|
-
def parse_value(untranslated, each_translated)
|
83
|
-
case untranslated
|
84
|
-
when Array
|
85
|
-
# implode array
|
86
|
-
untranslated.map { |from| parse_value(from, each_translated) }
|
87
|
-
when String
|
88
|
-
restore_interpolations untranslated, each_translated.next
|
89
|
-
else
|
90
|
-
untranslated
|
91
|
-
end
|
40
|
+
def no_results_error_message
|
41
|
+
I18n.t('i18n_tasks.google_translate.errors.no_results')
|
92
42
|
end
|
93
43
|
|
94
|
-
|
95
|
-
UNTRANSLATABLE_STRING = 'zxzxzx'
|
44
|
+
private
|
96
45
|
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
value.gsub INTERPOLATION_KEY_RE do
|
102
|
-
i += 1
|
103
|
-
"#{UNTRANSLATABLE_STRING}#{i}"
|
104
|
-
end
|
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
|
+
locale.split('-', 2).first
|
105
50
|
end
|
106
51
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
52
|
+
def api_key
|
53
|
+
@api_key ||= begin
|
54
|
+
key = @i18n_tasks.translation_config[:google_translate_api_key]
|
55
|
+
# fallback with deprecation warning
|
56
|
+
if @i18n_tasks.translation_config[:api_key]
|
57
|
+
@i18n_tasks.warn_deprecated(
|
58
|
+
'Please rename Google Translate API Key from `api_key` to `google_translate_api_key`.'
|
59
|
+
)
|
60
|
+
key ||= translation_config[:api_key]
|
61
|
+
end
|
62
|
+
fail CommandError, I18n.t('i18n_tasks.google_translate.errors.no_api_key') if key.blank?
|
63
|
+
key
|
115
64
|
end
|
116
|
-
rescue StandardError => e
|
117
|
-
raise CommandError.new(e, <<-TEXT.strip)
|
118
|
-
Error when restoring interpolations:
|
119
|
-
original: "#{untranslated}"
|
120
|
-
response: "#{translated}"
|
121
|
-
error: #{e.message} (#{e.class.name})
|
122
|
-
TEXT
|
123
65
|
end
|
124
66
|
end
|
125
67
|
end
|
@@ -65,7 +65,7 @@ module I18n::Tasks::Scanners
|
|
65
65
|
re && re =~ line
|
66
66
|
end
|
67
67
|
|
68
|
-
VALID_KEY_RE_DYNAMIC = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/
|
68
|
+
VALID_KEY_RE_DYNAMIC = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/
|
69
69
|
|
70
70
|
def valid_key?(key)
|
71
71
|
if @config[:strict]
|
data/lib/i18n/tasks/version.rb
CHANGED
@@ -82,10 +82,14 @@ search:
|
|
82
82
|
## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
|
83
83
|
## See this example of a custom scanner: https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example
|
84
84
|
|
85
|
-
##
|
85
|
+
## Translation Services
|
86
86
|
# translation:
|
87
|
+
# # Google Translate
|
87
88
|
# # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate
|
88
|
-
#
|
89
|
+
# google_translate_api_key: "AbC-dEf5"
|
90
|
+
# # DeepL Pro Translate
|
91
|
+
# # Get an API key and subscription at https://www.deepl.com/pro to use DeepL Pro
|
92
|
+
# deepl_api_key: "48E92789-57A3-466A-9959-1A1A1A1A1A1A"
|
89
93
|
|
90
94
|
## Do not consider these keys missing:
|
91
95
|
# ignore_missing:
|
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: 0.9.
|
4
|
+
version: 0.9.22
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- glebm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-03
|
11
|
+
date: 2018-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 2.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: deepl-rb
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.1.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.1.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: easy_translate
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -202,30 +216,30 @@ dependencies:
|
|
202
216
|
name: rubocop
|
203
217
|
requirement: !ruby/object:Gem::Requirement
|
204
218
|
requirements:
|
205
|
-
- - "
|
219
|
+
- - "~>"
|
206
220
|
- !ruby/object:Gem::Version
|
207
|
-
version:
|
221
|
+
version: 0.53.0
|
208
222
|
type: :development
|
209
223
|
prerelease: false
|
210
224
|
version_requirements: !ruby/object:Gem::Requirement
|
211
225
|
requirements:
|
212
|
-
- - "
|
226
|
+
- - "~>"
|
213
227
|
- !ruby/object:Gem::Version
|
214
|
-
version:
|
228
|
+
version: 0.53.0
|
215
229
|
- !ruby/object:Gem::Dependency
|
216
230
|
name: simplecov
|
217
231
|
requirement: !ruby/object:Gem::Requirement
|
218
232
|
requirements:
|
219
|
-
- - "
|
233
|
+
- - ">="
|
220
234
|
- !ruby/object:Gem::Version
|
221
|
-
version: 0
|
235
|
+
version: '0'
|
222
236
|
type: :development
|
223
237
|
prerelease: false
|
224
238
|
version_requirements: !ruby/object:Gem::Requirement
|
225
239
|
requirements:
|
226
|
-
- - "
|
240
|
+
- - ">="
|
227
241
|
- !ruby/object:Gem::Version
|
228
|
-
version: 0
|
242
|
+
version: '0'
|
229
243
|
- !ruby/object:Gem::Dependency
|
230
244
|
name: yard
|
231
245
|
requirement: !ruby/object:Gem::Requirement
|
@@ -262,6 +276,7 @@ files:
|
|
262
276
|
- i18n-tasks.gemspec
|
263
277
|
- lib/i18n/tasks.rb
|
264
278
|
- lib/i18n/tasks/base_task.rb
|
279
|
+
- lib/i18n/tasks/base_translator.rb
|
265
280
|
- lib/i18n/tasks/cli.rb
|
266
281
|
- lib/i18n/tasks/command/collection.rb
|
267
282
|
- lib/i18n/tasks/command/commander.rb
|
@@ -295,6 +310,7 @@ files:
|
|
295
310
|
- lib/i18n/tasks/data/tree/nodes.rb
|
296
311
|
- lib/i18n/tasks/data/tree/siblings.rb
|
297
312
|
- lib/i18n/tasks/data/tree/traversal.rb
|
313
|
+
- lib/i18n/tasks/deepl_translation.rb
|
298
314
|
- lib/i18n/tasks/google_translation.rb
|
299
315
|
- lib/i18n/tasks/html_keys.rb
|
300
316
|
- lib/i18n/tasks/ignore_keys.rb
|
@@ -361,7 +377,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
361
377
|
version: '0'
|
362
378
|
requirements: []
|
363
379
|
rubyforge_project:
|
364
|
-
rubygems_version: 2.
|
380
|
+
rubygems_version: 2.7.7
|
365
381
|
signing_key:
|
366
382
|
specification_version: 4
|
367
383
|
summary: Manage localization and translation with the awesome power of static analysis
|