pisti87's T-Deck rebooted every time he logged into a repeater, but only in
Hungarian. The login clock-skew warning does:
snprintf(msg, n, TR("Device clock differs from \"%s\" by %lu min%s"),
name, minutes, suffix);
and the Hungarian row reordered the conversions to "%lu ... %s ... %s". Varargs
are positional, so snprintf read the name POINTER as an unsigned long and then
took the minute count -- the integer 3 -- as a char* and dereferenced address 3.
Instant panic, every login, Hungarian only. English fit the declared order, so
it never showed there.
Fixing the four bad Hungarian rows is not sufficient: TR() returns a format
string and translations come from .lang files that users download or hand-write,
so any file can crash any device. TR() now compares the ordered conversion
signatures of key and translation and falls back to the English key on a
mismatch -- the key IS the call site's format string, so it is always correct.
The scan runs only for keys containing '%', which is a small minority.
Also fixed the four rows (three were Hungarian-only crashes or dropped values),
bumped hu to v11, and taught audit-lang.py to fail the build on a mismatch so a
future translation PR cannot reintroduce this. Unit-checked that the audit
detects the original bad row and accepts the repaired one.
Reported by pisti87.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The firmware asked for these keys and no .lang file carried them, so they
rendered English in all 13 languages with nothing to show a translator that
they were missing. Mostly strings that shipped with a feature whose PR did not
touch the language files: the SD arbitration and migration warnings, the
Wi-Fi/BLE coexistence messages, the keyboard-navigation vocabulary
(Up/Down/Left/Right/Select/Scroll) and section names (General, Clock & time,
Sensors, Screen, Home, Messages, Keys, Compact, Snake).
They go in with an EMPTY translation, which is the format's own way of saying
'not translated yet': the runtime loader requires a non-empty value
(`if (*p && tab[1])`) and gen-lang-builtin.py requires `if k and v`, so both
skip these rows and the English falls through exactly as before. Confirmed:
i18n_builtin.h regenerates byte-identical. No `# ver:` bump for the same
reason — device behaviour is unchanged, so there is nothing to re-download.
Also fixes the audit's own row parser to match the loader: a line is a comment
only when it has NO tab. Testing for a leading '#' hid every key starting with
LVGL recolor markup ("#7A7F87 Wardrive: …#"), the exact class the loader has a
comment warning about.
Every language is now at 0 missing keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing checked this. The .lang files are hand-maintained and i18n_builtin.h is
generated FROM them, so drift is invisible in both directions: a row whose
English no longer appears in any TR() call is translated for nothing, and a
TR() key with no row renders English in every language forever.
Matching mirrors TR() in i18n.cpp, which is the part a grep gets wrong —
adjacent C literals concatenate, LV_SYMBOL_* icon prefixes are stripped before
lookup, non-ASCII is often written as \x escapes, and menu/tab labels reach
TR() through a table field rather than as a literal (resolved by harvesting the
table's initialiser).
Current state: 910 TR() keys; every language carries ~57 dead rows and is
missing ~53 keys (hu: 77 dead, 52 missing).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>