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 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