Skip to content

Commit

Permalink
Closes #575 - Allow configuring translation backend in yaml
Browse files Browse the repository at this point in the history
- Use the Enum option parser for the translation_backend to prevent fallback to `en`
- Add `allow_blank` argument to the Enum option parser. We want -b to be optional so we can use the config file backend if present
- Update translation strings for translation_backend CLI argument to list all options
- Error when invalid backend is specified
- Retain :google backend default, specify in `I18n::Tasks::Configuration#translation_config`
- Tidy README to remove duplication and show backend configuration via config file
  • Loading branch information
jaredmoody authored and glebm committed Jun 10, 2024
1 parent 2d4d28e commit 2145066
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 49 deletions.
49 changes: 13 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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: <Google Translate API key>
```

Expand All @@ -452,6 +426,7 @@ GOOGLE_TRANSLATE_API_KEY=<Google Translate API key>
```yaml
# config/i18n-tasks.yml
translation:
backend: deepl
deepl_api_key: <DeepL Pro API key>
deepl_host: <optional>
deepl_version: <optional>
Expand All @@ -478,6 +453,7 @@ DEEPL_VERSION=<optional>
```yaml
# config/i18n-tasks.yml
translation:
backend: yandex
yandex_api_key: <Yandex API key>
```

Expand All @@ -495,6 +471,7 @@ YANDEX_API_KEY=<Yandex API key>
```yaml
# config/i18n-tasks.yml
translation:
backend: openai
openai_api_key: <OpenAI API key>
openai_model: <optional>
```
Expand Down
3 changes: 2 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion config/locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion lib/i18n/tasks/command/commands/missing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions lib/i18n/tasks/command/option_parsers/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions lib/i18n/tasks/command/options/locales.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions lib/i18n/tasks/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion lib/i18n/tasks/translation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 38 additions & 1 deletion spec/commands/missing_commands_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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

0 comments on commit 2145066

Please sign in to comment.