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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d1ad82cbc7ebc20ec9373f5061a16cd99f1f9ae8
4
- data.tar.gz: e2804767ad2cecf501e3797b5aadbec8b5be4817
2
+ SHA256:
3
+ metadata.gz: 84b3720d5321c4996a0cebe6b17a734445434d5aa549ebcc6771c09e1856bf6b
4
+ data.tar.gz: d966fc1e803e5cf34fe1dc61ddd37a10f56f5457b2749142511a38fb377e38ba
5
5
  SHA512:
6
- metadata.gz: 6db5a8794c92840f1714bd1e929e4b955e19a05b23de45c8ebc760498d084d687e1702a332a584d215b619b9a36900bba192f16e1f31e936c7eb29a055a88c74
7
- data.tar.gz: 6258135c86f6305493acea59754646426af11a1c1e2c657c4c95ea4010d80b24aad4276826ff69acfb52e4092fbcb346a01a57de135cc8df5d63d150a08f469a
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.21'
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
- api_key: <Google Translate API key>
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
@@ -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.api_key
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.
@@ -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.api_key
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 не дал результатов. Убедитесь в том, что платежная информация добавлена
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
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
- # Newer versions of SimpleCov do not work with the codeclimate-test-reporter gem, see:
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
@@ -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 = i18n.google_translate_forest missing, opt[:from]
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[:api_key] ||= ENV['GOOGLE_TRANSLATE_API_KEY'] if ENV.key?('GOOGLE_TRANSLATE_API_KEY')
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| # rubocop:disable Performance/HashEachMethods
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.inject empty_forest do |result, root|
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
- private
17
+ class GoogleTranslator < BaseTranslator
18
+ SUPPORTED_LOCALES_WITH_REGION = %w[zh-CN zh-TW].freeze
46
19
 
47
- def validate_google_translate_api_key!(key)
48
- fail CommandError, I18n.t('i18n_tasks.google_translate.errors.no_api_key') if key.blank?
20
+ def translate_values(list, **options)
21
+ EasyTranslate.translate(list, options)
49
22
  end
50
23
 
51
- # @param [Array<[String, Object]>] list of key-value pairs
52
- # @return [Array<String>] values for translation extracted from list
53
- def to_values(list)
54
- list.map { |l| dump_value l[1] }.flatten.compact
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
- # @param [Array<[String, Object]>] list
58
- # @param [Array<String>] translated_values
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
- # Prepare value for translation.
67
- # @return [String, Array<String, nil>, nil] value for Google Translate or nil for non-string values
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
- # Parse translated value from the each_translated enumerator
79
- # @param [Object] untranslated
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
- INTERPOLATION_KEY_RE = /%\{[^}]+}/
95
- UNTRANSLATABLE_STRING = 'zxzxzx'
44
+ private
96
45
 
97
- # @param [String] value
98
- # @return [String] 'hello, %{name}' => 'hello, <round-trippable string>'
99
- def replace_interpolations(value)
100
- i = -1
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
- # @param [String] untranslated
108
- # @param [String] translated
109
- # @return [String] 'hello, <round-trippable string>' => 'hello, %{name}'
110
- def restore_interpolations(untranslated, translated)
111
- return translated if untranslated !~ INTERPOLATION_KEY_RE
112
- values = untranslated.scan(INTERPOLATION_KEY_RE)
113
- translated.gsub(/#{Regexp.escape(UNTRANSLATABLE_STRING)}\d+/i) do |m|
114
- values[m[UNTRANSLATABLE_STRING.length..-1].to_i]
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}|[:\#{@}\[\]])+$/ # rubocop:disable Lint/InterpolationCheck
68
+ VALID_KEY_RE_DYNAMIC = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/
69
69
 
70
70
  def valid_key?(key)
71
71
  if @config[:strict]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module I18n
4
4
  module Tasks
5
- VERSION = '0.9.21'
5
+ VERSION = '0.9.22'
6
6
  end
7
7
  end
@@ -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
- ## Google Translate
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
- # api_key: "AbC-dEf5"
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.21
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-07 00:00:00.000000000 Z
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: '0'
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: '0'
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.13.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.13.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.6.12
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