mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-08-01 17:59:40 -04:00
Refactor shared language lists to avoid duplication (#776)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MIN_WORD_COUNT, MAX_WORD_COUNT, DEFAULT_WORD_COUNT, DEFAULT_LANGUAGE_CODE, getLanguageInfo } from '@/utils/dist/core/models/defaults';
|
||||
import { MIN_WORD_COUNT, MAX_WORD_COUNT, DEFAULT_WORD_COUNT, getLanguageInfo, resolveDefaultLanguage } from '@/utils/dist/core/models/defaults';
|
||||
import type {
|
||||
PasswordSettings,
|
||||
PasswordGeneratorType,
|
||||
@@ -103,7 +103,9 @@ const PasswordConfigForm: React.FC<IPasswordConfigFormProps> = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isDiceware = (settings.Type ?? 'basic') === 'diceware';
|
||||
const currentLanguage = settings.Language ?? DEFAULT_LANGUAGE_CODE;
|
||||
const currentLanguage = (settings.Language && settings.Language.length > 0)
|
||||
? settings.Language
|
||||
: resolveDefaultLanguage(navigator.language, dicewareLanguages);
|
||||
|
||||
/**
|
||||
* Human label for a Diceware option value (used in button tooltips).
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
import { resolveDefaultLanguage } from '@/utils/dist/core/models/defaults';
|
||||
import type { Item, PasswordSettings } from '@/utils/dist/core/models/vault';
|
||||
import { FieldKey } from '@/utils/dist/core/models/vault';
|
||||
import initWasm, * as core from '@/utils/dist/core/rust/aliasvault_core.js';
|
||||
@@ -70,10 +71,24 @@ export async function extractRootDomain(domain: string): Promise<string> {
|
||||
*/
|
||||
export async function generatePassword(settings: PasswordSettings, seed?: string): Promise<string> {
|
||||
await initRustCore();
|
||||
const payload = seed ? { ...settings, Seed: seed } : settings;
|
||||
const effective = await applyEffectiveDicewareLanguage(settings);
|
||||
const payload = seed ? { ...effective, Seed: seed } : effective;
|
||||
return core.generatePassword(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective Diceware passphrase language when none is explicitly chosen.
|
||||
*
|
||||
* The passphrase language is left empty by default ("auto").
|
||||
*/
|
||||
async function applyEffectiveDicewareLanguage(settings: PasswordSettings): Promise<PasswordSettings> {
|
||||
if (settings.Type !== 'diceware' || (settings.Language && settings.Language.trim().length > 0)) {
|
||||
return settings;
|
||||
}
|
||||
const codes = await getDicewareLanguages();
|
||||
return { ...settings, Language: resolveDefaultLanguage(navigator.language, codes) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of bundled Diceware wordlist language ISO codes (first is the default, 'en').
|
||||
* The set is owned by the Rust core; unknown codes fall back to English during generation.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DEFAULT_PASSWORD_LENGTH, DEFAULT_WORD_COUNT, DEFAULT_LANGUAGE_CODE } from '@/utils/dist/core/models/defaults';
|
||||
import { getAvailableLanguages } from '@/utils/dist/core/identity-generator';
|
||||
import { DEFAULT_PASSWORD_LENGTH, DEFAULT_WORD_COUNT, DEFAULT_LANGUAGE_CODE, matchAvailableLanguage } from '@/utils/dist/core/models/defaults';
|
||||
import type { EncryptionKey, PasswordSettings, TotpCode, Attachment } from '@/utils/dist/core/models/vault';
|
||||
|
||||
import { BaseRepository } from '../BaseRepository';
|
||||
@@ -69,7 +70,8 @@ export class SettingsRepository extends BaseRepository {
|
||||
UseNonAmbiguousChars: false,
|
||||
Type: 'basic',
|
||||
WordCount: DEFAULT_WORD_COUNT,
|
||||
Language: DEFAULT_LANGUAGE_CODE,
|
||||
// Empty = "auto": the passphrase language is resolved from the app language during runtime.
|
||||
Language: '',
|
||||
Capitalization: 'Lowercase',
|
||||
Separator: 'Dash',
|
||||
Salt: 'None'
|
||||
@@ -151,7 +153,9 @@ export class SettingsRepository extends BaseRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective identity language, falling back to browser language.
|
||||
* Get the effective identity language. Uses the explicit override when set, otherwise matches the
|
||||
* browser language to one of the identity generator's available languages via the shared
|
||||
* region-variant alternative-code table (e.g. "de-CH" -> "de"), falling back to English.
|
||||
* @returns The effective language code
|
||||
*/
|
||||
public getEffectiveIdentityLanguage(): string {
|
||||
@@ -159,8 +163,7 @@ export class SettingsRepository extends BaseRepository {
|
||||
if (storedLanguage) {
|
||||
return storedLanguage;
|
||||
}
|
||||
// Fall back to browser language (first two characters)
|
||||
return navigator.language.substring(0, 2);
|
||||
return matchAvailableLanguage(navigator.language, getAvailableLanguages()) ?? DEFAULT_LANGUAGE_CODE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -399,20 +399,6 @@ declare function convertAgeRangeToBirthdateOptions(ageRange: string): IBirthdate
|
||||
* @returns Array of ISO language codes (e.g. ["da", "de", "en", ...]).
|
||||
*/
|
||||
declare function getAvailableLanguages(): string[];
|
||||
/**
|
||||
* Maps a UI language code to an identity generator language code.
|
||||
* If no explicit match is found, returns null to indicate no preference.
|
||||
*
|
||||
* @param uiLanguageCode - The UI language code (e.g., "en", "en-US", "nl-NL", "de-DE", "fr")
|
||||
* @returns The matching identity generator language code or null if no match
|
||||
*
|
||||
* @example
|
||||
* mapUiLanguageToIdentityLanguage("en-US") // returns "en"
|
||||
* mapUiLanguageToIdentityLanguage("nl") // returns "nl"
|
||||
* mapUiLanguageToIdentityLanguage("de-CH") // returns "de"
|
||||
* mapUiLanguageToIdentityLanguage("ja") // returns null (no Japanese identity generator)
|
||||
*/
|
||||
declare function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null;
|
||||
|
||||
/**
|
||||
* Creates a new identity generator based on the language.
|
||||
@@ -429,4 +415,4 @@ declare const CreateIdentityGenerator: (language: string) => IdentityGenerator;
|
||||
*/
|
||||
declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator;
|
||||
|
||||
export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type Identity, IdentityGenerator, IdentityGeneratorDa, IdentityGeneratorDe, IdentityGeneratorEn, IdentityGeneratorEs, IdentityGeneratorFa, IdentityGeneratorFr, IdentityGeneratorIt, IdentityGeneratorNl, IdentityGeneratorRo, IdentityGeneratorSv, IdentityGeneratorUr, IdentityHelperUtils, UsernameEmailGenerator, convertAgeRangeToBirthdateOptions, getAvailableAgeRanges, getAvailableLanguages, mapUiLanguageToIdentityLanguage };
|
||||
export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type Identity, IdentityGenerator, IdentityGeneratorDa, IdentityGeneratorDe, IdentityGeneratorEn, IdentityGeneratorEs, IdentityGeneratorFa, IdentityGeneratorFr, IdentityGeneratorIt, IdentityGeneratorNl, IdentityGeneratorRo, IdentityGeneratorSv, IdentityGeneratorUr, IdentityHelperUtils, UsernameEmailGenerator, convertAgeRangeToBirthdateOptions, getAvailableAgeRanges, getAvailableLanguages };
|
||||
|
||||
@@ -42,8 +42,7 @@ __export(index_exports, {
|
||||
UsernameEmailGenerator: () => UsernameEmailGenerator,
|
||||
convertAgeRangeToBirthdateOptions: () => convertAgeRangeToBirthdateOptions,
|
||||
getAvailableAgeRanges: () => getAvailableAgeRanges,
|
||||
getAvailableLanguages: () => getAvailableLanguages,
|
||||
mapUiLanguageToIdentityLanguage: () => mapUiLanguageToIdentityLanguage
|
||||
getAvailableLanguages: () => getAvailableLanguages
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
@@ -11763,43 +11762,21 @@ function convertAgeRangeToBirthdateOptions(ageRange) {
|
||||
}
|
||||
|
||||
// src/utils/LanguageProvider.ts
|
||||
var LANGUAGE_DEFS = [
|
||||
{ value: "da", alternativeCodes: ["da-DK"] },
|
||||
{ value: "de", alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] },
|
||||
{ value: "en", alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] },
|
||||
{ value: "es", alternativeCodes: ["es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-EC", "es-GT", "es-CU", "es-BO", "es-DO", "es-HN", "es-PY", "es-SV", "es-NI", "es-CR", "es-PA", "es-UY", "es-PR"] },
|
||||
{ value: "fr", alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] },
|
||||
{ value: "it", alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] },
|
||||
{ value: "nl", alternativeCodes: ["nl-NL", "nl-BE"] },
|
||||
{ value: "ro", alternativeCodes: ["ro-RO", "ro-MD"] },
|
||||
{ value: "sv", alternativeCodes: ["sv-SE", "sv-FI"] },
|
||||
{ value: "ur", alternativeCodes: ["ur-PK", "ur-IN"] },
|
||||
{ value: "fa", alternativeCodes: ["fa-IR", "fa-AF"] }
|
||||
var SUPPORTED_LANGUAGE_CODES = [
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"nl",
|
||||
"ro",
|
||||
"sv",
|
||||
"ur",
|
||||
"fa"
|
||||
];
|
||||
function getAvailableLanguages() {
|
||||
return LANGUAGE_DEFS.map((lang) => lang.value);
|
||||
}
|
||||
function mapUiLanguageToIdentityLanguage(uiLanguageCode) {
|
||||
if (!uiLanguageCode) {
|
||||
return null;
|
||||
}
|
||||
const normalizedCode = uiLanguageCode.toLowerCase();
|
||||
const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode);
|
||||
if (exactMatch) {
|
||||
return exactMatch.value;
|
||||
}
|
||||
const alternativeMatch = LANGUAGE_DEFS.find(
|
||||
(lang) => lang.alternativeCodes?.some((code) => code.toLowerCase() === normalizedCode)
|
||||
);
|
||||
if (alternativeMatch) {
|
||||
return alternativeMatch.value;
|
||||
}
|
||||
const baseCode = normalizedCode.split("-")[0];
|
||||
const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode);
|
||||
if (baseMatch) {
|
||||
return baseMatch.value;
|
||||
}
|
||||
return null;
|
||||
return [...SUPPORTED_LANGUAGE_CODES];
|
||||
}
|
||||
|
||||
// src/factories/IdentityGeneratorFactory.ts
|
||||
@@ -11858,6 +11835,5 @@ var CreateUsernameEmailGenerator = () => {
|
||||
UsernameEmailGenerator,
|
||||
convertAgeRangeToBirthdateOptions,
|
||||
getAvailableAgeRanges,
|
||||
getAvailableLanguages,
|
||||
mapUiLanguageToIdentityLanguage
|
||||
getAvailableLanguages
|
||||
});
|
||||
|
||||
@@ -11718,43 +11718,21 @@ function convertAgeRangeToBirthdateOptions(ageRange) {
|
||||
}
|
||||
|
||||
// src/utils/LanguageProvider.ts
|
||||
var LANGUAGE_DEFS = [
|
||||
{ value: "da", alternativeCodes: ["da-DK"] },
|
||||
{ value: "de", alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] },
|
||||
{ value: "en", alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] },
|
||||
{ value: "es", alternativeCodes: ["es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-EC", "es-GT", "es-CU", "es-BO", "es-DO", "es-HN", "es-PY", "es-SV", "es-NI", "es-CR", "es-PA", "es-UY", "es-PR"] },
|
||||
{ value: "fr", alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] },
|
||||
{ value: "it", alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] },
|
||||
{ value: "nl", alternativeCodes: ["nl-NL", "nl-BE"] },
|
||||
{ value: "ro", alternativeCodes: ["ro-RO", "ro-MD"] },
|
||||
{ value: "sv", alternativeCodes: ["sv-SE", "sv-FI"] },
|
||||
{ value: "ur", alternativeCodes: ["ur-PK", "ur-IN"] },
|
||||
{ value: "fa", alternativeCodes: ["fa-IR", "fa-AF"] }
|
||||
var SUPPORTED_LANGUAGE_CODES = [
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"nl",
|
||||
"ro",
|
||||
"sv",
|
||||
"ur",
|
||||
"fa"
|
||||
];
|
||||
function getAvailableLanguages() {
|
||||
return LANGUAGE_DEFS.map((lang) => lang.value);
|
||||
}
|
||||
function mapUiLanguageToIdentityLanguage(uiLanguageCode) {
|
||||
if (!uiLanguageCode) {
|
||||
return null;
|
||||
}
|
||||
const normalizedCode = uiLanguageCode.toLowerCase();
|
||||
const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode);
|
||||
if (exactMatch) {
|
||||
return exactMatch.value;
|
||||
}
|
||||
const alternativeMatch = LANGUAGE_DEFS.find(
|
||||
(lang) => lang.alternativeCodes?.some((code) => code.toLowerCase() === normalizedCode)
|
||||
);
|
||||
if (alternativeMatch) {
|
||||
return alternativeMatch.value;
|
||||
}
|
||||
const baseCode = normalizedCode.split("-")[0];
|
||||
const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode);
|
||||
if (baseMatch) {
|
||||
return baseMatch.value;
|
||||
}
|
||||
return null;
|
||||
return [...SUPPORTED_LANGUAGE_CODES];
|
||||
}
|
||||
|
||||
// src/factories/IdentityGeneratorFactory.ts
|
||||
@@ -11812,6 +11790,5 @@ export {
|
||||
UsernameEmailGenerator,
|
||||
convertAgeRangeToBirthdateOptions,
|
||||
getAvailableAgeRanges,
|
||||
getAvailableLanguages,
|
||||
mapUiLanguageToIdentityLanguage
|
||||
getAvailableLanguages
|
||||
};
|
||||
|
||||
@@ -22,8 +22,8 @@ declare const MIN_WORD_COUNT = 3;
|
||||
declare const MAX_WORD_COUNT = 10;
|
||||
|
||||
/**
|
||||
* Generic, cross-platform language reference: maps a two-letter ISO 639-1 code to a flag and a
|
||||
* native display label.
|
||||
* Generic, cross-platform language reference: maps a two-letter ISO 639-1 code to a flag, a native
|
||||
* display label, and the alternative locale codes (BCP-47 region variants) that map onto it
|
||||
*/
|
||||
/**
|
||||
* Display metadata for a single language.
|
||||
@@ -35,12 +35,14 @@ interface ILanguageInfo {
|
||||
flag: string;
|
||||
/** Native display label. */
|
||||
label: string;
|
||||
/** Alternative locale codes (BCP-47 language-region tags) that map onto this language. */
|
||||
alternativeCodes?: string[];
|
||||
}
|
||||
/** Default ISO language code used as the universal fallback. */
|
||||
declare const DEFAULT_LANGUAGE_CODE = "en";
|
||||
/**
|
||||
* Known languages keyed by ISO 639-1 code, with a flag and native label.
|
||||
* Covers the AliasVault app UI languages so this list can be reused beyond a single feature.
|
||||
* Known languages keyed by ISO 639-1 code, with a flag, native label, and the region-variant locale
|
||||
* codes that map onto each.
|
||||
*/
|
||||
declare const LANGUAGES: ILanguageInfo[];
|
||||
/**
|
||||
@@ -56,14 +58,32 @@ declare function normalizeLanguageCode(code: string | null | undefined): string;
|
||||
* @returns The flag + label info for the code.
|
||||
*/
|
||||
declare function getLanguageInfo(code: string): ILanguageInfo;
|
||||
/**
|
||||
* Match an app/UI/browser locale to one of a feature's available ISO codes, using the region-variant
|
||||
* alternative codes from {@link LANGUAGES} (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
|
||||
* (the part before the '-'). Returns null when nothing matches, so callers can decide their own
|
||||
* fallback (e.g. "no preference" vs. a concrete default).
|
||||
*
|
||||
* @param appLanguage The app/UI/browser language tag (e.g. 'en', 'en-US', 'nl-BE').
|
||||
* @param availableCodes The codes the feature actually supports.
|
||||
* @returns The matching available code (in its original casing) or null if none matched.
|
||||
*
|
||||
* @example
|
||||
* matchAvailableLanguage('en-US', ['en', 'nl']) // 'en'
|
||||
* matchAvailableLanguage('de-CH', ['de', 'en']) // 'de'
|
||||
* matchAvailableLanguage('ja', ['en', 'nl']) // null
|
||||
*/
|
||||
declare function matchAvailableLanguage(appLanguage: string | null | undefined, availableCodes: string[]): string | null;
|
||||
/**
|
||||
* Resolve a default language code for an app/UI language, restricted to a set of available codes
|
||||
* (e.g. the Diceware wordlist languages returned by the Rust core). Returns the app language when it
|
||||
* is available, otherwise the first available code, otherwise English.
|
||||
* @param appLanguage The app/UI language tag.
|
||||
* (e.g. the Diceware wordlist languages returned by the Rust core, or the identity generator's
|
||||
* supported languages). Uses {@link matchAvailableLanguage} (region-variant aware), then falls back
|
||||
* to the first available code, otherwise English.
|
||||
* @param appLanguage The app/UI/browser language tag.
|
||||
* @param availableCodes The codes the feature actually supports.
|
||||
* @returns The resolved ISO code.
|
||||
*/
|
||||
declare function resolveDefaultLanguage(appLanguage: string | null | undefined, availableCodes: string[]): string;
|
||||
|
||||
export { DEFAULT_LANGUAGE_CODE, DEFAULT_PASSWORD_LENGTH, DEFAULT_WORD_COUNT, type ILanguageInfo, LANGUAGES, MAX_PASSWORD_LENGTH, MAX_WORD_COUNT, MIN_PASSWORD_LENGTH, MIN_WORD_COUNT, getLanguageInfo, normalizeLanguageCode, resolveDefaultLanguage };
|
||||
export { DEFAULT_LANGUAGE_CODE, DEFAULT_PASSWORD_LENGTH, DEFAULT_WORD_COUNT, type ILanguageInfo, LANGUAGES, MAX_PASSWORD_LENGTH, MAX_WORD_COUNT, MIN_PASSWORD_LENGTH, MIN_WORD_COUNT, getLanguageInfo, matchAvailableLanguage, normalizeLanguageCode, resolveDefaultLanguage };
|
||||
|
||||
@@ -13,24 +13,24 @@ var MAX_WORD_COUNT = 10;
|
||||
// src/defaults/Languages.ts
|
||||
var DEFAULT_LANGUAGE_CODE = "en";
|
||||
var LANGUAGES = [
|
||||
{ code: "en", flag: "\u{1F1FA}\u{1F1F8}", label: "English" },
|
||||
{ code: "nl", flag: "\u{1F1F3}\u{1F1F1}", label: "Nederlands" },
|
||||
{ code: "de", flag: "\u{1F1E9}\u{1F1EA}", label: "Deutsch" },
|
||||
{ code: "fr", flag: "\u{1F1EB}\u{1F1F7}", label: "Fran\xE7ais" },
|
||||
{ code: "es", flag: "\u{1F1EA}\u{1F1F8}", label: "Espa\xF1ol" },
|
||||
{ code: "it", flag: "\u{1F1EE}\u{1F1F9}", label: "Italiano" },
|
||||
{ code: "da", flag: "\u{1F1E9}\u{1F1F0}", label: "Dansk" },
|
||||
{ code: "fi", flag: "\u{1F1EB}\u{1F1EE}", label: "Suomi" },
|
||||
{ code: "he", flag: "\u{1F1EE}\u{1F1F1}", label: "\u05E2\u05D1\u05E8\u05D9\u05EA" },
|
||||
{ code: "pl", flag: "\u{1F1F5}\u{1F1F1}", label: "Polski" },
|
||||
{ code: "pt", flag: "\u{1F1E7}\u{1F1F7}", label: "Portugu\xEAs Brasileiro" },
|
||||
{ code: "ro", flag: "\u{1F1F7}\u{1F1F4}", label: "Rom\xE2n\u0103" },
|
||||
{ code: "ru", flag: "\u{1F1F7}\u{1F1FA}", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
|
||||
{ code: "sv", flag: "\u{1F1F8}\u{1F1EA}", label: "Svenska" },
|
||||
{ code: "uk", flag: "\u{1F1FA}\u{1F1E6}", label: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
|
||||
{ code: "zh", flag: "\u{1F1E8}\u{1F1F3}", label: "\u7B80\u4F53\u4E2D\u6587" },
|
||||
{ code: "ur", flag: "\u{1F1F5}\u{1F1F0}", label: "\u0627\u0631\u062F\u0648" },
|
||||
{ code: "fa", flag: "\u{1F1EE}\u{1F1F7}", label: "\u0641\u0627\u0631\u0633\u06CC" }
|
||||
{ code: "en", flag: "\u{1F1FA}\u{1F1F8}", label: "English", alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] },
|
||||
{ code: "nl", flag: "\u{1F1F3}\u{1F1F1}", label: "Nederlands", alternativeCodes: ["nl-NL", "nl-BE"] },
|
||||
{ code: "de", flag: "\u{1F1E9}\u{1F1EA}", label: "Deutsch", alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] },
|
||||
{ code: "fr", flag: "\u{1F1EB}\u{1F1F7}", label: "Fran\xE7ais", alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] },
|
||||
{ code: "es", flag: "\u{1F1EA}\u{1F1F8}", label: "Espa\xF1ol", alternativeCodes: ["es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-EC", "es-GT", "es-CU", "es-BO", "es-DO", "es-HN", "es-PY", "es-SV", "es-NI", "es-CR", "es-PA", "es-UY", "es-PR"] },
|
||||
{ code: "it", flag: "\u{1F1EE}\u{1F1F9}", label: "Italiano", alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] },
|
||||
{ code: "da", flag: "\u{1F1E9}\u{1F1F0}", label: "Dansk", alternativeCodes: ["da-DK"] },
|
||||
{ code: "fi", flag: "\u{1F1EB}\u{1F1EE}", label: "Suomi", alternativeCodes: ["fi-FI"] },
|
||||
{ code: "he", flag: "\u{1F1EE}\u{1F1F1}", label: "\u05E2\u05D1\u05E8\u05D9\u05EA", alternativeCodes: ["he-IL"] },
|
||||
{ code: "pl", flag: "\u{1F1F5}\u{1F1F1}", label: "Polski", alternativeCodes: ["pl-PL"] },
|
||||
{ code: "pt", flag: "\u{1F1E7}\u{1F1F7}", label: "Portugu\xEAs Brasileiro", alternativeCodes: ["pt-BR", "pt-PT"] },
|
||||
{ code: "ro", flag: "\u{1F1F7}\u{1F1F4}", label: "Rom\xE2n\u0103", alternativeCodes: ["ro-RO", "ro-MD"] },
|
||||
{ code: "ru", flag: "\u{1F1F7}\u{1F1FA}", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439", alternativeCodes: ["ru-RU", "ru-BY", "ru-KZ", "ru-UA"] },
|
||||
{ code: "sv", flag: "\u{1F1F8}\u{1F1EA}", label: "Svenska", alternativeCodes: ["sv-SE", "sv-FI"] },
|
||||
{ code: "uk", flag: "\u{1F1FA}\u{1F1E6}", label: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430", alternativeCodes: ["uk-UA"] },
|
||||
{ code: "zh", flag: "\u{1F1E8}\u{1F1F3}", label: "\u7B80\u4F53\u4E2D\u6587", alternativeCodes: ["zh-CN", "zh-SG", "zh-Hans", "zh-TW", "zh-HK", "zh-MO", "zh-Hant"] },
|
||||
{ code: "ur", flag: "\u{1F1F5}\u{1F1F0}", label: "\u0627\u0631\u062F\u0648", alternativeCodes: ["ur-PK", "ur-IN"] },
|
||||
{ code: "fa", flag: "\u{1F1EE}\u{1F1F7}", label: "\u0641\u0627\u0631\u0633\u06CC", alternativeCodes: ["fa-IR", "fa-AF"] }
|
||||
];
|
||||
function normalizeLanguageCode(code) {
|
||||
return (code ?? "").slice(0, 2).toLowerCase();
|
||||
@@ -40,12 +40,31 @@ function getLanguageInfo(code) {
|
||||
const match = LANGUAGES.find((l) => l.code === iso);
|
||||
return match ?? { code, flag: "\u{1F310}", label: code };
|
||||
}
|
||||
function resolveDefaultLanguage(appLanguage, availableCodes) {
|
||||
const iso = normalizeLanguageCode(appLanguage);
|
||||
if (availableCodes.some((c) => c.toLowerCase() === iso)) {
|
||||
return iso;
|
||||
function matchAvailableLanguage(appLanguage, availableCodes) {
|
||||
if (!appLanguage) {
|
||||
return null;
|
||||
}
|
||||
return availableCodes[0] ?? DEFAULT_LANGUAGE_CODE;
|
||||
const lower = appLanguage.toLowerCase();
|
||||
const exact = availableCodes.find((c) => c.toLowerCase() === lower);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
const altEntry = LANGUAGES.find((l) => l.alternativeCodes?.some((ac) => ac.toLowerCase() === lower));
|
||||
if (altEntry) {
|
||||
const altMatch = availableCodes.find((c) => c.toLowerCase() === altEntry.code.toLowerCase());
|
||||
if (altMatch) {
|
||||
return altMatch;
|
||||
}
|
||||
}
|
||||
const base = normalizeLanguageCode(appLanguage);
|
||||
const baseMatch = availableCodes.find((c) => c.toLowerCase() === base);
|
||||
if (baseMatch) {
|
||||
return baseMatch;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function resolveDefaultLanguage(appLanguage, availableCodes) {
|
||||
return matchAvailableLanguage(appLanguage, availableCodes) ?? availableCodes[0] ?? DEFAULT_LANGUAGE_CODE;
|
||||
}
|
||||
|
||||
export { DEFAULT_LANGUAGE_CODE, DEFAULT_PASSWORD_LENGTH, DEFAULT_WORD_COUNT, LANGUAGES, MAX_PASSWORD_LENGTH, MAX_WORD_COUNT, MIN_PASSWORD_LENGTH, MIN_WORD_COUNT, getLanguageInfo, normalizeLanguageCode, resolveDefaultLanguage };
|
||||
export { DEFAULT_LANGUAGE_CODE, DEFAULT_PASSWORD_LENGTH, DEFAULT_WORD_COUNT, LANGUAGES, MAX_PASSWORD_LENGTH, MAX_WORD_COUNT, MIN_PASSWORD_LENGTH, MIN_WORD_COUNT, getLanguageInfo, matchAvailableLanguage, normalizeLanguageCode, resolveDefaultLanguage };
|
||||
|
||||
@@ -339,12 +339,14 @@ class SqliteClient implements IDatabaseClient {
|
||||
return explicitLanguage;
|
||||
}
|
||||
|
||||
// Otherwise, try to match UI language to an identity generator language
|
||||
const { mapUiLanguageToIdentityLanguage } = await import('@/utils/dist/core/identity-generator');
|
||||
// Otherwise, match the UI language to one of the identity generator's available languages using
|
||||
// the shared region-variant alternative-code table (e.g. "de-CH" -> "de").
|
||||
const { getAvailableLanguages } = await import('@/utils/dist/core/identity-generator');
|
||||
const { matchAvailableLanguage } = await import('@/utils/dist/core/models/defaults');
|
||||
const { default: i18n } = await import('@/i18n');
|
||||
|
||||
const uiLanguage = i18n.language;
|
||||
const mappedLanguage = mapUiLanguageToIdentityLanguage(uiLanguage);
|
||||
const mappedLanguage = matchAvailableLanguage(uiLanguage, getAvailableLanguages());
|
||||
|
||||
// Return the mapped language, or fall back to "en" if no match found
|
||||
return mappedLanguage ?? 'en';
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<div>
|
||||
<label for="diceware-language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">@Localizer["LanguageLabel"]</label>
|
||||
<select id="diceware-language" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
|
||||
@bind="_workingSettings.Language" @bind:after="OnSettingChanged">
|
||||
@bind="SelectedLanguage" @bind:after="OnSettingChanged">
|
||||
@foreach (var language in _dicewareLanguages)
|
||||
{
|
||||
<option value="@language">@Languages.GetDisplayLabel(language)</option>
|
||||
@@ -194,11 +194,27 @@
|
||||
/// </summary>
|
||||
private List<string> _dicewareLanguages = [Languages.DefaultLanguageCode];
|
||||
|
||||
/// <summary>
|
||||
/// The passphrase language shown when none is explicitly chosen ("auto"): the most appropriate
|
||||
/// available wordlist for the current app language.
|
||||
/// </summary>
|
||||
private string _effectiveLanguage = Languages.DefaultLanguageCode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the Diceware generator is currently selected.
|
||||
/// </summary>
|
||||
private bool IsDiceware => _workingSettings.Type == "diceware";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected passphrase language for the dropdown. When the user has not made an
|
||||
/// explicit choice the resolved "auto" language is shown.
|
||||
/// </summary>
|
||||
private string SelectedLanguage
|
||||
{
|
||||
get => string.IsNullOrEmpty(_workingSettings.Language) ? _effectiveLanguage : _workingSettings.Language;
|
||||
set => _workingSettings.Language = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -228,6 +244,10 @@
|
||||
// Load the Diceware wordlist languages available from the Rust core for the language picker.
|
||||
_dicewareLanguages = (await RustCoreService.GetDicewareLanguagesAsync()).ToList();
|
||||
|
||||
// Resolve the "auto" language shown when the user has not picked one explicitly. Uses the same
|
||||
// app language + shared resolver as the Rust core, so the dropdown matches the generated preview.
|
||||
_effectiveLanguage = Languages.ResolveDefaultLanguage(CultureInfo.CurrentCulture.Name, _dicewareLanguages);
|
||||
|
||||
await RefreshPreview(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 and native label.
|
||||
/// code to a flag, native label, and the region-variant locale codes that map onto it.
|
||||
/// </summary>
|
||||
public static class Languages
|
||||
{
|
||||
@@ -19,28 +19,29 @@ public static class Languages
|
||||
public const string DefaultLanguageCode = "en";
|
||||
|
||||
/// <summary>
|
||||
/// Known languages keyed by ISO 639-1 code, with a flag and native label.
|
||||
/// 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)[] Meta =
|
||||
private static readonly (string Code, string Flag, string Label, string[] AlternativeCodes)[] Meta =
|
||||
[
|
||||
("en", "🇺🇸", "English"),
|
||||
("nl", "🇳🇱", "Nederlands"),
|
||||
("de", "🇩🇪", "Deutsch"),
|
||||
("fr", "🇫🇷", "Français"),
|
||||
("es", "🇪🇸", "Español"),
|
||||
("it", "🇮🇹", "Italiano"),
|
||||
("da", "🇩🇰", "Dansk"),
|
||||
("fi", "🇫🇮", "Suomi"),
|
||||
("he", "🇮🇱", "עברית"),
|
||||
("pl", "🇵🇱", "Polski"),
|
||||
("pt", "🇧🇷", "Português Brasileiro"),
|
||||
("ro", "🇷🇴", "Română"),
|
||||
("ru", "🇷🇺", "Русский"),
|
||||
("sv", "🇸🇪", "Svenska"),
|
||||
("uk", "🇺🇦", "Українська"),
|
||||
("zh", "🇨🇳", "简体中文"),
|
||||
("ur", "🇵🇰", "اردو"),
|
||||
("fa", "🇮🇷", "فارسی"),
|
||||
("en", "🇺🇸", "English", ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"]),
|
||||
("nl", "🇳🇱", "Nederlands", ["nl-NL", "nl-BE"]),
|
||||
("de", "🇩🇪", "Deutsch", ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"]),
|
||||
("fr", "🇫🇷", "Français", ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"]),
|
||||
("es", "🇪🇸", "Español", ["es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-EC", "es-GT", "es-CU", "es-BO", "es-DO", "es-HN", "es-PY", "es-SV", "es-NI", "es-CR", "es-PA", "es-UY", "es-PR"]),
|
||||
("it", "🇮🇹", "Italiano", ["it-IT", "it-CH", "it-SM", "it-VA"]),
|
||||
("da", "🇩🇰", "Dansk", ["da-DK"]),
|
||||
("fi", "🇫🇮", "Suomi", ["fi-FI"]),
|
||||
("he", "🇮🇱", "עברית", ["he-IL"]),
|
||||
("pl", "🇵🇱", "Polski", ["pl-PL"]),
|
||||
("pt", "🇧🇷", "Português Brasileiro", ["pt-BR", "pt-PT"]),
|
||||
("ro", "🇷🇴", "Română", ["ro-RO", "ro-MD"]),
|
||||
("ru", "🇷🇺", "Русский", ["ru-RU", "ru-BY", "ru-KZ", "ru-UA"]),
|
||||
("sv", "🇸🇪", "Svenska", ["sv-SE", "sv-FI"]),
|
||||
("uk", "🇺🇦", "Українська", ["uk-UA"]),
|
||||
("zh", "🇨🇳", "简体中文", ["zh-CN", "zh-SG", "zh-Hans", "zh-TW", "zh-HK", "zh-MO", "zh-Hant"]),
|
||||
("ur", "🇵🇰", "اردو", ["ur-PK", "ur-IN"]),
|
||||
("fa", "🇮🇷", "فارسی", ["fa-IR", "fa-AF"]),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +64,7 @@ public static class Languages
|
||||
public static string GetDisplayLabel(string code)
|
||||
{
|
||||
var iso = Normalize(code);
|
||||
foreach (var (metaCode, flag, label) in Meta)
|
||||
foreach (var (metaCode, flag, label, _) in Meta)
|
||||
{
|
||||
if (metaCode == iso)
|
||||
{
|
||||
@@ -82,7 +83,7 @@ public static class Languages
|
||||
public static string GetFlag(string code)
|
||||
{
|
||||
var iso = Normalize(code);
|
||||
foreach (var (metaCode, flag, _) in Meta)
|
||||
foreach (var (metaCode, flag, _, _) in Meta)
|
||||
{
|
||||
if (metaCode == iso)
|
||||
{
|
||||
@@ -102,7 +103,7 @@ public static class Languages
|
||||
public static string GetLabel(string code)
|
||||
{
|
||||
var iso = Normalize(code);
|
||||
foreach (var (metaCode, _, label) in Meta)
|
||||
foreach (var (metaCode, _, label, _) in Meta)
|
||||
{
|
||||
if (metaCode == iso)
|
||||
{
|
||||
@@ -113,25 +114,83 @@ public static class Languages
|
||||
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. Returns the app language when available, otherwise the first available code,
|
||||
/// otherwise English.
|
||||
/// 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)
|
||||
{
|
||||
var iso = Normalize(appLanguage);
|
||||
foreach (var code in availableCodes)
|
||||
{
|
||||
if (string.Equals(code, iso, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
return availableCodes.Count > 0 ? availableCodes[0] : DefaultLanguageCode;
|
||||
return MatchAvailableLanguage(appLanguage, availableCodes)
|
||||
?? (availableCodes.Count > 0 ? availableCodes[0] : DefaultLanguageCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,11 @@ public class PasswordSettings
|
||||
public int WordCount { get; set; } = PasswordGeneratorDefaults.DefaultWordCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Diceware wordlist language. Unknown languages fall back to English in the Rust core.
|
||||
/// Gets or sets the Diceware wordlist language. Empty means "auto": the most appropriate available
|
||||
/// wordlist is resolved from the app language at runtime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Language")]
|
||||
public string Language { get; set; } = "en";
|
||||
public string Language { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Diceware capitalization ("None", "TitleCase", "Uppercase", "Lowercase" or "Random").
|
||||
|
||||
@@ -503,8 +503,13 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
|
||||
await InitializeAsync();
|
||||
}
|
||||
|
||||
var result = await _identityGeneratorModule!.InvokeAsync<string?>("mapUiLanguageToIdentityLanguage", uiLanguageCode);
|
||||
return result;
|
||||
var codes = await _identityGeneratorModule!.InvokeAsync<List<string>>("getAvailableLanguages");
|
||||
if (codes == null || codes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Languages.MatchAvailableLanguage(uiLanguageCode, codes);
|
||||
}
|
||||
catch (JSException ex)
|
||||
{
|
||||
|
||||
@@ -372,6 +372,17 @@ public class RustCoreService : IAsyncDisposable
|
||||
node["Seed"] = seed;
|
||||
}
|
||||
|
||||
// Resolve the effective passphrase language when none is explicitly chosen ("auto"). Pick the
|
||||
// most appropriate available Diceware wordlist for the current app language using the shared
|
||||
// region-variant table (e.g. "de-CH" -> "de"), falling back to English.
|
||||
if (string.Equals(settings.Type, "diceware", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.IsNullOrWhiteSpace(settings.Language))
|
||||
{
|
||||
var codes = await GetDicewareLanguagesAsync();
|
||||
var appLanguage = System.Globalization.CultureInfo.CurrentCulture.Name;
|
||||
node["Language"] = Languages.ResolveDefaultLanguage(appLanguage, codes);
|
||||
}
|
||||
|
||||
return await jsRuntime.InvokeAsync<string>("rustCoreGeneratePassword", node.ToJsonString());
|
||||
}
|
||||
|
||||
|
||||
@@ -399,20 +399,6 @@ declare function convertAgeRangeToBirthdateOptions(ageRange: string): IBirthdate
|
||||
* @returns Array of ISO language codes (e.g. ["da", "de", "en", ...]).
|
||||
*/
|
||||
declare function getAvailableLanguages(): string[];
|
||||
/**
|
||||
* Maps a UI language code to an identity generator language code.
|
||||
* If no explicit match is found, returns null to indicate no preference.
|
||||
*
|
||||
* @param uiLanguageCode - The UI language code (e.g., "en", "en-US", "nl-NL", "de-DE", "fr")
|
||||
* @returns The matching identity generator language code or null if no match
|
||||
*
|
||||
* @example
|
||||
* mapUiLanguageToIdentityLanguage("en-US") // returns "en"
|
||||
* mapUiLanguageToIdentityLanguage("nl") // returns "nl"
|
||||
* mapUiLanguageToIdentityLanguage("de-CH") // returns "de"
|
||||
* mapUiLanguageToIdentityLanguage("ja") // returns null (no Japanese identity generator)
|
||||
*/
|
||||
declare function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null;
|
||||
|
||||
/**
|
||||
* Creates a new identity generator based on the language.
|
||||
@@ -429,4 +415,4 @@ declare const CreateIdentityGenerator: (language: string) => IdentityGenerator;
|
||||
*/
|
||||
declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator;
|
||||
|
||||
export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type Identity, IdentityGenerator, IdentityGeneratorDa, IdentityGeneratorDe, IdentityGeneratorEn, IdentityGeneratorEs, IdentityGeneratorFa, IdentityGeneratorFr, IdentityGeneratorIt, IdentityGeneratorNl, IdentityGeneratorRo, IdentityGeneratorSv, IdentityGeneratorUr, IdentityHelperUtils, UsernameEmailGenerator, convertAgeRangeToBirthdateOptions, getAvailableAgeRanges, getAvailableLanguages, mapUiLanguageToIdentityLanguage };
|
||||
export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type Identity, IdentityGenerator, IdentityGeneratorDa, IdentityGeneratorDe, IdentityGeneratorEn, IdentityGeneratorEs, IdentityGeneratorFa, IdentityGeneratorFr, IdentityGeneratorIt, IdentityGeneratorNl, IdentityGeneratorRo, IdentityGeneratorSv, IdentityGeneratorUr, IdentityHelperUtils, UsernameEmailGenerator, convertAgeRangeToBirthdateOptions, getAvailableAgeRanges, getAvailableLanguages };
|
||||
|
||||
@@ -42,8 +42,7 @@ __export(index_exports, {
|
||||
UsernameEmailGenerator: () => UsernameEmailGenerator,
|
||||
convertAgeRangeToBirthdateOptions: () => convertAgeRangeToBirthdateOptions,
|
||||
getAvailableAgeRanges: () => getAvailableAgeRanges,
|
||||
getAvailableLanguages: () => getAvailableLanguages,
|
||||
mapUiLanguageToIdentityLanguage: () => mapUiLanguageToIdentityLanguage
|
||||
getAvailableLanguages: () => getAvailableLanguages
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
@@ -11763,43 +11762,21 @@ function convertAgeRangeToBirthdateOptions(ageRange) {
|
||||
}
|
||||
|
||||
// src/utils/LanguageProvider.ts
|
||||
var LANGUAGE_DEFS = [
|
||||
{ value: "da", alternativeCodes: ["da-DK"] },
|
||||
{ value: "de", alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] },
|
||||
{ value: "en", alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] },
|
||||
{ value: "es", alternativeCodes: ["es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-EC", "es-GT", "es-CU", "es-BO", "es-DO", "es-HN", "es-PY", "es-SV", "es-NI", "es-CR", "es-PA", "es-UY", "es-PR"] },
|
||||
{ value: "fr", alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] },
|
||||
{ value: "it", alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] },
|
||||
{ value: "nl", alternativeCodes: ["nl-NL", "nl-BE"] },
|
||||
{ value: "ro", alternativeCodes: ["ro-RO", "ro-MD"] },
|
||||
{ value: "sv", alternativeCodes: ["sv-SE", "sv-FI"] },
|
||||
{ value: "ur", alternativeCodes: ["ur-PK", "ur-IN"] },
|
||||
{ value: "fa", alternativeCodes: ["fa-IR", "fa-AF"] }
|
||||
var SUPPORTED_LANGUAGE_CODES = [
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"nl",
|
||||
"ro",
|
||||
"sv",
|
||||
"ur",
|
||||
"fa"
|
||||
];
|
||||
function getAvailableLanguages() {
|
||||
return LANGUAGE_DEFS.map((lang) => lang.value);
|
||||
}
|
||||
function mapUiLanguageToIdentityLanguage(uiLanguageCode) {
|
||||
if (!uiLanguageCode) {
|
||||
return null;
|
||||
}
|
||||
const normalizedCode = uiLanguageCode.toLowerCase();
|
||||
const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode);
|
||||
if (exactMatch) {
|
||||
return exactMatch.value;
|
||||
}
|
||||
const alternativeMatch = LANGUAGE_DEFS.find(
|
||||
(lang) => lang.alternativeCodes?.some((code) => code.toLowerCase() === normalizedCode)
|
||||
);
|
||||
if (alternativeMatch) {
|
||||
return alternativeMatch.value;
|
||||
}
|
||||
const baseCode = normalizedCode.split("-")[0];
|
||||
const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode);
|
||||
if (baseMatch) {
|
||||
return baseMatch.value;
|
||||
}
|
||||
return null;
|
||||
return [...SUPPORTED_LANGUAGE_CODES];
|
||||
}
|
||||
|
||||
// src/factories/IdentityGeneratorFactory.ts
|
||||
@@ -11858,6 +11835,5 @@ var CreateUsernameEmailGenerator = () => {
|
||||
UsernameEmailGenerator,
|
||||
convertAgeRangeToBirthdateOptions,
|
||||
getAvailableAgeRanges,
|
||||
getAvailableLanguages,
|
||||
mapUiLanguageToIdentityLanguage
|
||||
getAvailableLanguages
|
||||
});
|
||||
|
||||
@@ -11718,43 +11718,21 @@ function convertAgeRangeToBirthdateOptions(ageRange) {
|
||||
}
|
||||
|
||||
// src/utils/LanguageProvider.ts
|
||||
var LANGUAGE_DEFS = [
|
||||
{ value: "da", alternativeCodes: ["da-DK"] },
|
||||
{ value: "de", alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] },
|
||||
{ value: "en", alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] },
|
||||
{ value: "es", alternativeCodes: ["es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-EC", "es-GT", "es-CU", "es-BO", "es-DO", "es-HN", "es-PY", "es-SV", "es-NI", "es-CR", "es-PA", "es-UY", "es-PR"] },
|
||||
{ value: "fr", alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] },
|
||||
{ value: "it", alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] },
|
||||
{ value: "nl", alternativeCodes: ["nl-NL", "nl-BE"] },
|
||||
{ value: "ro", alternativeCodes: ["ro-RO", "ro-MD"] },
|
||||
{ value: "sv", alternativeCodes: ["sv-SE", "sv-FI"] },
|
||||
{ value: "ur", alternativeCodes: ["ur-PK", "ur-IN"] },
|
||||
{ value: "fa", alternativeCodes: ["fa-IR", "fa-AF"] }
|
||||
var SUPPORTED_LANGUAGE_CODES = [
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"nl",
|
||||
"ro",
|
||||
"sv",
|
||||
"ur",
|
||||
"fa"
|
||||
];
|
||||
function getAvailableLanguages() {
|
||||
return LANGUAGE_DEFS.map((lang) => lang.value);
|
||||
}
|
||||
function mapUiLanguageToIdentityLanguage(uiLanguageCode) {
|
||||
if (!uiLanguageCode) {
|
||||
return null;
|
||||
}
|
||||
const normalizedCode = uiLanguageCode.toLowerCase();
|
||||
const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode);
|
||||
if (exactMatch) {
|
||||
return exactMatch.value;
|
||||
}
|
||||
const alternativeMatch = LANGUAGE_DEFS.find(
|
||||
(lang) => lang.alternativeCodes?.some((code) => code.toLowerCase() === normalizedCode)
|
||||
);
|
||||
if (alternativeMatch) {
|
||||
return alternativeMatch.value;
|
||||
}
|
||||
const baseCode = normalizedCode.split("-")[0];
|
||||
const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode);
|
||||
if (baseMatch) {
|
||||
return baseMatch.value;
|
||||
}
|
||||
return null;
|
||||
return [...SUPPORTED_LANGUAGE_CODES];
|
||||
}
|
||||
|
||||
// src/factories/IdentityGeneratorFactory.ts
|
||||
@@ -11812,6 +11790,5 @@ export {
|
||||
UsernameEmailGenerator,
|
||||
convertAgeRangeToBirthdateOptions,
|
||||
getAvailableAgeRanges,
|
||||
getAvailableLanguages,
|
||||
mapUiLanguageToIdentityLanguage
|
||||
getAvailableLanguages
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ package_name="models"
|
||||
package_path="."
|
||||
|
||||
echo "📦 Building $package_name..."
|
||||
npm install && npm run lint && npm run build
|
||||
npm install && npm run lint && npm run test && npm run build
|
||||
|
||||
echo ""
|
||||
echo "🔄 Generating platform-specific models (C#, Swift, Kotlin)..."
|
||||
|
||||
1236
core/models/package-lock.json
generated
1236
core/models/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
"lint:fix": "eslint src --fix",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -19,6 +20,7 @@
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"eslint": "^9.19.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^4.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ function csString(value) {
|
||||
|
||||
function generateCSharp(languages, defaultCode) {
|
||||
const tuples = languages
|
||||
.map((l) => ` ("${csString(l.code)}", "${l.flag}", "${csString(l.label)}"),`)
|
||||
.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 />
|
||||
@@ -39,7 +42,7 @@ 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 and native label.
|
||||
/// code to a flag, native label, and the region-variant locale codes that map onto it.
|
||||
/// </summary>
|
||||
public static class Languages
|
||||
{
|
||||
@@ -49,9 +52,10 @@ public static class Languages
|
||||
public const string DefaultLanguageCode = "${csString(defaultCode)}";
|
||||
|
||||
/// <summary>
|
||||
/// Known languages keyed by ISO 639-1 code, with a flag and native label.
|
||||
/// 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)[] Meta =
|
||||
private static readonly (string Code, string Flag, string Label, string[] AlternativeCodes)[] Meta =
|
||||
[
|
||||
${tuples}
|
||||
];
|
||||
@@ -76,7 +80,7 @@ ${tuples}
|
||||
public static string GetDisplayLabel(string code)
|
||||
{
|
||||
var iso = Normalize(code);
|
||||
foreach (var (metaCode, flag, label) in Meta)
|
||||
foreach (var (metaCode, flag, label, _) in Meta)
|
||||
{
|
||||
if (metaCode == iso)
|
||||
{
|
||||
@@ -95,7 +99,7 @@ ${tuples}
|
||||
public static string GetFlag(string code)
|
||||
{
|
||||
var iso = Normalize(code);
|
||||
foreach (var (metaCode, flag, _) in Meta)
|
||||
foreach (var (metaCode, flag, _, _) in Meta)
|
||||
{
|
||||
if (metaCode == iso)
|
||||
{
|
||||
@@ -115,7 +119,7 @@ ${tuples}
|
||||
public static string GetLabel(string code)
|
||||
{
|
||||
var iso = Normalize(code);
|
||||
foreach (var (metaCode, _, label) in Meta)
|
||||
foreach (var (metaCode, _, label, _) in Meta)
|
||||
{
|
||||
if (metaCode == iso)
|
||||
{
|
||||
@@ -126,26 +130,84 @@ ${tuples}
|
||||
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. Returns the app language when available, otherwise the first available code,
|
||||
/// otherwise English.
|
||||
/// 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)
|
||||
{
|
||||
var iso = Normalize(appLanguage);
|
||||
foreach (var code in availableCodes)
|
||||
{
|
||||
if (string.Equals(code, iso, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
return availableCodes.Count > 0 ? availableCodes[0] : DefaultLanguageCode;
|
||||
return MatchAvailableLanguage(appLanguage, availableCodes)
|
||||
?? (availableCodes.Count > 0 ? availableCodes[0] : DefaultLanguageCode);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Generic, cross-platform language reference: maps a two-letter ISO 639-1 code to a flag and a
|
||||
* native display label.
|
||||
* Generic, cross-platform language reference: maps a two-letter ISO 639-1 code to a flag, a native
|
||||
* display label, and the alternative locale codes (BCP-47 region variants) that map onto it
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -13,34 +13,36 @@ export interface ILanguageInfo {
|
||||
flag: string;
|
||||
/** Native display label. */
|
||||
label: string;
|
||||
/** Alternative locale codes (BCP-47 language-region tags) that map onto this language. */
|
||||
alternativeCodes?: string[];
|
||||
}
|
||||
|
||||
/** Default ISO language code used as the universal fallback. */
|
||||
export const DEFAULT_LANGUAGE_CODE = 'en';
|
||||
|
||||
/**
|
||||
* Known languages keyed by ISO 639-1 code, with a flag and native label.
|
||||
* Covers the AliasVault app UI languages so this list can be reused beyond a single feature.
|
||||
* Known languages keyed by ISO 639-1 code, with a flag, native label, and the region-variant locale
|
||||
* codes that map onto each.
|
||||
*/
|
||||
export const LANGUAGES: ILanguageInfo[] = [
|
||||
{ code: 'en', flag: '🇺🇸', label: 'English' },
|
||||
{ code: 'nl', flag: '🇳🇱', label: 'Nederlands' },
|
||||
{ code: 'de', flag: '🇩🇪', label: 'Deutsch' },
|
||||
{ code: 'fr', flag: '🇫🇷', label: 'Français' },
|
||||
{ code: 'es', flag: '🇪🇸', label: 'Español' },
|
||||
{ code: 'it', flag: '🇮🇹', label: 'Italiano' },
|
||||
{ code: 'da', flag: '🇩🇰', label: 'Dansk' },
|
||||
{ code: 'fi', flag: '🇫🇮', label: 'Suomi' },
|
||||
{ code: 'he', flag: '🇮🇱', label: 'עברית' },
|
||||
{ code: 'pl', flag: '🇵🇱', label: 'Polski' },
|
||||
{ code: 'pt', flag: '🇧🇷', label: 'Português Brasileiro' },
|
||||
{ code: 'ro', flag: '🇷🇴', label: 'Română' },
|
||||
{ code: 'ru', flag: '🇷🇺', label: 'Русский' },
|
||||
{ code: 'sv', flag: '🇸🇪', label: 'Svenska' },
|
||||
{ code: 'uk', flag: '🇺🇦', label: 'Українська' },
|
||||
{ code: 'zh', flag: '🇨🇳', label: '简体中文' },
|
||||
{ code: 'ur', flag: '🇵🇰', label: 'اردو' },
|
||||
{ code: 'fa', flag: '🇮🇷', label: 'فارسی' },
|
||||
{ code: 'en', flag: '🇺🇸', label: 'English', alternativeCodes: ['en-US', 'en-GB', 'en-CA', 'en-AU', 'en-NZ', 'en-IE', 'en-ZA', 'en-SG', 'en-IN'] },
|
||||
{ code: 'nl', flag: '🇳🇱', label: 'Nederlands', alternativeCodes: ['nl-NL', 'nl-BE'] },
|
||||
{ code: 'de', flag: '🇩🇪', label: 'Deutsch', alternativeCodes: ['de-DE', 'de-AT', 'de-CH', 'de-LU', 'de-LI'] },
|
||||
{ code: 'fr', flag: '🇫🇷', label: 'Français', alternativeCodes: ['fr-FR', 'fr-CA', 'fr-BE', 'fr-CH', 'fr-LU', 'fr-MC'] },
|
||||
{ code: 'es', flag: '🇪🇸', label: 'Español', alternativeCodes: ['es-ES', 'es-MX', 'es-AR', 'es-CO', 'es-CL', 'es-PE', 'es-VE', 'es-EC', 'es-GT', 'es-CU', 'es-BO', 'es-DO', 'es-HN', 'es-PY', 'es-SV', 'es-NI', 'es-CR', 'es-PA', 'es-UY', 'es-PR'] },
|
||||
{ code: 'it', flag: '🇮🇹', label: 'Italiano', alternativeCodes: ['it-IT', 'it-CH', 'it-SM', 'it-VA'] },
|
||||
{ code: 'da', flag: '🇩🇰', label: 'Dansk', alternativeCodes: ['da-DK'] },
|
||||
{ code: 'fi', flag: '🇫🇮', label: 'Suomi', alternativeCodes: ['fi-FI'] },
|
||||
{ code: 'he', flag: '🇮🇱', label: 'עברית', alternativeCodes: ['he-IL'] },
|
||||
{ code: 'pl', flag: '🇵🇱', label: 'Polski', alternativeCodes: ['pl-PL'] },
|
||||
{ code: 'pt', flag: '🇧🇷', label: 'Português Brasileiro', alternativeCodes: ['pt-BR', 'pt-PT'] },
|
||||
{ code: 'ro', flag: '🇷🇴', label: 'Română', alternativeCodes: ['ro-RO', 'ro-MD'] },
|
||||
{ code: 'ru', flag: '🇷🇺', label: 'Русский', alternativeCodes: ['ru-RU', 'ru-BY', 'ru-KZ', 'ru-UA'] },
|
||||
{ code: 'sv', flag: '🇸🇪', label: 'Svenska', alternativeCodes: ['sv-SE', 'sv-FI'] },
|
||||
{ code: 'uk', flag: '🇺🇦', label: 'Українська', alternativeCodes: ['uk-UA'] },
|
||||
{ code: 'zh', flag: '🇨🇳', label: '简体中文', alternativeCodes: ['zh-CN', 'zh-SG', 'zh-Hans', 'zh-TW', 'zh-HK', 'zh-MO', 'zh-Hant'] },
|
||||
{ code: 'ur', flag: '🇵🇰', label: 'اردو', alternativeCodes: ['ur-PK', 'ur-IN'] },
|
||||
{ code: 'fa', flag: '🇮🇷', label: 'فارسی', alternativeCodes: ['fa-IR', 'fa-AF'] },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -64,18 +66,69 @@ export function getLanguageInfo(code: string): ILanguageInfo {
|
||||
return match ?? { code, flag: '🌐', label: code };
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an app/UI/browser locale to one of a feature's available ISO codes, using the region-variant
|
||||
* alternative codes from {@link LANGUAGES} (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
|
||||
* (the part before the '-'). Returns null when nothing matches, so callers can decide their own
|
||||
* fallback (e.g. "no preference" vs. a concrete default).
|
||||
*
|
||||
* @param appLanguage The app/UI/browser language tag (e.g. 'en', 'en-US', 'nl-BE').
|
||||
* @param availableCodes The codes the feature actually supports.
|
||||
* @returns The matching available code (in its original casing) or null if none matched.
|
||||
*
|
||||
* @example
|
||||
* matchAvailableLanguage('en-US', ['en', 'nl']) // 'en'
|
||||
* matchAvailableLanguage('de-CH', ['de', 'en']) // 'de'
|
||||
* matchAvailableLanguage('ja', ['en', 'nl']) // null
|
||||
*/
|
||||
export function matchAvailableLanguage(
|
||||
appLanguage: string | null | undefined,
|
||||
availableCodes: string[]
|
||||
): string | null {
|
||||
if (!appLanguage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lower = appLanguage.toLowerCase();
|
||||
|
||||
// 1. Exact match against an available code (e.g. 'nl' or even a full tag the feature lists).
|
||||
const exact = availableCodes.find((c) => c.toLowerCase() === lower);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
|
||||
/*
|
||||
* 2. Alternative-code match: find the language whose region variants include this tag, then return
|
||||
* its base code if the feature supports it (e.g. 'en-GB' -> 'en').
|
||||
*/
|
||||
const altEntry = LANGUAGES.find((l) => l.alternativeCodes?.some((ac) => ac.toLowerCase() === lower));
|
||||
if (altEntry) {
|
||||
const altMatch = availableCodes.find((c) => c.toLowerCase() === altEntry.code.toLowerCase());
|
||||
if (altMatch) {
|
||||
return altMatch;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Base language code match (e.g. an unlisted 'en-ZZ' still resolves to 'en').
|
||||
const base = normalizeLanguageCode(appLanguage);
|
||||
const baseMatch = availableCodes.find((c) => c.toLowerCase() === base);
|
||||
if (baseMatch) {
|
||||
return baseMatch;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a default language code for an app/UI language, restricted to a set of available codes
|
||||
* (e.g. the Diceware wordlist languages returned by the Rust core). Returns the app language when it
|
||||
* is available, otherwise the first available code, otherwise English.
|
||||
* @param appLanguage The app/UI language tag.
|
||||
* (e.g. the Diceware wordlist languages returned by the Rust core, or the identity generator's
|
||||
* supported languages). Uses {@link matchAvailableLanguage} (region-variant aware), then falls back
|
||||
* to the first available code, otherwise English.
|
||||
* @param appLanguage The app/UI/browser language tag.
|
||||
* @param availableCodes The codes the feature actually supports.
|
||||
* @returns The resolved ISO code.
|
||||
*/
|
||||
export function resolveDefaultLanguage(appLanguage: string | null | undefined, availableCodes: string[]): string {
|
||||
const iso = normalizeLanguageCode(appLanguage);
|
||||
if (availableCodes.some((c) => c.toLowerCase() === iso)) {
|
||||
return iso;
|
||||
}
|
||||
return availableCodes[0] ?? DEFAULT_LANGUAGE_CODE;
|
||||
return matchAvailableLanguage(appLanguage, availableCodes) ?? availableCodes[0] ?? DEFAULT_LANGUAGE_CODE;
|
||||
}
|
||||
|
||||
112
core/models/src/defaults/__tests__/Languages.test.ts
Normal file
112
core/models/src/defaults/__tests__/Languages.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
DEFAULT_LANGUAGE_CODE,
|
||||
LANGUAGES,
|
||||
getLanguageInfo,
|
||||
matchAvailableLanguage,
|
||||
normalizeLanguageCode,
|
||||
resolveDefaultLanguage,
|
||||
} from '../Languages';
|
||||
|
||||
/** The identity generator's supported subset, used to exercise restricted matching. */
|
||||
const IDENTITY_CODES = ['da', 'de', 'en', 'es', 'fr', 'it', 'nl', 'ro', 'sv', 'ur', 'fa'];
|
||||
/** The Diceware passphrase wordlist subset (owned by the Rust core). */
|
||||
const DICEWARE_CODES = ['en', 'nl', 'de', 'fr', 'es', 'it'];
|
||||
|
||||
describe('Languages', () => {
|
||||
describe('normalizeLanguageCode', () => {
|
||||
it('takes the first two characters, lowercased', () => {
|
||||
expect(normalizeLanguageCode('nl-NL')).toBe('nl');
|
||||
expect(normalizeLanguageCode('EN')).toBe('en');
|
||||
expect(normalizeLanguageCode('de-CH')).toBe('de');
|
||||
});
|
||||
|
||||
it('handles null/undefined/empty', () => {
|
||||
expect(normalizeLanguageCode(null)).toBe('');
|
||||
expect(normalizeLanguageCode(undefined)).toBe('');
|
||||
expect(normalizeLanguageCode('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLanguageInfo', () => {
|
||||
it('returns the metadata for a known code', () => {
|
||||
const nl = getLanguageInfo('nl');
|
||||
expect(nl.code).toBe('nl');
|
||||
expect(nl.label).toBe('Nederlands');
|
||||
expect(nl.alternativeCodes).toContain('nl-BE');
|
||||
});
|
||||
|
||||
it('falls back to a globe + raw code for unknown languages', () => {
|
||||
const unknown = getLanguageInfo('xx');
|
||||
expect(unknown.flag).toBe('🌐');
|
||||
expect(unknown.label).toBe('xx');
|
||||
});
|
||||
});
|
||||
|
||||
describe('alternativeCodes data', () => {
|
||||
it('defines unique, region-tagged alternatives for every language', () => {
|
||||
const seen = new Set<string>();
|
||||
for (const lang of LANGUAGES) {
|
||||
for (const alt of lang.alternativeCodes ?? []) {
|
||||
expect(alt.toLowerCase()).not.toBe(lang.code.toLowerCase());
|
||||
expect(seen.has(alt.toLowerCase())).toBe(false);
|
||||
seen.add(alt.toLowerCase());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchAvailableLanguage', () => {
|
||||
it('matches an exact available code', () => {
|
||||
expect(matchAvailableLanguage('nl', IDENTITY_CODES)).toBe('nl');
|
||||
});
|
||||
|
||||
it('maps a region variant via alternativeCodes', () => {
|
||||
expect(matchAvailableLanguage('nl-BE', IDENTITY_CODES)).toBe('nl');
|
||||
expect(matchAvailableLanguage('de-CH', IDENTITY_CODES)).toBe('de');
|
||||
expect(matchAvailableLanguage('en-US', DICEWARE_CODES)).toBe('en');
|
||||
});
|
||||
|
||||
it('is case-insensitive', () => {
|
||||
expect(matchAvailableLanguage('NL-BE', IDENTITY_CODES)).toBe('nl');
|
||||
expect(matchAvailableLanguage('En-Us', IDENTITY_CODES)).toBe('en');
|
||||
});
|
||||
|
||||
it('falls back to the base language for unlisted region variants', () => {
|
||||
expect(matchAvailableLanguage('en-ZZ', IDENTITY_CODES)).toBe('en');
|
||||
expect(matchAvailableLanguage('nl-ZZ', IDENTITY_CODES)).toBe('nl');
|
||||
});
|
||||
|
||||
it('returns null when the language is not available', () => {
|
||||
// Japanese has no identity generator.
|
||||
expect(matchAvailableLanguage('ja', IDENTITY_CODES)).toBeNull();
|
||||
// Polish is a known UI language but not a Diceware wordlist.
|
||||
expect(matchAvailableLanguage('pl-PL', DICEWARE_CODES)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for empty/invalid input', () => {
|
||||
expect(matchAvailableLanguage(null, IDENTITY_CODES)).toBeNull();
|
||||
expect(matchAvailableLanguage(undefined, IDENTITY_CODES)).toBeNull();
|
||||
expect(matchAvailableLanguage('', IDENTITY_CODES)).toBeNull();
|
||||
expect(matchAvailableLanguage('invalid', IDENTITY_CODES)).toBeNull();
|
||||
expect(matchAvailableLanguage('123', IDENTITY_CODES)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDefaultLanguage', () => {
|
||||
it('returns the matched code when available', () => {
|
||||
expect(resolveDefaultLanguage('de-AT', DICEWARE_CODES)).toBe('de');
|
||||
expect(resolveDefaultLanguage('nl', DICEWARE_CODES)).toBe('nl');
|
||||
});
|
||||
|
||||
it('falls back to the first available code when nothing matches', () => {
|
||||
expect(resolveDefaultLanguage('ja', DICEWARE_CODES)).toBe('en');
|
||||
expect(resolveDefaultLanguage('pl-PL', DICEWARE_CODES)).toBe('en');
|
||||
});
|
||||
|
||||
it('falls back to English when there are no available codes', () => {
|
||||
expect(resolveDefaultLanguage('nl', [])).toBe(DEFAULT_LANGUAGE_CODE);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getAvailableLanguages, mapUiLanguageToIdentityLanguage } from '../utils/LanguageProvider';
|
||||
import { getAvailableLanguages } from '../utils/LanguageProvider';
|
||||
|
||||
describe('LanguageProvider', () => {
|
||||
describe('getAvailableLanguages', () => {
|
||||
@@ -8,82 +8,9 @@ describe('LanguageProvider', () => {
|
||||
|
||||
expect(languages).toBeDefined();
|
||||
expect(languages.length).toBeGreaterThan(0);
|
||||
// Returns ISO codes only; display metadata (flag/label) is resolved by clients from core/models.
|
||||
expect(languages.every(code => typeof code === 'string')).toBe(true);
|
||||
expect(languages).toContain('en');
|
||||
expect(languages).toContain('nl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapUiLanguageToIdentityLanguage', () => {
|
||||
describe('Dutch mappings', () => {
|
||||
it('should map "nl" to "nl"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('nl')).toBe('nl');
|
||||
});
|
||||
|
||||
it('should map "nl-NL" to "nl"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('nl-NL')).toBe('nl');
|
||||
});
|
||||
|
||||
it('should map "nl-BE" to "nl" (Belgian Dutch)', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('nl-BE')).toBe('nl');
|
||||
});
|
||||
|
||||
it('should be case-insensitive for "NL"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('NL')).toBe('nl');
|
||||
});
|
||||
|
||||
it('should be case-insensitive for "NL-NL"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('NL-NL')).toBe('nl');
|
||||
});
|
||||
|
||||
it('should be case-insensitive for "NL-BE"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('NL-BE')).toBe('nl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should return null for null input', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage(null)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for undefined input', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage(undefined)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for empty string', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for whitespace', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage(' ')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for invalid format', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('invalid')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for numbers', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('123')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed case and locale variants', () => {
|
||||
it('should handle mixed case "En-Us"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('En-Us')).toBe('en');
|
||||
});
|
||||
|
||||
it('should handle mixed case "nL-Nl"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('nL-Nl')).toBe('nl');
|
||||
});
|
||||
|
||||
it('should extract base language from unknown locale "en-ZZ"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('en-ZZ')).toBe('en');
|
||||
});
|
||||
|
||||
it('should extract base language from unknown locale "nl-ZZ"', () => {
|
||||
expect(mapUiLanguageToIdentityLanguage('nl-ZZ')).toBe('nl');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +1,18 @@
|
||||
/**
|
||||
* Internal definition of an identity-generator language: its ISO code plus the alternative locale
|
||||
* codes that map onto it. Display metadata (flag, native label) is intentionally NOT stored here —
|
||||
* clients look that up from the shared `core/models` language reference so it lives in one place.
|
||||
* The set of languages the identity generator can produce identities for.
|
||||
*/
|
||||
interface ILanguageDef {
|
||||
/**
|
||||
* The language code (e.g., "en", "nl", "de").
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* Alternative language codes that map to this identity generator language.
|
||||
* Used for matching UI locale codes to identity generator languages.
|
||||
* For example, "en-US", "en-GB", "en-CA" all map to "en".
|
||||
*/
|
||||
alternativeCodes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The languages supported by the identity generator. Only ISO codes and their alternative locale
|
||||
* codes are defined here; flag emojis and native labels come from the shared `core/models` language
|
||||
* reference (`getLanguageInfo`).
|
||||
*/
|
||||
const LANGUAGE_DEFS: ILanguageDef[] = [
|
||||
{ value: 'da', alternativeCodes: ['da-DK'] },
|
||||
{ value: 'de', alternativeCodes: ['de-DE', 'de-AT', 'de-CH', 'de-LU', 'de-LI'] },
|
||||
{ value: 'en', alternativeCodes: ['en-US', 'en-GB', 'en-CA', 'en-AU', 'en-NZ', 'en-IE', 'en-ZA', 'en-SG', 'en-IN'] },
|
||||
{ value: 'es', alternativeCodes: ['es-ES', 'es-MX', 'es-AR', 'es-CO', 'es-CL', 'es-PE', 'es-VE', 'es-EC', 'es-GT', 'es-CU', 'es-BO', 'es-DO', 'es-HN', 'es-PY', 'es-SV', 'es-NI', 'es-CR', 'es-PA', 'es-UY', 'es-PR'] },
|
||||
{ value: 'fr', alternativeCodes: ['fr-FR', 'fr-CA', 'fr-BE', 'fr-CH', 'fr-LU', 'fr-MC'] },
|
||||
{ value: 'it', alternativeCodes: ['it-IT', 'it-CH', 'it-SM', 'it-VA'] },
|
||||
{ value: 'nl', alternativeCodes: ['nl-NL', 'nl-BE'] },
|
||||
{ value: 'ro', alternativeCodes: ['ro-RO', 'ro-MD'] },
|
||||
{ value: 'sv', alternativeCodes: ['sv-SE', 'sv-FI'] },
|
||||
{ value: 'ur', alternativeCodes: ['ur-PK', 'ur-IN'] },
|
||||
{ value: 'fa', alternativeCodes: ['fa-IR', 'fa-AF'] },
|
||||
const SUPPORTED_LANGUAGE_CODES: string[] = [
|
||||
'da',
|
||||
'de',
|
||||
'en',
|
||||
'es',
|
||||
'fr',
|
||||
'it',
|
||||
'nl',
|
||||
'ro',
|
||||
'sv',
|
||||
'ur',
|
||||
'fa',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -41,50 +20,5 @@ const LANGUAGE_DEFS: ILanguageDef[] = [
|
||||
* @returns Array of ISO language codes (e.g. ["da", "de", "en", ...]).
|
||||
*/
|
||||
export function getAvailableLanguages(): string[] {
|
||||
return LANGUAGE_DEFS.map(lang => lang.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a UI language code to an identity generator language code.
|
||||
* If no explicit match is found, returns null to indicate no preference.
|
||||
*
|
||||
* @param uiLanguageCode - The UI language code (e.g., "en", "en-US", "nl-NL", "de-DE", "fr")
|
||||
* @returns The matching identity generator language code or null if no match
|
||||
*
|
||||
* @example
|
||||
* mapUiLanguageToIdentityLanguage("en-US") // returns "en"
|
||||
* mapUiLanguageToIdentityLanguage("nl") // returns "nl"
|
||||
* mapUiLanguageToIdentityLanguage("de-CH") // returns "de"
|
||||
* mapUiLanguageToIdentityLanguage("ja") // returns null (no Japanese identity generator)
|
||||
*/
|
||||
export function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null {
|
||||
if (!uiLanguageCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedCode = uiLanguageCode.toLowerCase();
|
||||
|
||||
// First, try exact match with the primary value
|
||||
const exactMatch = LANGUAGE_DEFS.find(lang => lang.value.toLowerCase() === normalizedCode);
|
||||
if (exactMatch) {
|
||||
return exactMatch.value;
|
||||
}
|
||||
|
||||
// Then, try matching with alternative codes
|
||||
const alternativeMatch = LANGUAGE_DEFS.find(lang =>
|
||||
lang.alternativeCodes?.some(code => code.toLowerCase() === normalizedCode)
|
||||
);
|
||||
if (alternativeMatch) {
|
||||
return alternativeMatch.value;
|
||||
}
|
||||
|
||||
// Finally, try matching the base language code (e.g., "en" from "en-US")
|
||||
const baseCode = normalizedCode.split('-')[0];
|
||||
const baseMatch = LANGUAGE_DEFS.find(lang => lang.value.toLowerCase() === baseCode);
|
||||
if (baseMatch) {
|
||||
return baseMatch.value;
|
||||
}
|
||||
|
||||
// No match found
|
||||
return null;
|
||||
return [...SUPPORTED_LANGUAGE_CODES];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user