#!/usr/bin/env node /** * Generates the generic language reference (ISO code -> flag + label) for the C# (Blazor) client. * * Input: core/models/src/defaults/Languages.ts (compiled to dist/defaults/index.js) * Output: apps/server/AliasVault.Client/Main/Models/Languages.cs (C#) * * The TypeScript clients (browser extension + mobile) import the metadata directly from the * distributed `@/utils/dist/core/models/defaults`, so only the C# variant needs to be generated. */ const fs = require('fs'); const path = require('path'); const { pathToFileURL } = require('url'); const REPO_ROOT = path.join(__dirname, '../../..'); const TS_SOURCE_REL = 'core/models/src/defaults/Languages.ts'; const DIST_ENTRY = path.join(__dirname, '../dist/defaults/index.js'); const CS_OUTPUT = path.join(REPO_ROOT, 'apps/server/AliasVault.Client/Main/Models/Languages.cs'); /** Escape a string for embedding inside a C# double-quoted literal. */ function csString(value) { return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } function generateCSharp(languages, defaultCode) { const tuples = languages .map((l) => { const alts = (l.alternativeCodes ?? []).map((c) => `"${csString(c)}"`).join(', '); return ` ("${csString(l.code)}", "${l.flag}", "${csString(l.label)}", [${alts}]),`; }) .join('\n'); return `// // This file is auto-generated from ${TS_SOURCE_REL} // Do not edit this file directly. Run 'core/models/build.sh' (or // 'node core/models/scripts/generate-languages.cjs') to regenerate. #nullable enable namespace AliasVault.Client.Main.Models; /// /// Generic language reference shared across all AliasVault clients: maps a two-letter ISO 639-1 /// code to a flag, native label, and the region-variant locale codes that map onto it. /// public static class Languages { /// /// Default ISO language code used as the universal fallback. /// public const string DefaultLanguageCode = "${csString(defaultCode)}"; /// /// Known languages keyed by ISO 639-1 code, with a flag, native label, and the BCP-47 /// region-variant locale codes (e.g. "en-US", "en-GB") that resolve onto the code. /// private static readonly (string Code, string Flag, string Label, string[] AlternativeCodes)[] Meta = [ ${tuples} ]; /// /// Normalize an app/UI language tag to a two-letter lowercase ISO code (e.g. "nl-NL" -> "nl"). /// /// The language tag. /// The two-letter lowercase code. public static string Normalize(string? code) { var value = code ?? string.Empty; return (value.Length >= 2 ? value.Substring(0, 2) : value).ToLowerInvariant(); } /// /// Get the display label (flag + native name) for an ISO language code. Falls back to a globe /// flag and the raw code for unknown languages. /// /// The ISO language code (case-insensitive). /// A display string such as "🇳🇱 Nederlands". public static string GetDisplayLabel(string code) { var iso = Normalize(code); foreach (var (metaCode, flag, label, _) in Meta) { if (metaCode == iso) { return $"{flag} {label}"; } } return $"🌐 {code}"; } /// /// Get the flag emoji for an ISO language code. Falls back to a globe flag for unknown languages. /// /// The ISO language code (case-insensitive). /// The flag emoji, e.g. "🇳🇱". public static string GetFlag(string code) { var iso = Normalize(code); foreach (var (metaCode, flag, _, _) in Meta) { if (metaCode == iso) { return flag; } } return "🌐"; } /// /// Get the native display label for an ISO language code. Falls back to the raw code for unknown /// languages. /// /// The ISO language code (case-insensitive). /// The native label, e.g. "Nederlands". public static string GetLabel(string code) { var iso = Normalize(code); foreach (var (metaCode, _, label, _) in Meta) { if (metaCode == iso) { return label; } } return code; } /// /// Match an app/UI/browser locale to one of a feature's available ISO codes, using the /// region-variant alternative codes (e.g. "en-GB" -> "en", "de-CH" -> "de"). Matching order: /// exact match against an available code, then the alternative-code table, then the base /// language code. Returns null when nothing matches, so callers can decide their own fallback. /// /// The app/UI/browser language tag (e.g. "en", "en-US", "nl-BE"). /// The codes the feature actually supports. /// The matching available code or null if none matched. public static string? MatchAvailableLanguage(string? appLanguage, System.Collections.Generic.IReadOnlyList availableCodes) { if (string.IsNullOrEmpty(appLanguage)) { return null; } var lower = appLanguage.ToLowerInvariant(); // 1. Exact match against an available code. foreach (var code in availableCodes) { if (string.Equals(code, lower, System.StringComparison.OrdinalIgnoreCase)) { return code; } } // 2. Alternative-code match: find the language whose region variants include this tag, then // return its base code if the feature supports it. foreach (var (metaCode, _, _, alternativeCodes) in Meta) { var isAlternative = false; foreach (var alt in alternativeCodes) { if (string.Equals(alt, lower, System.StringComparison.OrdinalIgnoreCase)) { isAlternative = true; break; } } if (isAlternative) { foreach (var code in availableCodes) { if (string.Equals(code, metaCode, System.StringComparison.OrdinalIgnoreCase)) { return code; } } } } // 3. Base language code match (e.g. an unlisted "en-ZZ" still resolves to "en"). var baseCode = Normalize(appLanguage); foreach (var code in availableCodes) { if (string.Equals(code, baseCode, System.StringComparison.OrdinalIgnoreCase)) { return code; } } return null; } /// /// Resolve a default language code for an app/UI language, restricted to a set of available /// codes. Uses (region-variant aware), then falls back to /// the first available code, otherwise English. /// /// The app/UI language tag. /// The codes the feature actually supports. /// The resolved ISO code. public static string ResolveDefaultLanguage(string? appLanguage, System.Collections.Generic.IReadOnlyList availableCodes) { return MatchAvailableLanguage(appLanguage, availableCodes) ?? (availableCodes.Count > 0 ? availableCodes[0] : DefaultLanguageCode); } } `; } function writeIfChanged(filePath, contents) { const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null; if (existing === contents) { console.log(` ✓ up to date: ${path.relative(REPO_ROOT, filePath)}`); return; } fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, contents); console.log(` ✎ wrote: ${path.relative(REPO_ROOT, filePath)}`); } async function main() { // The dist bundle is ESM, so load it via dynamic import. const mod = await import(pathToFileURL(DIST_ENTRY).href); const languages = mod.LANGUAGES; const defaultCode = mod.DEFAULT_LANGUAGE_CODE; if (!Array.isArray(languages) || languages.length === 0) { throw new Error(`LANGUAGES not found or empty in ${DIST_ENTRY}. Run 'npm run build' first.`); } console.log(`Generating language reference from ${TS_SOURCE_REL}:`); console.log(` ${languages.map((l) => l.code).join(', ')}`); writeIfChanged(CS_OUTPUT, generateCSharp(languages, defaultCode)); } main().catch((err) => { console.error(err); process.exit(1); });