Files
wadamesh/scripts/build/gen-lang-builtin.py
T
Kaj SchittecatandClaude Opus 4.8 efe77a67e3 i18n: bake the .lang files into boards that cannot rely on the Store (V4)
gen-lang-builtin.py generates src/ui-touch/i18n_builtin.h from the SAME
deploy/apps/lang/*.lang files the Store serves, so the downloaded and the
compiled-in translations can never drift — translators still edit one place.
TR() consults the loaded file overlay first, then the baked table, then
English, so a downloaded language still wins on boards that can fetch one.

CAP_BUILTIN_LANGS is ON for the V4 (its net worker is fragile at ~95%
internal RAM, so the Store is not dependable there and it would otherwise be
stuck on English) and OFF elsewhere. Cost is FLASH only, no RAM:
  V4     71.7% -> 85.5%
  T-Deck 75.5% (unchanged, gate off)

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

113 lines
4.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Bake deploy/apps/lang/*.lang into a compiled-in fallback table.
Boards that can reach the Lua Store download their language as a file. Boards
that cannot (the V4: ~14 KB of free internal heap, so the net worker and the
store are unreliable there) still need translations, and before this they were
stuck on English.
This generates src/ui-touch/i18n_builtin.h from the SAME .lang files the store
serves, so the two can never drift: translators edit deploy/apps/lang/*.lang,
and both paths follow. Rows are emitted sorted by key so TR() can binary-search
them, and the table lives in .rodata — flash, not RAM.
Run from the repo root (a PlatformIO pre-build step does this automatically):
python3 scripts/build/gen-lang-builtin.py
"""
import glob, os, sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
LANGDIR = os.path.join(ROOT, 'deploy/apps/lang')
OUT = os.path.join(ROOT, 'src/ui-touch/i18n_builtin.h')
# Index order must match the UiLang enum in i18n.h.
CODES = ["en", "hu", "nl", "de", "fr", "es", "it",
"ru", "uk", "bg", "sr", "el", "pt-br", "ro"]
def c_escape(s):
out = []
for ch in s:
if ch == '\\': out.append('\\\\')
elif ch == '"': out.append('\\"')
elif ch == '\n': out.append('\\n')
elif ch == '\t': out.append('\\t')
elif ch == '\r': out.append('\\r')
elif ord(ch) < 0x20: out.append('\\x%02x' % ord(ch))
else: out.append(ch)
return ''.join(out)
def read_lang(path):
"""key -> value, with the file's own \\n/\\t/\\\\ escapes decoded."""
rows = {}
for line in open(path, encoding='utf-8'):
line = line.rstrip('\n').rstrip('\r')
if '\t' not in line: # header/comment lines carry no tab
continue
k, v = line.split('\t', 1)
def un(t):
o, i = [], 0
while i < len(t):
if t[i] == '\\' and i + 1 < len(t):
n = t[i + 1]
o.append('\n' if n == 'n' else '\t' if n == 't' else n)
i += 2
else:
o.append(t[i]); i += 1
return ''.join(o)
k, v = un(k), un(v)
if k and v:
rows[k] = v
return rows
def main():
langs = {}
for code in CODES[1:]: # English is the key itself
p = os.path.join(LANGDIR, code + '.lang')
if os.path.exists(p):
langs[code] = read_lang(p)
if not langs:
sys.exit('no .lang files found in ' + LANGDIR)
w = ['// Generated by scripts/build/gen-lang-builtin.py — DO NOT EDIT.',
'// Source: deploy/apps/lang/*.lang (the same files the Lua Store serves).',
'// Compiled-in fallback for boards that cannot rely on downloading a language.',
'#pragma once', '#include "i18n.h"', '']
for code in CODES[1:]:
rows = langs.get(code, {})
sym = 'kBuiltin_' + code.replace('-', '_')
w.append('static const I18nPair %s[] = {' % sym)
for k in sorted(rows): # sorted: TR() binary-searches
w.append(' { "%s", "%s" },' % (c_escape(k), c_escape(rows[k])))
w.append('};')
w.append('')
w.append('static const I18nPair* const kBuiltinLang[LANG_COUNT] = {')
w.append(' nullptr,') # LANG_EN: the key is the English
for code in CODES[1:]:
sym = 'kBuiltin_' + code.replace('-', '_')
w.append(' %s,' % (sym if langs.get(code) else 'nullptr'))
w.append('};')
w.append('static const int kBuiltinLangCount[LANG_COUNT] = {')
w.append(' 0,')
for code in CODES[1:]:
sym = 'kBuiltin_' + code.replace('-', '_')
w.append(' %s,' % (('(int)(sizeof(%s)/sizeof(%s[0]))' % (sym, sym))
if langs.get(code) else '0'))
w.append('};')
w.append('')
open(OUT, 'w', encoding='utf-8').write('\n'.join(w))
total = sum(len(r) for r in langs.values())
print('%s: %d languages, %d rows, %d bytes' %
(os.path.relpath(OUT, ROOT), len(langs), total, os.path.getsize(OUT)))
if __name__ == '__main__':
main()