diff --git a/lib/i18n/tasks/interpolations.rb b/lib/i18n/tasks/interpolations.rb index f67384fa..8777820b 100644 --- a/lib/i18n/tasks/interpolations.rb +++ b/lib/i18n/tasks/interpolations.rb @@ -3,25 +3,30 @@ module I18n::Tasks module Interpolations class << self - attr_accessor :variable_regex + attr_accessor :variable_regex, :tag_pairs, :tag_with_localized_value_regex end - @variable_regex = /%{[^}]+}/.freeze + @variable_regex = /%\{[^}]+\}|\{\{.*?\}\}|\{%.*?%\}/.freeze + @tag_pairs = [ + ['{{', '}}'], + ['%{', '}'], + ['{%', '%}'] + ].freeze + @tag_with_localized_value_regex = /\{\{\s?(("[^"]+")|('[^']+'))\s?\|.*?\}\}/ def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disable Metrics/AbcSize locales ||= self.locales base_locale ||= self.base_locale result = empty_forest - variable_regex = I18n::Tasks::Interpolations.variable_regex data[base_locale].key_values.each do |key, value| next if !value.is_a?(String) || ignore_key?(key, :inconsistent_interpolations) - base_vars = Set.new(value.scan(variable_regex)) + base_vars = get_normalized_variables_set(value) (locales - [base_locale]).each do |current_locale| node = data[current_locale].first.children[key] next unless node&.value.is_a?(String) - if base_vars != Set.new(node.value.scan(variable_regex)) + if base_vars != get_normalized_variables_set(node.value) result.merge!(node.walk_to_root.reduce(nil) { |c, p| [p.derive(children: c)] }) end end @@ -30,5 +35,27 @@ def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disabl result.each { |root| root.data[:type] = :inconsistent_interpolations } result end + + def get_normalized_variables_set(string) + Set.new(string.scan(I18n::Tasks::Interpolations.variable_regex).map { |variable| normalize(variable) }) + end + + def normalize(variable) + normalized = nil + if (match = variable.match(I18n::Tasks::Interpolations.tag_with_localized_value_regex)) + variable = variable.sub(match[1], 'localized input') + end + I18n::Tasks::Interpolations.tag_pairs.each do |start, end_| + next unless variable.start_with?(start) + + normalized = variable.delete_prefix(start).delete_suffix(end_).strip + normalized = start + normalized + end_ + break + end + + fail 'No start/end tag pair detected' if normalized.nil? + + normalized + end end end diff --git a/lib/i18n/tasks/translators/base_translator.rb b/lib/i18n/tasks/translators/base_translator.rb index 3fea19ed..dbb5728d 100644 --- a/lib/i18n/tasks/translators/base_translator.rb +++ b/lib/i18n/tasks/translators/base_translator.rb @@ -104,7 +104,7 @@ def parse_value(untranslated, each_translated, opts) end end - INTERPOLATION_KEY_RE = /%\{[^}]+}/.freeze + INTERPOLATION_KEY_RE = /%\{[^}]+\}|\{\{.*?\}\}|\{%.*?%\}/.freeze UNTRANSLATABLE_STRING = 'X__' # @param [String] value diff --git a/lib/i18n/tasks/translators/deepl_translator.rb b/lib/i18n/tasks/translators/deepl_translator.rb index fa4dc747..5399b9ef 100644 --- a/lib/i18n/tasks/translators/deepl_translator.rb +++ b/lib/i18n/tasks/translators/deepl_translator.rb @@ -23,6 +23,11 @@ def initialize(*) def translate_values(list, from:, to:, **options) results = [] + + if (glossary = glossary_for(from, to)) + options.merge!({ glossary_id: glossary.id }) + end + list.each_slice(BATCH_SIZE) do |parts| res = DeepL.translate( parts, @@ -93,6 +98,13 @@ def to_deepl_target_locale(locale) end end + # Find the largest glossary given a language pair + def glossary_for(source, target) + DeepL.glossaries.list.select do |glossary| + glossary.source_lang == source && glossary.target_lang == target + end.max_by(&:entry_count) + end + def configure_api_key! api_key = @i18n_tasks.translation_config[:deepl_api_key] host = @i18n_tasks.translation_config[:deepl_host] diff --git a/spec/deepl_translate_spec.rb b/spec/deepl_translate_spec.rb index baad36df..79494a68 100644 --- a/spec/deepl_translate_spec.rb +++ b/spec/deepl_translate_spec.rb @@ -5,9 +5,20 @@ require 'deepl' RSpec.describe 'DeepL Translation' do - nil_value_test = ['nil-value-key', nil, nil] - text_test = ['key', "Hello, %{user} O'Neill! How are you?", "¡Hola, %{user} O'Neill! ¿Qué tal estás?"] - html_test_plrl = ['html-key.html.one', 'Hello %{count}', 'Hola %{count}'] + nil_value_test = ['nil-value-key', nil, nil] + + text_test = [ + 'key', + "Hello, %{user} O'Neill! How are you? {{ Check out this Liquid tag, it should not be translated }} \ + {% That applies to this Liquid tag as well %}", + "¡Hola, %{user} O'Neill! ¿Qué tal estás? {{ Check out this Liquid tag, it should not be translated }} \ + {% That applies to this Liquid tag as well %}" + ] + + html_test_plrl = [ + 'html-key.html.one', 'Hello %{count} {{ count }} {% count %}', + 'Hola %{count} {{ count }} {% count %}' + ] array_test = ['array-key', ['Hello.', nil, '', 'Goodbye.'], ['Hola.', nil, '', 'Adiós.']] array_hash_test = ['array-hash-key', [{ 'hash_key1' => 'How are you?' }, { 'hash_key2' => nil }, { 'hash_key3' => 'Well.' }], diff --git a/spec/interpolations_spec.rb b/spec/interpolations_spec.rb index bb4e2579..f6728861 100644 --- a/spec/interpolations_spec.rb +++ b/spec/interpolations_spec.rb @@ -5,9 +5,6 @@ RSpec.describe 'Interpolations' do let!(:task) { I18n::Tasks::BaseTask.new } - let(:base_keys) { { 'a' => 'hello %{world}', 'b' => 'foo', 'c' => { 'd' => 'hello %{name}' }, 'e' => 'ok' } } - let(:test_keys) { { 'a' => 'hello', 'b' => 'foo %{bar}', 'c' => { 'd' => 'hola %{amigo}' }, 'e' => 'ok' } } - around do |ex| TestCodebase.setup( 'config/i18n-tasks.yml' => { base_locale: 'en', locales: %w[es] }.to_yaml, @@ -19,13 +16,63 @@ TestCodebase.teardown end - it '#inconsistent_interpolation' do - wrong = task.inconsistent_interpolations - leaves = wrong.leaves.to_a + context 'when using ruby string interpolations' do + let(:base_keys) { { 'a' => 'hello %{world}', 'b' => 'foo', 'c' => { 'd' => 'hello %{name}' }, 'e' => 'ok' } } + let(:test_keys) { { 'a' => 'hello', 'b' => 'foo %{bar}', 'c' => { 'd' => 'hola %{amigo}' }, 'e' => 'ok' } } + + it 'detects inconsistent interpolations' do + wrong = task.inconsistent_interpolations + leaves = wrong.leaves.to_a + + expect(leaves.size).to eq 3 + expect(leaves[0].full_key).to eq 'es.a' + expect(leaves[1].full_key).to eq 'es.b' + expect(leaves[2].full_key).to eq 'es.c.d' + end + end + + context 'when using liquid tags' do + let(:base_keys) do + { + a: 'hello {{ world }}', + b: 'foo', + c: { + d: 'hello {{ name }}' + }, + e: 'ok', + f: 'inconsistent {{ whitespace}}', + g: 'includes a {% comment %}', + h: 'wrong {% comment %}', + i: 'with localized value: {{ "thanks" | owner_invoice }}', + j: 'with wrong function: {{ "thanks" | owner_invoices }}' + } + end + let(:test_keys) do + { + a: 'hello', + b: 'foo {{ bar }}', + c: { + d: 'hola {{ amigo }}' + }, + e: 'ok', + f: '{{whitespace }} inconsistentes', + g: 'incluye un {% comment %}', + h: '{% commentario %} equivocado', + i: 'con valor localizado: {{ "gracias" | owner_invoice }}', + j: 'con función incorrecta: {{ "thanks" | owner_invoice }}' + } + end + + it 'detects inconsistent interpolations' do + wrong = task.inconsistent_interpolations + leaves = wrong.leaves.to_a - expect(leaves.size).to eq 3 - expect(leaves[0].full_key).to eq 'es.a' - expect(leaves[1].full_key).to eq 'es.b' - expect(leaves[2].full_key).to eq 'es.c.d' + expect(leaves.size).to eq 5 + expect(leaves[0].full_key).to eq 'es.a' + expect(leaves[1].full_key).to eq 'es.b' + expect(leaves[2].full_key).to eq 'es.c.d' + expect(leaves[3].full_key).to eq 'es.h' + expect(leaves[4].full_key).to eq 'es.j' + end end end