Files
wadamesh/scripts/build/i18n-audit.py
T
Kaj SchittecatandClaude Opus 4.8 15415fd0f5 i18n: enumerate every user-visible literal instead of grepping for them (49 wrapped, v6)
My earlier sweeps searched for strings I could think of, which is why you
kept finding more. scripts/build/i18n-scan-unwrapped.py now parses every
call that takes user text — LVGL setters (label/checkbox/dropdown/roller/
placeholder/tab/table/msgbox), showAlert, showConfirm, appPageBegin,
createSettingsModal and the settings row helpers — splits the argument list
and reports any human-readable literal not wrapped in TR(). It found 55;
49 got wrapped (--fix does it mechanically), 6 skipped as brand/technical
(WADAMESH, CPU, PSK, OK, and two unit readouts). It now reports zero.

The 33 newly reachable keys are translated in all 13 languages: the Flood/
Zero-hop scope buttons, Share QR, Search contacts, Play / Set as
notification sound / Set as wallpaper, Probe now, On map, Install update,
Save update bin to SD, Choose .wav from files, Power, Clear history, the
Bluetooth/MQTT/Logs/System info sheet titles, both dropdown option lists,
the size preset list, Import, the URL bar, and Snake's New game.

Two format bugs behind the last stragglers: keys whose text begins with
LVGL recolor markup ('#7A7F87 Wardrive: ...#') were dropped as comment
lines by BOTH the device loader and the audit — a line is a comment only
when it has no tab, fixed in both. And several rows had been written with
literal '\xE2\x80\xA6' text where the file needs the real character; the
loader only unescapes \n \t \\, so those keys never matched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 17:46:00 +02:00

81 lines
3.2 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""i18n audit: every TR("...") key in the touch UI must be covered by every
deploy/apps/lang/*.lang file (the canonical translation source — translators
edit those files and PR them; the firmware ships no baked-in table).
Run from the repo root: python3 scripts/build/i18n-audit.py
Exit code 1 if any language is missing keys (prints them).
"""
import glob, os, re, sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def unescape_c(s):
"""Decode a C string literal body to the exact runtime text: escapes become
BYTES (like the compiler emits), normal chars their UTF-8 bytes, then the
whole buffer decodes as UTF-8 — so "\xE2\x80\xA6" and a literal … agree."""
out, i = bytearray(), 0
while i < len(s):
if s[i] == '\\' and i + 1 < len(s):
n = s[i + 1]
if n == 'n': out.append(0x0A); i += 2
elif n == 't': out.append(0x09); i += 2
elif n == 'x': out.append(int(s[i + 2:i + 4], 16)); i += 4
else: out += n.encode('utf-8'); i += 2
else:
out += s[i].encode('utf-8'); i += 1
return out.decode('utf-8', 'replace')
def source_keys():
keys = set()
for f in glob.glob(os.path.join(ROOT, 'src/ui-touch/*.cpp')) + \
glob.glob(os.path.join(ROOT, 'src/ui-touch/*.h')):
src = open(f, encoding='utf-8').read()
# An optional LV_SYMBOL_* macro may precede the literal: TR(LV_SYMBOL_OK " Text").
# Requiring the group to START with a quote silently skipped every such
# call - the audit's worst blind spot (the sheet rows never got checked).
for m in re.finditer(r'TR\(\s*((?:LV_SYMBOL_[A-Z0-9_]+\s+)?(?:"(?:[^"\\]|\\.)*"\s*)+)\)', src):
parts = re.findall(r'"((?:[^"\\]|\\.)*)"', m.group(1))
k = unescape_c(''.join(parts))
# strip icon glyphs + their spacer, exactly as TR() does at runtime
while k and 0xE000 <= ord(k[0]) <= 0xF8FF:
k = k[1:]
k = k.lstrip(' ')
if k.strip():
keys.add(k)
keys |= {"Apps", "Built-in", "Languages"} # TR(kStoreTabs[t])
return keys
# Known untranslatable / identical-everywhere keys.
SKIP = {'...', '😊', 'Snake', '© OpenStreetMap', '© OpenTopoMap'}
def lang_keys(path):
have = set()
for line in open(path, encoding='utf-8'):
# Header lines have no tab; '#' can legitimately START a key (LVGL
# recolor markup), so never treat a leading '#' as a comment marker.
if '\t' not in line:
continue
k = line.split('\t', 1)[0]
have.add(k.replace('\\n', '\n').replace('\\t', '\t').replace('\\\\', '\\'))
return have
def main():
keys = source_keys() - SKIP
bad = 0
for path in sorted(glob.glob(os.path.join(ROOT, 'deploy/apps/lang/*.lang'))):
missing = sorted(k for k in keys if k not in lang_keys(path))
tag = os.path.basename(path)
if missing:
bad += 1
print(f"{tag}: MISSING {len(missing)}")
for k in missing:
print(f" {k!r}")
else:
print(f"{tag}: ok ({len(keys)} keys covered)")
sys.exit(1 if bad else 0)
if __name__ == '__main__':
main()