From 0b427c7722fd02960ea5bf550d95d5fdbe9127d6 Mon Sep 17 00:00:00 2001 From: Tom Brouwer Date: Tue, 6 Feb 2024 14:37:44 +0100 Subject: [PATCH] 20321 Fix liquid tag consistency not being validated in interpolations check Note that interpolation checks can be skipped on specific keys using the 'ignore_inconsistent_interpolations' property in the yaml config file Ref bookingexperts/support#20321 --- lib/i18n/tasks/interpolations.rb | 34 ++++++++++++++-- spec/interpolations_spec.rb | 67 +++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/lib/i18n/tasks/interpolations.rb b/lib/i18n/tasks/interpolations.rb index f67384fa..64d278d9 100644 --- a/lib/i18n/tasks/interpolations.rb +++ b/lib/i18n/tasks/interpolations.rb @@ -3,9 +3,15 @@ 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 @@ -16,12 +22,14 @@ def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disabl 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 = Set.new( + value.scan(variable_regex).map { |interpolation| normalize(interpolation) } + ) (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 != Set.new(node.value.scan(variable_regex).map { |interpolation| normalize(interpolation) }) result.merge!(node.walk_to_root.reduce(nil) { |c, p| [p.derive(children: c)] }) end end @@ -30,5 +38,23 @@ def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disabl result.each { |root| root.data[:type] = :inconsistent_interpolations } result end + + def normalize(interpolation) + normalized = nil + if (match = interpolation.match(I18n::Tasks::Interpolations.tag_with_localized_value_regex)) + interpolation = interpolation.sub(match[1], 'localized input') + end + I18n::Tasks::Interpolations.tag_pairs.each do |start, end_| + next unless interpolation.start_with?(start) + + normalized = interpolation.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/spec/interpolations_spec.rb b/spec/interpolations_spec.rb index bb4e2579..ce5219ec 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 '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 '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