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>
This commit is contained in:
rsnetworkinginc
2026-08-22 09:37:30 +02:00
committed by GitHub
co-authored by rsnetworkinginc Juan José Mata Juan José Mata sure-admin
parent 381ede89d9
commit 6d6dac44f3
9 changed files with 49 additions and 12 deletions
-1
View File
@@ -447,7 +447,6 @@ en:
document_uploaded: Document uploaded successfully.
document_upload_failed: We couldn't upload the document to the vector store. Please try again.
invalid_ndjson_file_type: Invalid file type or format. Please upload a valid .ndjson or .json export file.
ndjson_uploaded: NDJSON file uploaded successfully.
document_provider_not_configured: No vector store is configured for document uploads.
show:
finalize_upload: Please finalize your file upload.
-5
View File
@@ -12,9 +12,4 @@ fr:
tiingo: Tiingo
twelve_data: Twelve Data
yahoo_finance: Yahoo Finance
tiingo: Tiingo
eodhd: EODHD
alpha_vantage: Alpha Vantage
mfapi: MFAPI.in
binance_public: Binance
moex_public: MOEX
@@ -103,7 +103,6 @@ hu:
created_label: "Létrehozva:"
security_note_title: "Fontos biztonsági megjegyzés"
security_note_body: "Ez az egyetlen alkalom, amikor az API-kulcsod megjelenik. Mindenképpen másold le most és tárold biztonságosan. Ha elveszíted, újat kell generálnod."
usage_instructions_title: "Az API-kulcs használata"
key_name: "Név"
permissions: "Jogosultságok"
critical_warning_title: "⚠️ Kritikus: Mentsd el az API-kulcsodat most"
-1
View File
@@ -97,7 +97,6 @@ de:
mark_recurring_title: Wiederkehrende Transaktion
potential_duplicate_title: Mögliches Duplikat erkannt
potential_duplicate_description: Diese ausstehende Transaktion könnte mit der unten stehenden gebuchten Transaktion übereinstimmen. Wenn ja, führe sie zusammen, um Doppelzählung zu vermeiden.
merge_duplicate: Ja, zusammenführen
keep_both: Nein, beide behalten
transaction:
pending: Ausstehend
-1
View File
@@ -127,7 +127,6 @@ en:
mark_recurring_title: Recurring Transaction
potential_duplicate_title: Possible duplicate detected
potential_duplicate_description: This pending transaction may be the same as the posted transaction below. If so, merge them to avoid double-counting.
merge_duplicate: Yes, merge them
keep_both: No, keep both
split_parent_row:
split_label: "Split"
-1
View File
@@ -123,7 +123,6 @@ hu:
mark_recurring_title: Ismétlődő tranzakció
potential_duplicate_title: Lehetséges duplikátum észlelve
potential_duplicate_description: Ez a függőben lévő tranzakció megegyezhet az alábbi közzétett tranzakcióval. Ha igen, egyesítsd őket a kettős könyvelés elkerülése érdekében.
merge_duplicate: Igen, egyesítsd őket
keep_both: Nem, mindkettőt tartsd meg
split_parent_row:
split_label: "Felosztva"
-1
View File
@@ -71,7 +71,6 @@ nl:
mark_recurring_title: Terugkerende Transactie
potential_duplicate_title: Mogelijk duplicaat gedetecteerd
potential_duplicate_description: Deze wachtende transactie kan hetzelfde zijn als de geposte transactie hieronder. Indien ja, voeg ze samen om dubbeltelling te voorkomen.
merge_duplicate: Ja, voeg ze samen
keep_both: Nee, bewaar beide
transaction:
pending: Wachtend
-1
View File
@@ -122,7 +122,6 @@ uk:
mark_recurring_title: Повторювана транзакція
potential_duplicate_title: Виявлено можливий дублікат
potential_duplicate_description: Ця очікувана транзакція може бути такою ж, як і опублікована транзакція нижче. Якщо так, об'єднайте їх, щоб уникнути подвійного підрахунку.
merge_duplicate: Так, об'єднати їх
keep_both: Ні, залишити обидві
split_parent_row:
split_label: "Розділити"
+49
View File
@@ -57,6 +57,26 @@ class I18nTest < ActiveSupport::TestCase
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/) }
@@ -65,4 +85,33 @@ class I18nTest < ActiveSupport::TestCase
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