Files
sure/test/i18n_test.rb
T
6d6dac44f3 fix(i18n): remove duplicate locale keys and guard with a regression test (#2789)
* fix(i18n): remove duplicate locale keys that shadow translations

YAML resolves duplicate keys by silently letting the last occurrence
win, so every duplicated key hides an earlier definition without any
warning:

- transactions/{en,de,nl,hu}.yml defined `transactions.merge_duplicate`
  twice: once as a stale flat string ("Yes, merge them") and once as the
  `success`/`failure` mapping the controllers actually read. The flat
  string is dead either way (the show view reads
  `transactions.show.merge_duplicate`), so drop it.
- securities/fr.yml listed five providers (tiingo, eodhd, alpha_vantage,
  mfapi, binance_public) twice inside `securities.providers`.
- settings/api_keys/hu.yml defined
  `settings.api_keys.created.usage_instructions_title` twice.
- imports/en.yml defined `imports.create.ndjson_uploaded` twice.

All removals keep the currently-winning value, so rendered output is
unchanged; this only makes the files match what I18n already loads.

Add a non-skipped I18nTest case that walks the raw Psych AST of every
file under config/locales and fails on same-level duplicate keys, so
shadowed translations can't sneak back in.

Fixes #1506
Fixes #1502

* Fix linter / merge errors

* fix(i18n): repair duplicate locale regression test

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: rsnetworkinginc <rsnetworkinginc@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-08-22 09:37:30 +02:00

118 lines
4.3 KiB
Ruby

require "i18n/tasks"
require "pathname"
require "yaml"
# We're currently skipping some i18n tests to speed up development. Eventually, we'll make a dedicated
# project for getting i18n working. More details on that here:
# https://github.com/maybe-finance/maybe/issues/1225
class I18nTest < ActiveSupport::TestCase
GERMAN_COVERAGE_GLOBS = [
"config/locales/breadcrumbs/*.yml",
"config/locales/doorkeeper.*.yml",
"config/locales/mailers/**/*.yml",
"config/locales/models/**/*.yml",
"config/locales/views/**/*.yml"
]
def setup
@i18n = I18n::Tasks::BaseTask.new
end
def test_german_locale_files_parse
german_locale_paths.each do |path|
assert_nothing_raised do
YAML.load_file(path, aliases: true)
end
end
end
def test_no_missing_keys
skip "Skipping missing keys test"
missing_keys = @i18n.missing_keys(locales: [ :en ])
assert_empty missing_keys,
"Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
end
def test_no_unused_keys
skip "Skipping unused keys test"
unused_keys = @i18n.unused_keys(locales: [ :en ])
assert_empty unused_keys,
"#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
end
def test_files_are_normalized
skip "Skipping file normalization test"
non_normalized = @i18n.non_normalized_paths(locales: [ :en ])
error_message = "The following files need to be normalized:\n" \
"#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \
"Please run `i18n-tasks normalize' to fix"
assert_empty non_normalized, error_message
end
def test_no_inconsistent_interpolations
skip "Skipping inconsistent interpolations test"
inconsistent_interpolations = @i18n.inconsistent_interpolations(locales: [ :en ])
error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
"Please run `i18n-tasks check-consistent-interpolations' to show them"
assert_empty inconsistent_interpolations, error_message
end
# YAML silently resolves duplicate keys by letting the last occurrence win,
# so a duplicated key shadows the earlier definition without any warning
# (see #1506 / #1502, where a stale `transactions.merge_duplicate` string
# shadowed — or was shadowed by — the `merge_duplicate.success/failure`
# mapping in several locales). Parse the raw YAML AST so duplicates can't
# sneak back in.
def test_no_duplicate_keys_within_locale_files
offenses = []
Dir[File.expand_path("../config/locales/**/*.yml", __dir__)].sort.each do |file|
Psych.parse_stream(File.read(file), filename: file).children.each do |doc|
offenses.concat(duplicate_key_offenses(doc.root, [], file))
end
end
assert_empty offenses,
"Duplicate keys found in locale files (the last occurrence silently wins):\n" \
"#{offenses.map { |offense| " #{offense}" }.join("\n")}"
end
private
def german_locale_paths
@german_locale_paths ||= locale_paths.select { |path| path.basename.to_s.match?(/(^|[._-])de\.yml\z/) }
end
def locale_paths
@locale_paths ||= GERMAN_COVERAGE_GLOBS.flat_map { |glob| Pathname.pwd.glob(glob) }.uniq
end
def duplicate_key_offenses(node, path, file)
offenses = []
case node
when Psych::Nodes::Mapping
first_definition_lines = {}
node.children.each_slice(2) do |key_node, value_node|
if key_node.is_a?(Psych::Nodes::Scalar)
key_path = path + [ key_node.value ]
if (first_line = first_definition_lines[key_node.value])
offenses << "#{file}:#{key_node.start_line + 1} duplicate key `#{key_path.join(".")}` (first defined on line #{first_line})"
else
first_definition_lines[key_node.value] = key_node.start_line + 1
end
offenses.concat(duplicate_key_offenses(value_node, key_path, file))
else
offenses.concat(duplicate_key_offenses(value_node, path, file))
end
end
when Psych::Nodes::Sequence
node.children.each { |child| offenses.concat(duplicate_key_offenses(child, path, file)) }
end
offenses
end
end