diff --git a/config/locales/en.yml b/config/locales/en.yml index 33c9083f..01972181 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,6 +33,7 @@ en: %{value_or_default_or_human_key} desc: add_missing: add missing keys to locale data + check_consistent_interpolations: verify that all translations use correct interpolation variables check_normalized: verify that all translation data is normalized config: display i18n-tasks configuration data: show locale data @@ -102,6 +103,8 @@ en: Google Translate returned no results. Make sure billing information is set at https://code.google.com/apis/console. health: no_keys_detected: No keys detected. Check data.read in config/i18n-tasks.yml. + inconsistent_interpolation: + none: No inconsistent interpolations found. missing: details_title: Value in other locales or source none: No translations are missing. diff --git a/config/locales/ru.yml b/config/locales/ru.yml index b3257ea8..50bf34e2 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -30,6 +30,8 @@ ru: %{value_or_default_or_human_key} desc: add_missing: добавить недостающие ключи к переводам + check_consistent_interpolations: убедитесь, что во всех переводах используются правильные + интерполяционные переменные check_normalized: проверить, что все файлы переводов нормализованы config: показать конфигурацию data: показать данные переводов @@ -99,6 +101,8 @@ ru: в https://code.google.com/apis/console. health: no_keys_detected: Ключи не обнаружены. Проверьте data.read в config/i18n-tasks.yml. + inconsistent_interpolation: + none: Не найдено несогласованных интерполяций. missing: details_title: На других языках или в коде none: Всё переведено. diff --git a/lib/i18n/tasks/base_task.rb b/lib/i18n/tasks/base_task.rb index 792a862d..cd5b8117 100644 --- a/lib/i18n/tasks/base_task.rb +++ b/lib/i18n/tasks/base_task.rb @@ -10,6 +10,7 @@ require 'i18n/tasks/used_keys' require 'i18n/tasks/ignore_keys' require 'i18n/tasks/missing_keys' +require 'i18n/tasks/inconsistent_interpolation' require 'i18n/tasks/unused_keys' require 'i18n/tasks/translation' require 'i18n/tasks/locale_pathname' @@ -30,6 +31,7 @@ class BaseTask include UsedKeys include IgnoreKeys include MissingKeys + include InconsistentInterpolation include UnusedKeys include Translation include Logging diff --git a/lib/i18n/tasks/command/commands/health.rb b/lib/i18n/tasks/command/commands/health.rb index b35b7488..72435552 100644 --- a/lib/i18n/tasks/command/commands/health.rb +++ b/lib/i18n/tasks/command/commands/health.rb @@ -16,7 +16,12 @@ def health(opt = {}) stats = i18n.forest_stats(forest) fail CommandError, t('i18n_tasks.health.no_keys_detected') if stats[:key_count].zero? terminal_report.forest_stats forest, stats - [missing(opt), unused(opt), check_normalized(opt)].detect { |result| result == :exit_1 } + [ + missing(opt), + unused(opt), + check_normalized(opt), + check_consistent_interpolations(opt) + ].detect { |result| result == :exit_1 } end end end diff --git a/lib/i18n/tasks/command/commands/inconsistent.rb b/lib/i18n/tasks/command/commands/inconsistent.rb new file mode 100644 index 00000000..b99db628 --- /dev/null +++ b/lib/i18n/tasks/command/commands/inconsistent.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module I18n::Tasks + module Command + module Commands + module Inconsistent + include Command::Collection + + cmd :check_consistent_interpolations, + pos: '[locale ...]', + desc: t('i18n_tasks.cmd.desc.check_consistent_interpolations'), + args: %i[locales out_format] + + def check_consistent_interpolations(opt = {}) + forest = i18n.inconsistent_interpolation(opt.slice(:locales, :base_locale)) + print_forest forest, opt, :inconsistent_interpolation + :exit_1 unless forest.empty? + end + end + end + end +end diff --git a/lib/i18n/tasks/commands.rb b/lib/i18n/tasks/commands.rb index b2dca497..7aa0d193 100644 --- a/lib/i18n/tasks/commands.rb +++ b/lib/i18n/tasks/commands.rb @@ -5,6 +5,7 @@ require 'i18n/tasks/command/commands/health' require 'i18n/tasks/command/commands/missing' require 'i18n/tasks/command/commands/usages' +require 'i18n/tasks/command/commands/inconsistent' require 'i18n/tasks/command/commands/eq_base' require 'i18n/tasks/command/commands/data' require 'i18n/tasks/command/commands/tree' @@ -17,6 +18,7 @@ class Commands < Command::Commander include Command::Commands::Health include Command::Commands::Missing include Command::Commands::Usages + include Command::Commands::Inconsistent include Command::Commands::EqBase include Command::Commands::Data include Command::Commands::Tree diff --git a/lib/i18n/tasks/inconsistent_interpolation.rb b/lib/i18n/tasks/inconsistent_interpolation.rb new file mode 100644 index 00000000..5f5adebe --- /dev/null +++ b/lib/i18n/tasks/inconsistent_interpolation.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module I18n::Tasks + module InconsistentInterpolation + VARIABLE_REGEX = /%{[^}]+}/ + + def inconsistent_interpolation(locales: nil, base_locale: nil) + locales ||= self.locales + base = base_locale || self.base_locale + tree = empty_forest + + data[base].key_values.each do |key, value| + next if ignore_key?(key, :inconsistent) || !value.is_a?(String) + + base_variables = Set.new(value.scan(VARIABLE_REGEX)) + + (locales - [base]).each do |current_locale| + node = data[current_locale].first.children[key] + + next if !node&.value&.is_a?(String) || base_variables == Set.new(node.value.scan(VARIABLE_REGEX)) + + tree.merge! inconsistent_interpolation_tree(current_locale, key) + end + end + + tree + end + + def inconsistent_interpolation_tree(locale, key) + data[locale].select_keys(root: false) { |x| x == key } + .set_root_key!(locale, type: :inconsistent_interpolation) + end + end +end diff --git a/lib/i18n/tasks/reports/base.rb b/lib/i18n/tasks/reports/base.rb index 4a078234..325f6a48 100644 --- a/lib/i18n/tasks/reports/base.rb +++ b/lib/i18n/tasks/reports/base.rb @@ -21,6 +21,10 @@ def missing_title(forest) "Missing translations (#{forest.leaves.count || '∅'})" end + def inconsistent_interpolation_title(forest) + "Inconsistent interpolations (#{forest.leaves.count || '∅'})" + end + def unused_title(key_values) "Unused keys (#{key_values.count || '∅'})" end diff --git a/lib/i18n/tasks/reports/terminal.rb b/lib/i18n/tasks/reports/terminal.rb index 19a3fb0f..ab099ae4 100644 --- a/lib/i18n/tasks/reports/terminal.rb +++ b/lib/i18n/tasks/reports/terminal.rb @@ -25,6 +25,15 @@ def missing_keys(forest = task.missing_keys) end end + def inconsistent_interpolation(forest = task.inconsistent_interpolation) + if forest.present? + print_title inconsistent_interpolation_title(forest) + show_tree(forest) + else + print_success I18n.t('i18n_tasks.inconsistent_interpolation.none') + end + end + def icon(type) glyph = missing_type_info(type)[:glyph] { missing_used: Rainbow(glyph).red, missing_diff: Rainbow(glyph).yellow }[type] diff --git a/spec/commands/inconsistent_commands_spec.rb b/spec/commands/inconsistent_commands_spec.rb new file mode 100644 index 00000000..557a36a3 --- /dev/null +++ b/spec/commands/inconsistent_commands_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Inconsistent commands' do + delegate :run_cmd, :in_test_app_dir, to: :TestCodebase + + 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' } } + + let(:wrong_subtree) { { 'es' => test_keys.slice('a', 'b', 'c') } } + + around do |ex| + TestCodebase.setup( + 'config/i18n-tasks.yml' => { base_locale: 'en', locales: %w[es] }.to_yaml, + 'config/locales/en.yml' => { 'en' => base_keys }.to_yaml, + 'config/locales/es.yml' => { 'es' => test_keys }.to_yaml + ) + + TestCodebase.in_test_app_dir { ex.call } + TestCodebase.teardown + end + + describe '#check_consistent_interpolations' do + it 'returns inconsistent keys' do + expect(YAML.load(run_cmd('check-consistent-interpolations', '-fyaml'))).to eq(wrong_subtree) + end + end +end diff --git a/spec/inconsistent_interpolation_spec.rb b/spec/inconsistent_interpolation_spec.rb new file mode 100644 index 00000000..2aaedb57 --- /dev/null +++ b/spec/inconsistent_interpolation_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'InconsistentInterpolation' 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, + 'config/locales/en.yml' => { 'en' => base_keys }.to_yaml, + 'config/locales/es.yml' => { 'es' => test_keys }.to_yaml + ) + + TestCodebase.in_test_app_dir { ex.call } + TestCodebase.teardown + end + + it '#inconsistent_interpolation' do + wrong = task.inconsistent_interpolation + 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 diff --git a/templates/config/i18n-tasks.yml b/templates/config/i18n-tasks.yml index 2e3aeaac..ac2bbc81 100644 --- a/templates/config/i18n-tasks.yml +++ b/templates/config/i18n-tasks.yml @@ -111,6 +111,10 @@ search: # fr,es: # - common.brand +## Exclude these keys from the `i18n-tasks check-consistent-interpolations` report: +# ignore_inconsistent: +# - 'activerecord.attributes.*' + ## Ignore these keys completely: # ignore: # - kaminari.*