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
         
     |