build web with optional ui languages

This commit is contained in:
proddy
2026-06-20 13:10:56 +02:00
parent 39b02591d9
commit f2eb1b2e7f
3 changed files with 71 additions and 3 deletions

View File

@@ -46,6 +46,38 @@ ${fileInfo.map((f) => `${INDENT}{"${f.uri}", "${f.mimeType}", ${f.variable}, ${f
static constexpr size_t WWW_ASSETS_COUNT = sizeof(WWW_ASSETS) / sizeof(WWW_ASSETS[0]);
`;
// Optional locale allow-list, shared with the Vite build via VITE_APP_LOCALES
// (e.g. "en,de,nl"). When set, locale chunks outside the list are NOT embedded
// into firmware flash. `en` is always kept as the fallback. Unset => embed all.
const ALL_LOCALES = [
'cz',
'de',
'en',
'fr',
'it',
'nl',
'no',
'pl',
'sk',
'sv',
'tr'
];
const localeAllowList = (process.env.VITE_APP_LOCALES || '')
.split(',')
.map((locale) => locale.trim())
.filter(Boolean);
const isExcludedLocaleChunk = (relativeFilePath) => {
if (localeAllowList.length === 0) return false;
const base = relativeFilePath.split(sep).pop();
const match = /^([a-z]{2})-[A-Za-z0-9_-]+\.js$/.exec(base);
if (!match) return false;
const code = match[1];
// Only treat known locale codes as locale chunks; never drop the en fallback.
if (!ALL_LOCALES.includes(code) || code === 'en') return false;
return !localeAllowList.includes(code);
};
const getFilesSync = (dir, files = []) => {
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
const entryPath = resolve(dir, entry.name);
@@ -116,7 +148,12 @@ writeStream.write(ARDUINO_INCLUDES);
const buildPath = resolve(sourcePath);
for (const filePath of getFilesSync(buildPath)) {
writeFile(relative(buildPath, filePath), readFileSync(filePath));
const relativeFilePath = relative(buildPath, filePath);
if (isExcludedLocaleChunk(relativeFilePath)) {
console.log(`Skipping locale (not in VITE_APP_LOCALES): ${relativeFilePath}`);
continue;
}
writeFile(relativeFilePath, readFileSync(filePath));
}
writeStream.write(generateWWWClass());

View File

@@ -9,7 +9,7 @@ import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors';
const AVAILABLE_LOCALES = [
const ALL_LOCALES = [
'de',
'en',
'it',
@@ -23,6 +23,20 @@ const AVAILABLE_LOCALES = [
'cz'
] as Locales[];
// Optional build-time allow-list (e.g. VITE_APP_LOCALES="en,de,nl"). When unset,
// every locale is available. `en` is always kept as the fallback locale, and the
// progmem generator embeds the matching subset into firmware flash.
const localeAllowList = (import.meta.env.VITE_APP_LOCALES ?? '')
.split(',')
.map((locale) => locale.trim())
.filter(Boolean);
const AVAILABLE_LOCALES: Locales[] = localeAllowList.length
? ALL_LOCALES.filter(
(locale) => locale === 'en' || localeAllowList.includes(locale)
)
: ALL_LOCALES;
const App = memo(() => {
const [wasLoaded, setWasLoaded] = useState(false);
const [locale, setLocale] = useState<Locales>('en');
@@ -30,7 +44,12 @@ const App = memo(() => {
useEffect(() => {
const initializeLocale = async () => {
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
const stored = localStorage.getItem('lang');
// Ignore a stored locale that isn't available (e.g. trimmed from this build).
const newLocale =
stored && AVAILABLE_LOCALES.includes(stored as Locales)
? (stored as Locales)
: browserLocale;
localStorage.setItem('lang', newLocale);
setLocale(newLocale);
await loadLocaleAsync(newLocale);

View File

@@ -1 +1,13 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
// Optional comma-separated allow-list of locales to ship (e.g. "en,de,nl").
// Unset => all locales are bundled and embedded. `en` is always kept as the
// fallback. Consumed by App.tsx (UI language list) and progmem-generator.js
// (which locale chunks get embedded into firmware flash).
readonly VITE_APP_LOCALES?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}