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