diff --git a/README.md b/README.md index 7066ec93..19fe1b7e 100644 --- a/README.md +++ b/README.md @@ -84,49 +84,22 @@ Usage: i18n-tasks add-missing [options] [locale ...] -h, --help Display this help message. ``` -### Google Translate missing keys +### Translate Missing Keys -Translate missing values with Google Translate ([more below on the API key](#google-translation-config)). +Translate missing keys using a backend service of your choice. ```console $ i18n-tasks translate-missing -# accepts from and locales options: -$ i18n-tasks translate-missing --from=base es fr +# accepts backend, from and locales options +$ i18n-tasks translate-missing --from=base es fr --backend=google ``` -### DeepL Pro Translate missing keys - -Translate missing values with DeepL Pro Translate ([more below on the API key](#deepl-translation-config)). - -```console -$ i18n-tasks translate-missing --backend=deepl - -# accepts from and locales options: -$ i18n-tasks translate-missing --backend=deepl --from=en fr nl -``` - -### Yandex Translate missing keys - -Translate missing values with Yandex Translate ([more below on the API key](#yandex-translation-config)). - -```console -$ i18n-tasks translate-missing --backend=yandex - -# accepts from and locales options: -$ i18n-tasks translate-missing --from=en es fr -``` - -### OpenAI Translate missing keys - -Translate missing values with OpenAI ([more below on the API key](#openai-translation-config)). - -```console -$ i18n-tasks translate-missing --backend=openai - -# accepts from and locales options: -$ i18n-tasks translate-missing --from=en es fr -``` +Available backends: +- `google` - [Google Translate](#google-translation-config) +- `deepl` - [DeepL Pro](#deepl-translation-config) +- `yandex` - [Yandex Translate](#yandex-translation-config) +- `openai` - [OpenAI](#openai-translation-config) ### Find usages @@ -435,6 +408,7 @@ Put the key in `GOOGLE_TRANSLATE_API_KEY` environment variable or in the config ```yaml # config/i18n-tasks.yml translation: + backend: google google_translate_api_key: ``` @@ -452,6 +426,7 @@ GOOGLE_TRANSLATE_API_KEY= ```yaml # config/i18n-tasks.yml translation: + backend: deepl deepl_api_key: deepl_host: deepl_version: @@ -478,6 +453,7 @@ DEEPL_VERSION= ```yaml # config/i18n-tasks.yml translation: + backend: yandex yandex_api_key: ``` @@ -495,6 +471,7 @@ YANDEX_API_KEY= ```yaml # config/i18n-tasks.yml translation: + backend: openai openai_api_key: openai_model: ``` diff --git a/config/locales/en.yml b/config/locales/en.yml index c335c8d3..2e72475a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -26,7 +26,7 @@ en: strict: >- Avoid inferring dynamic key usages such as t("cats.#{cat}.name"). Takes precedence over the config setting if set. - translation_backend: Translation backend (google or deepl) + translation_backend: Translation backend [google, deepl, yandex, openai]) value: >- Value. Interpolates: %%{value}, %%{human_key}, %%{key}, %%{default}, %%{value_or_human_key}, %%{value_or_default_or_human_key} @@ -69,6 +69,7 @@ en: enum_opt: invalid: "%{invalid} is not one of: %{valid}." errors: + invalid_backend: 'Invalid backend: %{invalid}. Must be one of %{valid}.' invalid_format: 'invalid format: %{invalid}. valid: %{valid}.' invalid_locale: 'invalid locale: %{invalid}' invalid_missing_type: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index eda1246b..c6e7e4b2 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -23,7 +23,7 @@ ru: out_format: 'Формат вывода: %{valid_text}.' pattern_router: 'Использовать pattern_router: ключи распределятся по файлам согласно data.write' strict: Не угадывать динамические использования ключей, например `t("category.#{category.key}")` - translation_backend: Движок перевода (google или deepl) + translation_backend: Движок перевода [google, deepl, yandex, openai] value: >- Значение, интерполируется с %%{value}, %%{human_key}, %%{key}, %%{default}, %%{value_or_human_key}, %%{value_or_default_or_human_key} @@ -66,6 +66,7 @@ ru: enum_opt: invalid: "%{invalid} не является одним из: %{valid}." errors: + invalid_backend: 'Недопустимый источник данных: %{invalid}. Должен быть одним из %{valid}.' invalid_format: 'Неизвестный формат %{invalid}. Форматы: %{valid}.' invalid_locale: Неверный язык %{invalid} invalid_missing_type: diff --git a/lib/i18n/tasks/command/commands/missing.rb b/lib/i18n/tasks/command/commands/missing.rb index 84e348e1..06883a83 100644 --- a/lib/i18n/tasks/command/commands/missing.rb +++ b/lib/i18n/tasks/command/commands/missing.rb @@ -48,7 +48,9 @@ def translate_missing(opt = {}) pattern_re = i18n.compile_key_pattern(opt[:pattern]) missing.select_keys! { |full_key, _node| full_key =~ pattern_re } end - translated = i18n.translate_forest missing, from: opt[:from], backend: opt[:backend].to_sym + + backend = opt[:backend].presence || i18n.translation_config[:backend] + translated = i18n.translate_forest missing, from: opt[:from], backend: backend.to_sym i18n.data.merge! translated log_stderr t('i18n_tasks.translate_missing.translated', count: translated.leaves.count) print_forest translated, opt diff --git a/lib/i18n/tasks/command/option_parsers/enum.rb b/lib/i18n/tasks/command/option_parsers/enum.rb index e476d247..8bd1577e 100644 --- a/lib/i18n/tasks/command/option_parsers/enum.rb +++ b/lib/i18n/tasks/command/option_parsers/enum.rb @@ -9,17 +9,18 @@ class Parser I18n.t('i18n_tasks.cmd.enum_opt.invalid', invalid: invalid, valid: valid * ', ') end - def initialize(valid, error_message = DEFAULT_ERROR) + def initialize(valid, error_message = DEFAULT_ERROR, allow_blank: false) @valid = valid.map(&:to_s) @error_message = error_message + @allow_blank = allow_blank end def call(value, *) - return @valid.first unless value.present? + return @valid.first if value.blank? && !@allow_blank if @valid.include?(value) value - else + elsif value.present? || !@allow_blank fail CommandError, @error_message.call(value, @valid) end end diff --git a/lib/i18n/tasks/command/options/locales.rb b/lib/i18n/tasks/command/options/locales.rb index 46becad5..1d06127e 100644 --- a/lib/i18n/tasks/command/options/locales.rb +++ b/lib/i18n/tasks/command/options/locales.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'i18n/tasks/command/option_parsers/locale' +require 'i18n/tasks/command/option_parsers/enum' module I18n::Tasks module Command @@ -31,13 +32,21 @@ module Locales parser: OptionParsers::Locale::Parser, default: 'base' - TRANSLATION_BACKENDS = %w[google deepl].freeze + TRANSLATION_BACKENDS = %w[google deepl yandex openai].freeze arg :translation_backend, '-b', '--backend BACKEND', t('i18n_tasks.cmd.args.desc.translation_backend'), - parser: OptionParsers::Locale::Parser, - default: TRANSLATION_BACKENDS[0] + parser: + OptionParsers::Enum::Parser.new( + TRANSLATION_BACKENDS, + proc do |value, valid| + if value.present? + I18n.t('i18n_tasks.cmd.errors.invalid_backend', invalid: value&.strip, valid: valid * ', ') + end + end, + allow_blank: true + ) end end end diff --git a/lib/i18n/tasks/configuration.rb b/lib/i18n/tasks/configuration.rb index 4463e382..1bc3c4a4 100644 --- a/lib/i18n/tasks/configuration.rb +++ b/lib/i18n/tasks/configuration.rb @@ -5,7 +5,8 @@ module I18n::Tasks::Configuration # rubocop:disable Metrics/ModuleLength base_locale: 'en', internal_locale: 'en', search: ::I18n::Tasks::UsedKeys::SEARCH_DEFAULTS, - data: ::I18n::Tasks::Data::DATA_DEFAULTS + data: ::I18n::Tasks::Data::DATA_DEFAULTS, + translation_backend: :google }.freeze # i18n-tasks config (defaults + config/i18n-tasks.yml) @@ -59,9 +60,10 @@ def data_config # translation config # @return [Hash{String => String,Hash,Array}] - def translation_config + def translation_config # rubocop:disable Metrics/AbcSize @config_sections[:translation] ||= begin conf = (config[:translation] || {}).with_indifferent_access + conf[:backend] ||= DEFAULTS[:translation_backend] conf[:google_translate_api_key] = ENV['GOOGLE_TRANSLATE_API_KEY'] if ENV.key?('GOOGLE_TRANSLATE_API_KEY') conf[:deepl_api_key] = ENV['DEEPL_AUTH_KEY'] if ENV.key?('DEEPL_AUTH_KEY') conf[:deepl_host] = ENV['DEEPL_HOST'] if ENV.key?('DEEPL_HOST') diff --git a/lib/i18n/tasks/translation.rb b/lib/i18n/tasks/translation.rb index 31edfdf5..b73bfcba 100644 --- a/lib/i18n/tasks/translation.rb +++ b/lib/i18n/tasks/translation.rb @@ -11,7 +11,7 @@ module Translation # @param [String] from locale # @param [:deepl, :openai, :google, :yandex] backend # @return [I18n::Tasks::Tree::Siblings] translated forest - def translate_forest(forest, from:, backend: :google) + def translate_forest(forest, from:, backend:) case backend when :deepl Translators::DeeplTranslator.new(self).translate_forest(forest, from) diff --git a/spec/commands/missing_commands_spec.rb b/spec/commands/missing_commands_spec.rb index 4411d747..973abb5d 100644 --- a/spec/commands/missing_commands_spec.rb +++ b/spec/commands/missing_commands_spec.rb @@ -6,10 +6,11 @@ delegate :run_cmd, to: :TestCodebase let(:missing_keys) { { 'a' => 'A', 'ref' => :ref } } + let(:config) { { base_locale: 'en', locales: %w[es fr] } } around do |ex| TestCodebase.setup( - 'config/i18n-tasks.yml' => { base_locale: 'en', locales: %w[es fr] }.to_yaml, + 'config/i18n-tasks.yml' => config.to_yaml, 'config/locales/es.yml' => { 'es' => missing_keys }.to_yaml ) TestCodebase.in_test_app_dir { ex.call } @@ -47,4 +48,40 @@ end end end + + describe '#translate_missing' do + it 'defaults the backend to google when not specified' do + google_double = instance_double(I18n::Tasks::Translators::GoogleTranslator) + allow(I18n::Tasks::Translators::GoogleTranslator).to receive(:new).and_return(google_double) + allow(google_double).to receive(:translate_forest).and_return(I18n::Tasks::BaseTask.new.empty_forest) + expect(google_double).to receive(:translate_forest) + + run_cmd 'translate-missing' + end + + it 'errors when invalid backend is specified' do + invalid = 'awesome-translate' + + expect { run_cmd 'translate-missing', "-b #{invalid}" }.to( + raise_error( + I18n::Tasks::CommandError, + I18n.t('i18n_tasks.cmd.errors.invalid_backend', + invalid: invalid, valid: I18n::Tasks::Command::Options::Locales::TRANSLATION_BACKENDS * ', ') + ) + ) + end + + context 'when backend is specified in config' do + let(:config) { { base_locale: 'en', locales: %w[es fr], translation: { backend: 'deepl' } } } + + it 'uses the backend from the configuration' do + deepl_double = instance_double(I18n::Tasks::Translators::DeeplTranslator) + allow(I18n::Tasks::Translators::DeeplTranslator).to receive(:new).and_return(deepl_double) + allow(deepl_double).to receive(:translate_forest).and_return(I18n::Tasks::BaseTask.new.empty_forest) + expect(deepl_double).to receive(:translate_forest) + + run_cmd 'translate-missing' + end + end + end end