diff --git a/apps/browser-extension/src/entrypoints/popup/components/Forms/PasswordConfigForm.tsx b/apps/browser-extension/src/entrypoints/popup/components/Forms/PasswordConfigForm.tsx index b64b0d46f..3cb520a0e 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/Forms/PasswordConfigForm.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/Forms/PasswordConfigForm.tsx @@ -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 = ({ 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). diff --git a/apps/browser-extension/src/utils/RustCore.ts b/apps/browser-extension/src/utils/RustCore.ts index 010d75694..7cb338a88 100644 --- a/apps/browser-extension/src/utils/RustCore.ts +++ b/apps/browser-extension/src/utils/RustCore.ts @@ -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 { */ export async function generatePassword(settings: PasswordSettings, seed?: string): Promise { 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 { + 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. diff --git a/apps/browser-extension/src/utils/db/repositories/SettingsRepository.ts b/apps/browser-extension/src/utils/db/repositories/SettingsRepository.ts index 8e7a88d4e..f1d56048a 100644 --- a/apps/browser-extension/src/utils/db/repositories/SettingsRepository.ts +++ b/apps/browser-extension/src/utils/db/repositories/SettingsRepository.ts @@ -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; } /** diff --git a/apps/browser-extension/src/utils/dist/core/identity-generator/index.d.ts b/apps/browser-extension/src/utils/dist/core/identity-generator/index.d.ts index 0622ddd0d..2e6235283 100644 --- a/apps/browser-extension/src/utils/dist/core/identity-generator/index.d.ts +++ b/apps/browser-extension/src/utils/dist/core/identity-generator/index.d.ts @@ -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 }; diff --git a/apps/browser-extension/src/utils/dist/core/identity-generator/index.js b/apps/browser-extension/src/utils/dist/core/identity-generator/index.js index a4f3c5090..41780e57d 100644 --- a/apps/browser-extension/src/utils/dist/core/identity-generator/index.js +++ b/apps/browser-extension/src/utils/dist/core/identity-generator/index.js @@ -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 }); diff --git a/apps/browser-extension/src/utils/dist/core/identity-generator/index.mjs b/apps/browser-extension/src/utils/dist/core/identity-generator/index.mjs index c19ae5b4e..5d4cb295a 100644 --- a/apps/browser-extension/src/utils/dist/core/identity-generator/index.mjs +++ b/apps/browser-extension/src/utils/dist/core/identity-generator/index.mjs @@ -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 }; diff --git a/apps/browser-extension/src/utils/dist/core/models/defaults/index.d.ts b/apps/browser-extension/src/utils/dist/core/models/defaults/index.d.ts index 2c5e628a2..e9380650f 100644 --- a/apps/browser-extension/src/utils/dist/core/models/defaults/index.d.ts +++ b/apps/browser-extension/src/utils/dist/core/models/defaults/index.d.ts @@ -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 }; diff --git a/apps/browser-extension/src/utils/dist/core/models/defaults/index.js b/apps/browser-extension/src/utils/dist/core/models/defaults/index.js index cc870c2f8..9c2b09b1e 100644 --- a/apps/browser-extension/src/utils/dist/core/models/defaults/index.js +++ b/apps/browser-extension/src/utils/dist/core/models/defaults/index.js @@ -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 }; diff --git a/apps/mobile-app/utils/SqliteClient.ts b/apps/mobile-app/utils/SqliteClient.ts index d4d595eed..0f7d34b4c 100644 --- a/apps/mobile-app/utils/SqliteClient.ts +++ b/apps/mobile-app/utils/SqliteClient.ts @@ -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'; diff --git a/apps/server/AliasVault.Client/Main/Components/Settings/PasswordSettingsPopup.razor b/apps/server/AliasVault.Client/Main/Components/Settings/PasswordSettingsPopup.razor index e781fb7b7..ea85008e6 100644 --- a/apps/server/AliasVault.Client/Main/Components/Settings/PasswordSettingsPopup.razor +++ b/apps/server/AliasVault.Client/Main/Components/Settings/PasswordSettingsPopup.razor @@ -97,7 +97,7 @@