diff --git a/interface/progmem-generator.js b/interface/progmem-generator.js index be5cead21..79f4f4f18 100644 --- a/interface/progmem-generator.js +++ b/interface/progmem-generator.js @@ -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()); diff --git a/interface/src/App.tsx b/interface/src/App.tsx index 9149eb940..8db9f2f28 100644 --- a/interface/src/App.tsx +++ b/interface/src/App.tsx @@ -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('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); diff --git a/interface/src/vite-env.d.ts b/interface/src/vite-env.d.ts index 11f02fe2a..c5d41d92f 100644 --- a/interface/src/vite-env.d.ts +++ b/interface/src/vite-env.d.ts @@ -1 +1,13 @@ /// + +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; +}