Skip to content

Restore lost interpolation placeholders in 141 translated values across 16 languages - #1086

Open
emooreatx wants to merge 1 commit into
mainfrom
fix/l10n-placeholder-parity
Open

Restore lost interpolation placeholders in 141 translated values across 16 languages#1086
emooreatx wants to merge 1 commit into
mainfrom
fix/l10n-placeholder-parity

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Restores the {named} / printf interpolation placeholders that 141 translated values lost, across 16 languages. Their English sources never changed, so these strings render today with the substitution missing ("Showing entries" where the UI meant "Showing 42 entries") or with a token no formatter will fill.

Closes #1085.

What was wrong

Language Values
vi Vietnamese 64
te Telugu 29
my Burmese 9
zh Chinese (Simplified) 8
ar Arabic 5
fa Persian 4
pt Portuguese 4
sw Swahili 4
am Amharic 2
de German 2
es Spanish 2
fr French 2
tr Turkish 2
ur Urdu 2
ha Hausa 1
ja Japanese 1
Total 16 languages 141

By kind: 120 values had lost a placeholder outright, 13 carried a placeholder en.json does not have, 8 carried a renamed or otherwise mismatched set. Representative cases:

de audit_showing        en "Showing {count} entries" -> was "Zeige {current} von {total}"   (renamed)
ha audit_showing        en ['{count}']               -> was ['{end}','{start}','{total}']   (renamed)
ar mobile.tickets_days_deadline   en ['{days}']      -> was []                              (lost)
zh mobile.settings_integrity_verified_count  en ['{count}'] -> was []                       (lost)
my trust_calc_debug     en []                        -> was ['{API}','{calc}','{pending}']  (extra)

de audit_showing shows the shape of it: the translation is a faithful rendering of an older English source (Showing {current} of {total}), left behind when en.json moved to Showing {count} entries.

How it was detected

For every (language, key) pair, the multiset of runtime interpolation tokens in the translated value is compared against the en.json value's. Tokens are matched with

\$\{[^}]*\}|\{[A-Za-z0-9_]+\}|%[0-9]*\$?[sd]

covering named braces ({count}), indexed ({0}), Kotlin template (${...}) and printf (%s, %d, %1$s). Missing and empty translations are excluded — that is translation lag, a different problem. This is the same rule that found the drift in the first place (CIRISServer's client/tools/check_localization_sync.py, check_placeholder_parity), ported unchanged so the count here is directly comparable: 103,552 value comparisons over 3,724 English strings in 29 languages, 141 findings — exactly the 141 reported in #1085, same per-language distribution.

How it was repaired

Each corrupt value is replaced with CIRISServer's vendored value for the same key — and only where the two repos' en.json values for that key are byte-identical. An unchanged English source means the replacement is a pure repair; had the source been retranslated on your side, overwriting it would be corruption in the other direction. 0 keys were skipped: all 141 sources matched byte-for-byte.

Provenance: the values come from CIRISServer's vendored copy of this client, where they were restored from the pre-regression translations and verified placeholder-clean.

Surgical application: each file is loaded preserving its own key insertion order and re-serialized with this repo's exact formatting (2-space indent, ensure_ascii=False, trailing newline) — a round-trip that is byte-identical on an untouched file, verified against all 30 bundle files before any edit. Only the 141 value lines move.

Verification

$ detect (repaired tree, canonical bundle)
languages considered: 29
en string values: 3724; value compares examined: 103552
placeholder-drift findings: 0 across 0 language(s)

  ... and 0 findings in each of the other four bundle copies

$ git diff --stat
80 files changed, 705 insertions(+), 705 deletions(-)      # 141 values x 5 bundles

$ git diff --name-only | grep -E 'en\.json|manifest\.json' | wc -l
0
$ git diff --name-only | grep -v '/localization/' | wc -l
0

Key-address list and key order are preserved in all 80 files; every one of the 705 changed lines is a string value. The five committed runtime bundle copies —

client/shared/src/desktopMain/resources/localization   (canonical)
client/desktopApp/src/main/resources/localization
client/androidApp/src/main/assets/localization
client/iosApp/iosApp/localization
client/iosApp/Resources/app/localization

— were byte-identical before and are byte-identical after (diff -rq clean against the canonical bundle).

Recommendation: gate this class

There was no check on this side that could have caught it. The FOREIGN_ALPHABET ratchet catches a value written in the wrong script; a value in the right language that quietly dropped its {count} passes every existing check — right key, right script, plausible sentence, and the key-set comparison sees nothing missing. That is why 141 of them accumulated between 2.9.24 and 2.9.28 without a red build.

The parity rule above is cheap (one regex, one multiset compare, ~100k comparisons in well under a second) and has no false-positive mode: a translated value legitimately carrying a different set of interpolation tokens than its source is always a bug. Worth adding next to the alphabet ratchet as an error-severity check, along with a mirror check that the five bundle copies stay byte-identical — a stale copy ships raw keys at runtime for whichever platform loads it.

🤖 Generated with Claude Code

https://claude.ai/code/session_016tDGM9WfkcScEvHRRYfc4i

16 languages carry translations whose `{named}` / printf placeholders no
longer match their en.json source, so those strings render with the
substitution missing or unsubstituted at runtime. The English sources are
unchanged — only the translations drifted.

Detected by comparing, for every (language, key), the multiset of runtime
interpolation tokens in the translated value against the en.json value:
120 values had lost a placeholder, 13 carried one en.json does not have,
8 had a renamed/mismatched set.

Each corrupt value is replaced with CIRISServer's vendored value for the
same key, and only where the two repos' en.json values for that key are
byte-identical — an unchanged source means the replacement is a pure
repair, not an overwrite of a retranslation. Zero keys were skipped.

Applied to all five committed runtime bundle copies, which remain
byte-identical. Key sets, key order and file formatting are untouched:
705 changed lines = 141 values x 5 bundles, no en.json or manifest change.

Refs #1085

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tDGM9WfkcScEvHRRYfc4i

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2468c8b204

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"settings_integrity_unverified": "Tính toàn vẹn chưa được xác minh",
"settings_integrity_verified": "Tính toàn vẹn được xác minh",
"settings_integrity_verified_count": "Số lần xác minh tính toàn vẹn",
"settings_integrity_verified_count": "Đã xác minh {count} lần kiểm tra",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Supply the placeholder used by the integrity message

When Vietnamese is selected and Level 4 reports nonzero checked and total file counts, this string renders the literal {count}. SettingsScreen.kt:1266 supplies only passed, checked, and mode, while LocalizationManager.interpolate leaves parameters absent from that map unchanged. Update the caller to supply count (the same regression is introduced in the Chinese bundle) before adding this placeholder to these translations.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2.9.28 localization: 141 placeholder-corrupt values across 16 languages — translations lost their {param} placeholders

1 participant