mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-07-31 09:17:11 -04:00
247 lines
8.8 KiB
JavaScript
247 lines
8.8 KiB
JavaScript
#!/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 `// <auto-generated />
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static class Languages
|
|
{
|
|
/// <summary>
|
|
/// Default ISO language code used as the universal fallback.
|
|
/// </summary>
|
|
public const string DefaultLanguageCode = "${csString(defaultCode)}";
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private static readonly (string Code, string Flag, string Label, string[] AlternativeCodes)[] Meta =
|
|
[
|
|
${tuples}
|
|
];
|
|
|
|
/// <summary>
|
|
/// Normalize an app/UI language tag to a two-letter lowercase ISO code (e.g. "nl-NL" -> "nl").
|
|
/// </summary>
|
|
/// <param name="code">The language tag.</param>
|
|
/// <returns>The two-letter lowercase code.</returns>
|
|
public static string Normalize(string? code)
|
|
{
|
|
var value = code ?? string.Empty;
|
|
return (value.Length >= 2 ? value.Substring(0, 2) : value).ToLowerInvariant();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="code">The ISO language code (case-insensitive).</param>
|
|
/// <returns>A display string such as "🇳🇱 Nederlands".</returns>
|
|
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}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the flag emoji for an ISO language code. Falls back to a globe flag for unknown languages.
|
|
/// </summary>
|
|
/// <param name="code">The ISO language code (case-insensitive).</param>
|
|
/// <returns>The flag emoji, e.g. "🇳🇱".</returns>
|
|
public static string GetFlag(string code)
|
|
{
|
|
var iso = Normalize(code);
|
|
foreach (var (metaCode, flag, _, _) in Meta)
|
|
{
|
|
if (metaCode == iso)
|
|
{
|
|
return flag;
|
|
}
|
|
}
|
|
|
|
return "🌐";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the native display label for an ISO language code. Falls back to the raw code for unknown
|
|
/// languages.
|
|
/// </summary>
|
|
/// <param name="code">The ISO language code (case-insensitive).</param>
|
|
/// <returns>The native label, e.g. "Nederlands".</returns>
|
|
public static string GetLabel(string code)
|
|
{
|
|
var iso = Normalize(code);
|
|
foreach (var (metaCode, _, label, _) in Meta)
|
|
{
|
|
if (metaCode == iso)
|
|
{
|
|
return label;
|
|
}
|
|
}
|
|
|
|
return code;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="appLanguage">The app/UI/browser language tag (e.g. "en", "en-US", "nl-BE").</param>
|
|
/// <param name="availableCodes">The codes the feature actually supports.</param>
|
|
/// <returns>The matching available code or null if none matched.</returns>
|
|
public static string? MatchAvailableLanguage(string? appLanguage, System.Collections.Generic.IReadOnlyList<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve a default language code for an app/UI language, restricted to a set of available
|
|
/// codes. Uses <see cref="MatchAvailableLanguage"/> (region-variant aware), then falls back to
|
|
/// the first available code, otherwise English.
|
|
/// </summary>
|
|
/// <param name="appLanguage">The app/UI language tag.</param>
|
|
/// <param name="availableCodes">The codes the feature actually supports.</param>
|
|
/// <returns>The resolved ISO code.</returns>
|
|
public static string ResolveDefaultLanguage(string? appLanguage, System.Collections.Generic.IReadOnlyList<string> 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);
|
|
});
|