mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-08-06 23:49:53 +00:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
461b9c47e5
commit
efe77a67e3
@@ -0,0 +1,112 @@
|
||||
#!/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()
|
||||
@@ -263,3 +263,16 @@
|
||||
#ifndef CAP_LUA_APPS
|
||||
#define CAP_LUA_APPS 1
|
||||
#endif
|
||||
|
||||
// Compile the translations into the image (i18n_builtin.h, generated from
|
||||
// deploy/apps/lang/*.lang) instead of relying on downloading a .lang file.
|
||||
// ON for boards where the Lua Store is not dependable — the V4 runs at ~95%
|
||||
// internal RAM with Wi-Fi up, so its net worker and the store are fragile and
|
||||
// it would otherwise be stuck on English. Costs ~400 KB of FLASH, no RAM.
|
||||
#ifndef CAP_BUILTIN_LANGS
|
||||
#if defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_LORA_V4)
|
||||
#define CAP_BUILTIN_LANGS 1
|
||||
#else
|
||||
#define CAP_BUILTIN_LANGS 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "i18n.h"
|
||||
#include <string.h>
|
||||
#include "device_caps.h"
|
||||
#if CAP_BUILTIN_LANGS
|
||||
#include "i18n_builtin.h" // generated: baked-in translations
|
||||
#endif
|
||||
|
||||
// Native language names shown in the picker. Index order == UiLang.
|
||||
const char* const kUiLangNames[LANG_COUNT] = {
|
||||
@@ -38,7 +42,11 @@ bool i18nHasFileOverlay() { return s_file_n > 0; }
|
||||
|
||||
const char* TR(const char* en) {
|
||||
if (!en) return "";
|
||||
#if CAP_BUILTIN_LANGS
|
||||
if (!s_file_n && s_ui_lang == LANG_EN) return en;
|
||||
#else
|
||||
if (!s_file_n) return en; // no language file loaded: the keys ARE the English UI
|
||||
#endif
|
||||
// Icon-prefixed labels ("<glyph> Copy") carry the LVGL symbol's UTF-8 bytes
|
||||
// (3-byte private-use sequences, 0xEE/0xEF lead) in the lookup key, but the
|
||||
// table is keyed on the plain text — so those labels never matched and the
|
||||
@@ -62,6 +70,18 @@ const char* TR(const char* en) {
|
||||
if (c < 0) hi = mid - 1; else lo = mid + 1;
|
||||
}
|
||||
}
|
||||
#if CAP_BUILTIN_LANGS
|
||||
if (!v && s_ui_lang != LANG_EN && kBuiltinLang[s_ui_lang]) {
|
||||
const I18nPair* tab = kBuiltinLang[s_ui_lang];
|
||||
int lo = 0, hi = kBuiltinLangCount[s_ui_lang] - 1;
|
||||
while (lo <= hi) { // sorted by the generator
|
||||
const int mid = (lo + hi) / 2;
|
||||
const int c = strcmp(base, tab[mid].key);
|
||||
if (c == 0) { v = tab[mid].val; break; }
|
||||
if (c < 0) hi = mid - 1; else lo = mid + 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!v) return en; // untranslated: original, prefix intact
|
||||
if (plen == 0) return v; // plain key: the table cell directly
|
||||
static char ring[4][120];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user