From f2159e12635b6c8ba22e4e0b3c5fcb8f0e3ea76d Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 28 Jun 2026 21:13:04 +0200 Subject: [PATCH] Update identity generator language list references (#776) --- .../settings/IdentityGeneratorSettings.tsx | 22 +-- apps/browser-extension/src/i18n/config.ts | 123 ++--------------- .../dist/core/identity-generator/index.d.ts | 35 +---- .../dist/core/identity-generator/index.js | 90 +++---------- .../dist/core/identity-generator/index.mjs | 90 +++---------- .../utils/dist/core/models/defaults/index.js | 8 +- .../settings/identity-generator-language.tsx | 16 ++- .../(tabs)/settings/identity-generator.tsx | 10 +- .../dist/core/identity-generator/index.d.ts | 35 +---- .../dist/core/identity-generator/index.js | 90 +++---------- .../dist/core/identity-generator/index.mjs | 90 +++---------- .../utils/dist/core/models/defaults/index.js | 8 +- .../Main/Models/Languages.cs | 47 ++++++- .../Services/JsInterop/JsInteropService.cs | 18 ++- .../Services/LanguageService.cs | 55 ++------ .../dist/core/identity-generator/index.d.ts | 35 +---- .../js/dist/core/identity-generator/index.js | 90 +++---------- .../js/dist/core/identity-generator/index.mjs | 90 +++---------- core/models/scripts/generate-languages.cjs | 39 ++++++ core/models/src/defaults/Languages.ts | 8 +- .../identity-generator/package-lock.json | 27 +++- .../src/__tests__/LanguageProvider.test.ts | 9 +- .../src/utils/LanguageProvider.ts | 126 +++++------------- 23 files changed, 342 insertions(+), 819 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/popup/pages/settings/IdentityGeneratorSettings.tsx b/apps/browser-extension/src/entrypoints/popup/pages/settings/IdentityGeneratorSettings.tsx index 7d47c64c5..b23f62d58 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/settings/IdentityGeneratorSettings.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/settings/IdentityGeneratorSettings.tsx @@ -6,7 +6,8 @@ import { useDb } from '@/entrypoints/popup/context/DbContext'; import { useLoading } from '@/entrypoints/popup/context/LoadingContext'; import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate'; -import { getAvailableLanguages, getAvailableAgeRanges, ILanguageOption, IAgeRangeOption } from '@/utils/dist/core/identity-generator'; +import { getAvailableLanguages, getAvailableAgeRanges, IAgeRangeOption } from '@/utils/dist/core/identity-generator'; +import { getLanguageInfo } from '@/utils/dist/core/models/defaults'; /** * Identity Generator Settings page component. @@ -21,7 +22,7 @@ const IdentityGeneratorSettings: React.FC = () => { const [language, setLanguage] = useState('en'); const [gender, setGender] = useState('random'); const [ageRange, setAgeRange] = useState('random'); - const [languageOptions, setLanguageOptions] = useState([]); + const [languageOptions, setLanguageOptions] = useState([]); const [ageRangeOptions, setAgeRangeOptions] = useState([]); const GENDER_OPTIONS = [ @@ -34,7 +35,9 @@ const IdentityGeneratorSettings: React.FC = () => { * Load settings on mount. */ useEffect(() => { - const languages = getAvailableLanguages(); + const languages = getAvailableLanguages().sort( + (a, b) => getLanguageInfo(a).label.localeCompare(getLanguageInfo(b).label) + ); const ranges = getAvailableAgeRanges(); setLanguageOptions(languages); setAgeRangeOptions(ranges); @@ -109,11 +112,14 @@ const IdentityGeneratorSettings: React.FC = () => { onChange={(e) => handleLanguageChange(e.target.value)} className="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" > - {languageOptions.map((option) => ( - - ))} + {languageOptions.map((code) => { + const info = getLanguageInfo(code); + return ( + + ); + })} diff --git a/apps/browser-extension/src/i18n/config.ts b/apps/browser-extension/src/i18n/config.ts index 831a80135..da83b69ec 100644 --- a/apps/browser-extension/src/i18n/config.ts +++ b/apps/browser-extension/src/i18n/config.ts @@ -3,6 +3,8 @@ * Add new languages here to make them available throughout the application */ +import { getLanguageInfo } from '@/utils/dist/core/models/defaults'; + import daTranslations from './locales/da.json'; import deTranslations from './locales/de.json'; import enTranslations from './locales/en.json'; @@ -76,121 +78,13 @@ export const LANGUAGE_RESOURCES = { }; /** - * List of all available languages with their code, name, native name and flag. - * When adding a new language, add the language to the map here. + * List of all available UI languages with their code, native name and flag. */ -export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [ - { - code: 'da', - name: 'Danish', - nativeName: 'Dansk', - flag: '🇩🇰' - }, - { - code: 'de', - name: 'German', - nativeName: 'Deutsch', - flag: '🇩🇪' - }, - { - code: 'en', - name: 'English', - nativeName: 'English', - flag: '🇺🇸' - }, - { - code: 'es', - name: 'Spanish', - nativeName: 'Español', - flag: '🇪🇸' - }, - { - code: 'fi', - name: 'Finnish', - nativeName: 'Suomi', - flag: '🇫🇮' - }, - { - code: 'fr', - name: 'French', - nativeName: 'Français', - flag: '🇫🇷' - }, - { - code: 'he', - name: 'Hebrew', - nativeName: 'עברית', - flag: '🇮🇱' - }, - { - code: 'it', - name: 'Italian', - nativeName: 'Italiano', - flag: '🇮🇹' - }, - { - code: 'nl', - name: 'Dutch', - nativeName: 'Nederlands', - flag: '🇳🇱' - }, - { - code: 'pl', - name: 'Polish', - nativeName: 'Polski', - flag: '🇵🇱' - }, - { - code: 'pt', - name: 'Portuguese Brazilian', - nativeName: 'Português Brasileiro', - flag: '🇧🇷' - }, - { - code: 'ro', - name: 'Romanian', - nativeName: 'Română', - flag: '🇷🇴' - }, - { - code: 'ru', - name: 'Russian', - nativeName: 'Русский', - flag: '🇷🇺' - }, - { - code: 'sv', - name: 'Swedish', - nativeName: 'Svenska', - flag: '🇸🇪' - }, - { - code: 'uk', - name: 'Ukrainian', - nativeName: 'Українська', - flag: '🇺🇦' - }, - { - code: 'zh', - name: 'Chinese', - nativeName: '简体中文', - flag: '🇨🇳' - }, - /* - * { - * code: 'es', - * name: 'Spanish', - * nativeName: 'Español', - * flag: '🇪🇸' - * }, - * { - * code: 'fr', - * name: 'French', - * nativeName: 'Français', - * flag: '🇫🇷' - * }, - */ -]; +export const AVAILABLE_LANGUAGES: ILanguageConfig[] = + (Object.keys(LANGUAGE_RESOURCES) as Array).map((code) => { + const info = getLanguageInfo(code); + return { code, nativeName: info.label, flag: info.flag }; + }); /** * Default language that is used when no language is set in the browser or when a localized string is not found for the current language. @@ -201,7 +95,6 @@ export const LANGUAGE_CODES = AVAILABLE_LANGUAGES.map(lang => lang.code); export interface ILanguageConfig { code: string; - name: string; nativeName: string; flag?: string; } 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 d3568c843..0622ddd0d 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 @@ -395,35 +395,10 @@ declare function getAvailableAgeRanges(): IAgeRangeOption[]; declare function convertAgeRangeToBirthdateOptions(ageRange: string): IBirthdateOptions | null; /** - * Represents a language option for identity generation. + * Gets all available language codes for identity generation. + * @returns Array of ISO language codes (e.g. ["da", "de", "en", ...]). */ -interface ILanguageOption { - /** - * The language code (e.g., "en", "nl", "de") - */ - value: string; - /** - * The display label in the native language (e.g., "English", "Nederlands", "Deutsch") - */ - label: string; - /** - * The flag emoji for the language (e.g., "🇬🇧", "🇳🇱", "🇩🇪") - */ - flag: 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[]; -} -/** - * Gets all available languages for identity generation. - * Display labels are in the native language, with optional flag emoji that clients can choose to display. - * Languages are sorted alphabetically by label for consistent display across all platforms. - * @returns Array of language options sorted alphabetically by label - */ -declare function getAvailableLanguages(): ILanguageOption[]; +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. @@ -435,7 +410,7 @@ declare function getAvailableLanguages(): ILanguageOption[]; * mapUiLanguageToIdentityLanguage("en-US") // returns "en" * mapUiLanguageToIdentityLanguage("nl") // returns "nl" * mapUiLanguageToIdentityLanguage("de-CH") // returns "de" - * mapUiLanguageToIdentityLanguage("fr") // returns null (no French identity generator) + * mapUiLanguageToIdentityLanguage("ja") // returns null (no Japanese identity generator) */ declare function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null; @@ -454,4 +429,4 @@ declare const CreateIdentityGenerator: (language: string) => IdentityGenerator; */ declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator; -export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type ILanguageOption, 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, mapUiLanguageToIdentityLanguage }; 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 6c2e9ebc5..a4f3c5090 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 @@ -11763,95 +11763,39 @@ 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"] } +]; function getAvailableLanguages() { - const languages = [ - { - value: "da", - label: "Dansk", - flag: "\u{1F1E9}\u{1F1F0}", - alternativeCodes: ["da-DK"] - }, - { - value: "de", - label: "Deutsch", - flag: "\u{1F1E9}\u{1F1EA}", - alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] - }, - { - value: "en", - label: "English", - flag: "\u{1F1EC}\u{1F1E7}", - alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] - }, - { - value: "es", - label: "Espa\xF1ol", - flag: "\u{1F1EA}\u{1F1F8}", - 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", - label: "Fran\xE7ais", - flag: "\u{1F1EB}\u{1F1F7}", - alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] - }, - { - value: "it", - label: "Italiano", - flag: "\u{1F1EE}\u{1F1F9}", - alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] - }, - { - value: "nl", - label: "Nederlands", - flag: "\u{1F1F3}\u{1F1F1}", - alternativeCodes: ["nl-NL", "nl-BE"] - }, - { - value: "ro", - label: "Rom\xE2n\u0103", - flag: "\u{1F1F7}\u{1F1F4}", - alternativeCodes: ["ro-RO", "ro-MD"] - }, - { - value: "sv", - label: "Svenska", - flag: "\u{1F1F8}\u{1F1EA}", - alternativeCodes: ["sv-SE", "sv-FI"] - }, - { - value: "ur", - label: "\u0627\u0631\u062F\u0648", - flag: "\u{1F1F5}\u{1F1F0}", - alternativeCodes: ["ur-PK", "ur-IN"] - }, - { - value: "fa", - label: "\u0641\u0627\u0631\u0633\u06CC", - flag: "\u{1F1EE}\u{1F1F7}", - alternativeCodes: ["fa-IR", "fa-AF"] - } - ]; - return languages.sort((a, b) => a.label.localeCompare(b.label)); + return LANGUAGE_DEFS.map((lang) => lang.value); } function mapUiLanguageToIdentityLanguage(uiLanguageCode) { if (!uiLanguageCode) { return null; } const normalizedCode = uiLanguageCode.toLowerCase(); - const availableLanguages = getAvailableLanguages(); - const exactMatch = availableLanguages.find((lang) => lang.value.toLowerCase() === normalizedCode); + const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode); if (exactMatch) { return exactMatch.value; } - const alternativeMatch = availableLanguages.find( + 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 = availableLanguages.find((lang) => lang.value.toLowerCase() === baseCode); + const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode); if (baseMatch) { return baseMatch.value; } 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 7d8cd682e..c19ae5b4e 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,95 +11718,39 @@ 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"] } +]; function getAvailableLanguages() { - const languages = [ - { - value: "da", - label: "Dansk", - flag: "\u{1F1E9}\u{1F1F0}", - alternativeCodes: ["da-DK"] - }, - { - value: "de", - label: "Deutsch", - flag: "\u{1F1E9}\u{1F1EA}", - alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] - }, - { - value: "en", - label: "English", - flag: "\u{1F1EC}\u{1F1E7}", - alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] - }, - { - value: "es", - label: "Espa\xF1ol", - flag: "\u{1F1EA}\u{1F1F8}", - 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", - label: "Fran\xE7ais", - flag: "\u{1F1EB}\u{1F1F7}", - alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] - }, - { - value: "it", - label: "Italiano", - flag: "\u{1F1EE}\u{1F1F9}", - alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] - }, - { - value: "nl", - label: "Nederlands", - flag: "\u{1F1F3}\u{1F1F1}", - alternativeCodes: ["nl-NL", "nl-BE"] - }, - { - value: "ro", - label: "Rom\xE2n\u0103", - flag: "\u{1F1F7}\u{1F1F4}", - alternativeCodes: ["ro-RO", "ro-MD"] - }, - { - value: "sv", - label: "Svenska", - flag: "\u{1F1F8}\u{1F1EA}", - alternativeCodes: ["sv-SE", "sv-FI"] - }, - { - value: "ur", - label: "\u0627\u0631\u062F\u0648", - flag: "\u{1F1F5}\u{1F1F0}", - alternativeCodes: ["ur-PK", "ur-IN"] - }, - { - value: "fa", - label: "\u0641\u0627\u0631\u0633\u06CC", - flag: "\u{1F1EE}\u{1F1F7}", - alternativeCodes: ["fa-IR", "fa-AF"] - } - ]; - return languages.sort((a, b) => a.label.localeCompare(b.label)); + return LANGUAGE_DEFS.map((lang) => lang.value); } function mapUiLanguageToIdentityLanguage(uiLanguageCode) { if (!uiLanguageCode) { return null; } const normalizedCode = uiLanguageCode.toLowerCase(); - const availableLanguages = getAvailableLanguages(); - const exactMatch = availableLanguages.find((lang) => lang.value.toLowerCase() === normalizedCode); + const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode); if (exactMatch) { return exactMatch.value; } - const alternativeMatch = availableLanguages.find( + 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 = availableLanguages.find((lang) => lang.value.toLowerCase() === baseCode); + const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode); if (baseMatch) { return baseMatch.value; } 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 a261ea496..cc870c2f8 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,7 +13,7 @@ var MAX_WORD_COUNT = 10; // src/defaults/Languages.ts var DEFAULT_LANGUAGE_CODE = "en"; var LANGUAGES = [ - { code: "en", flag: "\u{1F1EC}\u{1F1E7}", label: "English" }, + { 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" }, @@ -23,12 +23,14 @@ var LANGUAGES = [ { 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" }, + { 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: "\u4E2D\u6587" } + { 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" } ]; function normalizeLanguageCode(code) { return (code ?? "").slice(0, 2).toLowerCase(); diff --git a/apps/mobile-app/app/(tabs)/settings/identity-generator-language.tsx b/apps/mobile-app/app/(tabs)/settings/identity-generator-language.tsx index d0fc49ad2..561180600 100644 --- a/apps/mobile-app/app/(tabs)/settings/identity-generator-language.tsx +++ b/apps/mobile-app/app/(tabs)/settings/identity-generator-language.tsx @@ -4,7 +4,8 @@ import { useState, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleSheet, View, TouchableOpacity } from 'react-native'; -import { getAvailableLanguages, ILanguageOption } from '@/utils/dist/core/identity-generator'; +import { getAvailableLanguages } from '@/utils/dist/core/identity-generator'; +import { getLanguageInfo } from '@/utils/dist/core/models/defaults'; import { useColors } from '@/hooks/useColorScheme'; import { useVaultMutate } from '@/hooks/useVaultMutate'; @@ -28,7 +29,9 @@ export default function IdentityGeneratorLanguageScreen(): React.ReactNode { const { executeVaultMutation } = useVaultMutate(); const [language, setLanguage] = useState('en'); - const [languageOptions] = useState(() => getAvailableLanguages()); + const [languageOptions] = useState(() => + getAvailableLanguages().sort((a, b) => getLanguageInfo(a).label.localeCompare(getLanguageInfo(b).label)) + ); // Store pending changes and initial values const pendingChanges = useRef<{ language?: string }>({}); @@ -130,14 +133,15 @@ export default function IdentityGeneratorLanguageScreen(): React.ReactNode { {languageOptions.map((option, index) => { const isLast = index === languageOptions.length - 1; + const info = getLanguageInfo(option); return ( handleLanguageSelect(option.value)} + onPress={() => handleLanguageSelect(option)} > - {option.flag} {option.label} - {language === option.value && ( + {info.flag} {info.label} + {language === option && ( )} diff --git a/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx b/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx index 8ad47434e..ca9b5f894 100644 --- a/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx +++ b/apps/mobile-app/app/(tabs)/settings/identity-generator.tsx @@ -4,7 +4,8 @@ import { useState, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleSheet, View, TouchableOpacity } from 'react-native'; -import { getAvailableAgeRanges, IAgeRangeOption, getAvailableLanguages, ILanguageOption } from '@/utils/dist/core/identity-generator'; +import { getAvailableAgeRanges, IAgeRangeOption } from '@/utils/dist/core/identity-generator'; +import { getLanguageInfo } from '@/utils/dist/core/models/defaults'; import { useColors } from '@/hooks/useColorScheme'; import { useVaultMutate } from '@/hooks/useVaultMutate'; @@ -29,7 +30,6 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode { const [language, setLanguage] = useState('en'); const [gender, setGender] = useState('random'); const [ageRange, setAgeRange] = useState('random'); - const [languageOptions] = useState(() => getAvailableLanguages()); const [ageRangeOptions] = useState(() => getAvailableAgeRanges()); // Store pending changes and initial values (language is managed in subview) @@ -119,9 +119,9 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode { * Get the display label for the current language */ const getLanguageDisplayLabel = useCallback((): string => { - const option = languageOptions.find(opt => opt.value === language); - return option ? `${option.flag} ${option.label}` : language; - }, [language, languageOptions]); + const info = getLanguageInfo(language); + return `${info.flag} ${info.label}`; + }, [language]); /** * Handle gender change diff --git a/apps/mobile-app/utils/dist/core/identity-generator/index.d.ts b/apps/mobile-app/utils/dist/core/identity-generator/index.d.ts index d3568c843..0622ddd0d 100644 --- a/apps/mobile-app/utils/dist/core/identity-generator/index.d.ts +++ b/apps/mobile-app/utils/dist/core/identity-generator/index.d.ts @@ -395,35 +395,10 @@ declare function getAvailableAgeRanges(): IAgeRangeOption[]; declare function convertAgeRangeToBirthdateOptions(ageRange: string): IBirthdateOptions | null; /** - * Represents a language option for identity generation. + * Gets all available language codes for identity generation. + * @returns Array of ISO language codes (e.g. ["da", "de", "en", ...]). */ -interface ILanguageOption { - /** - * The language code (e.g., "en", "nl", "de") - */ - value: string; - /** - * The display label in the native language (e.g., "English", "Nederlands", "Deutsch") - */ - label: string; - /** - * The flag emoji for the language (e.g., "🇬🇧", "🇳🇱", "🇩🇪") - */ - flag: 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[]; -} -/** - * Gets all available languages for identity generation. - * Display labels are in the native language, with optional flag emoji that clients can choose to display. - * Languages are sorted alphabetically by label for consistent display across all platforms. - * @returns Array of language options sorted alphabetically by label - */ -declare function getAvailableLanguages(): ILanguageOption[]; +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. @@ -435,7 +410,7 @@ declare function getAvailableLanguages(): ILanguageOption[]; * mapUiLanguageToIdentityLanguage("en-US") // returns "en" * mapUiLanguageToIdentityLanguage("nl") // returns "nl" * mapUiLanguageToIdentityLanguage("de-CH") // returns "de" - * mapUiLanguageToIdentityLanguage("fr") // returns null (no French identity generator) + * mapUiLanguageToIdentityLanguage("ja") // returns null (no Japanese identity generator) */ declare function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null; @@ -454,4 +429,4 @@ declare const CreateIdentityGenerator: (language: string) => IdentityGenerator; */ declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator; -export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type ILanguageOption, 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, mapUiLanguageToIdentityLanguage }; diff --git a/apps/mobile-app/utils/dist/core/identity-generator/index.js b/apps/mobile-app/utils/dist/core/identity-generator/index.js index 6c2e9ebc5..a4f3c5090 100644 --- a/apps/mobile-app/utils/dist/core/identity-generator/index.js +++ b/apps/mobile-app/utils/dist/core/identity-generator/index.js @@ -11763,95 +11763,39 @@ 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"] } +]; function getAvailableLanguages() { - const languages = [ - { - value: "da", - label: "Dansk", - flag: "\u{1F1E9}\u{1F1F0}", - alternativeCodes: ["da-DK"] - }, - { - value: "de", - label: "Deutsch", - flag: "\u{1F1E9}\u{1F1EA}", - alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] - }, - { - value: "en", - label: "English", - flag: "\u{1F1EC}\u{1F1E7}", - alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] - }, - { - value: "es", - label: "Espa\xF1ol", - flag: "\u{1F1EA}\u{1F1F8}", - 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", - label: "Fran\xE7ais", - flag: "\u{1F1EB}\u{1F1F7}", - alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] - }, - { - value: "it", - label: "Italiano", - flag: "\u{1F1EE}\u{1F1F9}", - alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] - }, - { - value: "nl", - label: "Nederlands", - flag: "\u{1F1F3}\u{1F1F1}", - alternativeCodes: ["nl-NL", "nl-BE"] - }, - { - value: "ro", - label: "Rom\xE2n\u0103", - flag: "\u{1F1F7}\u{1F1F4}", - alternativeCodes: ["ro-RO", "ro-MD"] - }, - { - value: "sv", - label: "Svenska", - flag: "\u{1F1F8}\u{1F1EA}", - alternativeCodes: ["sv-SE", "sv-FI"] - }, - { - value: "ur", - label: "\u0627\u0631\u062F\u0648", - flag: "\u{1F1F5}\u{1F1F0}", - alternativeCodes: ["ur-PK", "ur-IN"] - }, - { - value: "fa", - label: "\u0641\u0627\u0631\u0633\u06CC", - flag: "\u{1F1EE}\u{1F1F7}", - alternativeCodes: ["fa-IR", "fa-AF"] - } - ]; - return languages.sort((a, b) => a.label.localeCompare(b.label)); + return LANGUAGE_DEFS.map((lang) => lang.value); } function mapUiLanguageToIdentityLanguage(uiLanguageCode) { if (!uiLanguageCode) { return null; } const normalizedCode = uiLanguageCode.toLowerCase(); - const availableLanguages = getAvailableLanguages(); - const exactMatch = availableLanguages.find((lang) => lang.value.toLowerCase() === normalizedCode); + const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode); if (exactMatch) { return exactMatch.value; } - const alternativeMatch = availableLanguages.find( + 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 = availableLanguages.find((lang) => lang.value.toLowerCase() === baseCode); + const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode); if (baseMatch) { return baseMatch.value; } diff --git a/apps/mobile-app/utils/dist/core/identity-generator/index.mjs b/apps/mobile-app/utils/dist/core/identity-generator/index.mjs index 7d8cd682e..c19ae5b4e 100644 --- a/apps/mobile-app/utils/dist/core/identity-generator/index.mjs +++ b/apps/mobile-app/utils/dist/core/identity-generator/index.mjs @@ -11718,95 +11718,39 @@ 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"] } +]; function getAvailableLanguages() { - const languages = [ - { - value: "da", - label: "Dansk", - flag: "\u{1F1E9}\u{1F1F0}", - alternativeCodes: ["da-DK"] - }, - { - value: "de", - label: "Deutsch", - flag: "\u{1F1E9}\u{1F1EA}", - alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] - }, - { - value: "en", - label: "English", - flag: "\u{1F1EC}\u{1F1E7}", - alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] - }, - { - value: "es", - label: "Espa\xF1ol", - flag: "\u{1F1EA}\u{1F1F8}", - 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", - label: "Fran\xE7ais", - flag: "\u{1F1EB}\u{1F1F7}", - alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] - }, - { - value: "it", - label: "Italiano", - flag: "\u{1F1EE}\u{1F1F9}", - alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] - }, - { - value: "nl", - label: "Nederlands", - flag: "\u{1F1F3}\u{1F1F1}", - alternativeCodes: ["nl-NL", "nl-BE"] - }, - { - value: "ro", - label: "Rom\xE2n\u0103", - flag: "\u{1F1F7}\u{1F1F4}", - alternativeCodes: ["ro-RO", "ro-MD"] - }, - { - value: "sv", - label: "Svenska", - flag: "\u{1F1F8}\u{1F1EA}", - alternativeCodes: ["sv-SE", "sv-FI"] - }, - { - value: "ur", - label: "\u0627\u0631\u062F\u0648", - flag: "\u{1F1F5}\u{1F1F0}", - alternativeCodes: ["ur-PK", "ur-IN"] - }, - { - value: "fa", - label: "\u0641\u0627\u0631\u0633\u06CC", - flag: "\u{1F1EE}\u{1F1F7}", - alternativeCodes: ["fa-IR", "fa-AF"] - } - ]; - return languages.sort((a, b) => a.label.localeCompare(b.label)); + return LANGUAGE_DEFS.map((lang) => lang.value); } function mapUiLanguageToIdentityLanguage(uiLanguageCode) { if (!uiLanguageCode) { return null; } const normalizedCode = uiLanguageCode.toLowerCase(); - const availableLanguages = getAvailableLanguages(); - const exactMatch = availableLanguages.find((lang) => lang.value.toLowerCase() === normalizedCode); + const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode); if (exactMatch) { return exactMatch.value; } - const alternativeMatch = availableLanguages.find( + 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 = availableLanguages.find((lang) => lang.value.toLowerCase() === baseCode); + const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode); if (baseMatch) { return baseMatch.value; } diff --git a/apps/mobile-app/utils/dist/core/models/defaults/index.js b/apps/mobile-app/utils/dist/core/models/defaults/index.js index a261ea496..cc870c2f8 100644 --- a/apps/mobile-app/utils/dist/core/models/defaults/index.js +++ b/apps/mobile-app/utils/dist/core/models/defaults/index.js @@ -13,7 +13,7 @@ var MAX_WORD_COUNT = 10; // src/defaults/Languages.ts var DEFAULT_LANGUAGE_CODE = "en"; var LANGUAGES = [ - { code: "en", flag: "\u{1F1EC}\u{1F1E7}", label: "English" }, + { 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" }, @@ -23,12 +23,14 @@ var LANGUAGES = [ { 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" }, + { 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: "\u4E2D\u6587" } + { 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" } ]; function normalizeLanguageCode(code) { return (code ?? "").slice(0, 2).toLowerCase(); diff --git a/apps/server/AliasVault.Client/Main/Models/Languages.cs b/apps/server/AliasVault.Client/Main/Models/Languages.cs index c16d81388..7c69d6dc2 100644 --- a/apps/server/AliasVault.Client/Main/Models/Languages.cs +++ b/apps/server/AliasVault.Client/Main/Models/Languages.cs @@ -23,7 +23,7 @@ public static class Languages /// private static readonly (string Code, string Flag, string Label)[] Meta = [ - ("en", "🇬🇧", "English"), + ("en", "🇺🇸", "English"), ("nl", "🇳🇱", "Nederlands"), ("de", "🇩🇪", "Deutsch"), ("fr", "🇫🇷", "Français"), @@ -33,12 +33,14 @@ public static class Languages ("fi", "🇫🇮", "Suomi"), ("he", "🇮🇱", "עברית"), ("pl", "🇵🇱", "Polski"), - ("pt", "🇧🇷", "Português"), + ("pt", "🇧🇷", "Português Brasileiro"), ("ro", "🇷🇴", "Română"), ("ru", "🇷🇺", "Русский"), ("sv", "🇸🇪", "Svenska"), ("uk", "🇺🇦", "Українська"), - ("zh", "🇨🇳", "中文"), + ("zh", "🇨🇳", "简体中文"), + ("ur", "🇵🇰", "اردو"), + ("fa", "🇮🇷", "فارسی"), ]; /// @@ -72,6 +74,45 @@ public static class Languages return $"🌐 {code}"; } + /// + /// Get the flag emoji for an ISO language code. Falls back to a globe flag for unknown languages. + /// + /// The ISO language code (case-insensitive). + /// The flag emoji, e.g. "🇳🇱". + public static string GetFlag(string code) + { + var iso = Normalize(code); + foreach (var (metaCode, flag, _) in Meta) + { + if (metaCode == iso) + { + return flag; + } + } + + return "🌐"; + } + + /// + /// Get the native display label for an ISO language code. Falls back to the raw code for unknown + /// languages. + /// + /// The ISO language code (case-insensitive). + /// The native label, e.g. "Nederlands". + public static string GetLabel(string code) + { + var iso = Normalize(code); + foreach (var (metaCode, _, label) in Meta) + { + if (metaCode == iso) + { + return label; + } + } + + return code; + } + /// /// 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, diff --git a/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs b/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs index 3511a15c7..b938f8c81 100644 --- a/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs +++ b/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs @@ -9,6 +9,7 @@ namespace AliasVault.Client.Services.JsInterop; using System.Security.Cryptography; using System.Text.Json; +using AliasVault.Client.Main.Models; using AliasVault.Client.Services.JsInterop.Models; using AliasVault.Shared.Core; using Microsoft.AspNetCore.Components; @@ -436,8 +437,21 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) await InitializeAsync(); } - var result = await _identityGeneratorModule!.InvokeAsync>("getAvailableLanguages"); - return result ?? new List(); + var codes = await _identityGeneratorModule!.InvokeAsync>("getAvailableLanguages"); + if (codes == null) + { + return new List(); + } + + return codes + .Select(code => new LanguageOption + { + Value = code, + Flag = Languages.GetFlag(code), + Label = Languages.GetLabel(code), + }) + .OrderBy(option => option.Label, StringComparer.CurrentCulture) + .ToList(); } catch (JSException ex) { diff --git a/apps/server/AliasVault.Client/Services/LanguageService.cs b/apps/server/AliasVault.Client/Services/LanguageService.cs index 6da95bba8..5fbb0ce4a 100644 --- a/apps/server/AliasVault.Client/Services/LanguageService.cs +++ b/apps/server/AliasVault.Client/Services/LanguageService.cs @@ -8,6 +8,7 @@ namespace AliasVault.Client.Services; using System.Globalization; +using AliasVault.Client.Main.Models; using AliasVault.Client.Services.Database; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization; @@ -23,31 +24,13 @@ public class LanguageService( DbService dbService) { /// - /// Language configuration containing all supported languages. - /// To add a new language, simply add a new entry to this list. + /// The set of UI languages the client has translations for. Native display names and flag emojis + /// are sourced from the shared reference, so adding a new language only + /// requires adding its ISO code here (plus the matching translation resources). /// - private static readonly List SupportedLanguages = new() + private static readonly List SupportedLanguageCodes = new() { - new LanguageConfig("da", "Dansk", "🇩🇰"), - new LanguageConfig("de", "Deutsch", "🇩🇪"), - new LanguageConfig("en", "English", "🇺🇸"), - new LanguageConfig("es", "Español", "🇪🇸"), - new LanguageConfig("fi", "Suomi", "🇫🇮"), - new LanguageConfig("fr", "Français", "🇫🇷"), - new LanguageConfig("he", "עברית", "🇮🇱"), - new LanguageConfig("it", "Italiano", "🇮🇹"), - new LanguageConfig("nl", "Nederlands", "🇳🇱"), - new LanguageConfig("pl", "Polski", "🇵🇱"), - new LanguageConfig("pt", "Português Brasileiro", "🇧🇷"), - new LanguageConfig("ro", "Română", "🇷🇴"), - new LanguageConfig("ru", "Русский", "🇷🇺"), - new LanguageConfig("sv", "Svenska", "🇸🇪"), - new LanguageConfig("uk", "Українська", "🇺🇦"), - new LanguageConfig("zh", "简体中文", "🇨🇳"), - - // Add new languages here: - // new LanguageConfig("fr", "Français", "🇫🇷"), - // new LanguageConfig("es", "Español", "🇪🇸"), + "da", "de", "en", "es", "fi", "fr", "he", "it", "nl", "pl", "pt", "ro", "ru", "sv", "uk", "zh", }; private readonly ILocalStorageService _localStorage = localStorage; @@ -66,16 +49,7 @@ public class LanguageService( /// Dictionary of language codes and display names. public static Dictionary GetSupportedLanguages() { - return SupportedLanguages.ToDictionary(lang => lang.Code, lang => lang.DisplayName); - } - - /// - /// Gets the list of supported languages with flag emojis. - /// - /// Dictionary of language codes and display names with flag emojis. - public static Dictionary GetSupportedLanguagesWithFlags() - { - return SupportedLanguages.ToDictionary(lang => lang.Code, lang => $"{lang.FlagEmoji} {lang.DisplayName}"); + return SupportedLanguageCodes.ToDictionary(code => code, Languages.GetLabel); } /// @@ -85,8 +59,7 @@ public class LanguageService( /// Flag emoji string. public static string GetLanguageFlag(string languageCode) { - var language = SupportedLanguages.FirstOrDefault(lang => lang.Code == languageCode); - return language?.FlagEmoji ?? "🌐"; + return Languages.GetFlag(languageCode); } /// @@ -96,8 +69,7 @@ public class LanguageService( /// Display name string. public static string GetLanguageDisplayName(string languageCode) { - var language = SupportedLanguages.FirstOrDefault(lang => lang.Code == languageCode); - return language?.DisplayName ?? languageCode; + return Languages.GetLabel(languageCode); } /// @@ -107,7 +79,7 @@ public class LanguageService( /// True if the language is supported, false otherwise. public static bool IsLanguageSupported(string languageCode) { - return SupportedLanguages.Any(lang => lang.Code == languageCode); + return SupportedLanguageCodes.Contains(languageCode); } /// @@ -116,7 +88,7 @@ public class LanguageService( /// Default language code. public static string GetDefaultLanguage() { - return SupportedLanguages.FirstOrDefault()?.Code ?? "en"; + return SupportedLanguageCodes.FirstOrDefault() ?? "en"; } /// @@ -333,9 +305,4 @@ public class LanguageService( // Ignore migration errors - user can still change language manually } } - - /// - /// Configuration for a supported language. - /// - private sealed record LanguageConfig(string Code, string DisplayName, string FlagEmoji); } diff --git a/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.d.ts b/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.d.ts index d3568c843..0622ddd0d 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.d.ts +++ b/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.d.ts @@ -395,35 +395,10 @@ declare function getAvailableAgeRanges(): IAgeRangeOption[]; declare function convertAgeRangeToBirthdateOptions(ageRange: string): IBirthdateOptions | null; /** - * Represents a language option for identity generation. + * Gets all available language codes for identity generation. + * @returns Array of ISO language codes (e.g. ["da", "de", "en", ...]). */ -interface ILanguageOption { - /** - * The language code (e.g., "en", "nl", "de") - */ - value: string; - /** - * The display label in the native language (e.g., "English", "Nederlands", "Deutsch") - */ - label: string; - /** - * The flag emoji for the language (e.g., "🇬🇧", "🇳🇱", "🇩🇪") - */ - flag: 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[]; -} -/** - * Gets all available languages for identity generation. - * Display labels are in the native language, with optional flag emoji that clients can choose to display. - * Languages are sorted alphabetically by label for consistent display across all platforms. - * @returns Array of language options sorted alphabetically by label - */ -declare function getAvailableLanguages(): ILanguageOption[]; +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. @@ -435,7 +410,7 @@ declare function getAvailableLanguages(): ILanguageOption[]; * mapUiLanguageToIdentityLanguage("en-US") // returns "en" * mapUiLanguageToIdentityLanguage("nl") // returns "nl" * mapUiLanguageToIdentityLanguage("de-CH") // returns "de" - * mapUiLanguageToIdentityLanguage("fr") // returns null (no French identity generator) + * mapUiLanguageToIdentityLanguage("ja") // returns null (no Japanese identity generator) */ declare function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null; @@ -454,4 +429,4 @@ declare const CreateIdentityGenerator: (language: string) => IdentityGenerator; */ declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator; -export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type ILanguageOption, 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, mapUiLanguageToIdentityLanguage }; diff --git a/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.js b/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.js index 6c2e9ebc5..a4f3c5090 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.js +++ b/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.js @@ -11763,95 +11763,39 @@ 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"] } +]; function getAvailableLanguages() { - const languages = [ - { - value: "da", - label: "Dansk", - flag: "\u{1F1E9}\u{1F1F0}", - alternativeCodes: ["da-DK"] - }, - { - value: "de", - label: "Deutsch", - flag: "\u{1F1E9}\u{1F1EA}", - alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] - }, - { - value: "en", - label: "English", - flag: "\u{1F1EC}\u{1F1E7}", - alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] - }, - { - value: "es", - label: "Espa\xF1ol", - flag: "\u{1F1EA}\u{1F1F8}", - 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", - label: "Fran\xE7ais", - flag: "\u{1F1EB}\u{1F1F7}", - alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] - }, - { - value: "it", - label: "Italiano", - flag: "\u{1F1EE}\u{1F1F9}", - alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] - }, - { - value: "nl", - label: "Nederlands", - flag: "\u{1F1F3}\u{1F1F1}", - alternativeCodes: ["nl-NL", "nl-BE"] - }, - { - value: "ro", - label: "Rom\xE2n\u0103", - flag: "\u{1F1F7}\u{1F1F4}", - alternativeCodes: ["ro-RO", "ro-MD"] - }, - { - value: "sv", - label: "Svenska", - flag: "\u{1F1F8}\u{1F1EA}", - alternativeCodes: ["sv-SE", "sv-FI"] - }, - { - value: "ur", - label: "\u0627\u0631\u062F\u0648", - flag: "\u{1F1F5}\u{1F1F0}", - alternativeCodes: ["ur-PK", "ur-IN"] - }, - { - value: "fa", - label: "\u0641\u0627\u0631\u0633\u06CC", - flag: "\u{1F1EE}\u{1F1F7}", - alternativeCodes: ["fa-IR", "fa-AF"] - } - ]; - return languages.sort((a, b) => a.label.localeCompare(b.label)); + return LANGUAGE_DEFS.map((lang) => lang.value); } function mapUiLanguageToIdentityLanguage(uiLanguageCode) { if (!uiLanguageCode) { return null; } const normalizedCode = uiLanguageCode.toLowerCase(); - const availableLanguages = getAvailableLanguages(); - const exactMatch = availableLanguages.find((lang) => lang.value.toLowerCase() === normalizedCode); + const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode); if (exactMatch) { return exactMatch.value; } - const alternativeMatch = availableLanguages.find( + 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 = availableLanguages.find((lang) => lang.value.toLowerCase() === baseCode); + const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode); if (baseMatch) { return baseMatch.value; } diff --git a/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.mjs b/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.mjs index 7d8cd682e..c19ae5b4e 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.mjs +++ b/apps/server/AliasVault.Client/wwwroot/js/dist/core/identity-generator/index.mjs @@ -11718,95 +11718,39 @@ 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"] } +]; function getAvailableLanguages() { - const languages = [ - { - value: "da", - label: "Dansk", - flag: "\u{1F1E9}\u{1F1F0}", - alternativeCodes: ["da-DK"] - }, - { - value: "de", - label: "Deutsch", - flag: "\u{1F1E9}\u{1F1EA}", - alternativeCodes: ["de-DE", "de-AT", "de-CH", "de-LU", "de-LI"] - }, - { - value: "en", - label: "English", - flag: "\u{1F1EC}\u{1F1E7}", - alternativeCodes: ["en-US", "en-GB", "en-CA", "en-AU", "en-NZ", "en-IE", "en-ZA", "en-SG", "en-IN"] - }, - { - value: "es", - label: "Espa\xF1ol", - flag: "\u{1F1EA}\u{1F1F8}", - 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", - label: "Fran\xE7ais", - flag: "\u{1F1EB}\u{1F1F7}", - alternativeCodes: ["fr-FR", "fr-CA", "fr-BE", "fr-CH", "fr-LU", "fr-MC"] - }, - { - value: "it", - label: "Italiano", - flag: "\u{1F1EE}\u{1F1F9}", - alternativeCodes: ["it-IT", "it-CH", "it-SM", "it-VA"] - }, - { - value: "nl", - label: "Nederlands", - flag: "\u{1F1F3}\u{1F1F1}", - alternativeCodes: ["nl-NL", "nl-BE"] - }, - { - value: "ro", - label: "Rom\xE2n\u0103", - flag: "\u{1F1F7}\u{1F1F4}", - alternativeCodes: ["ro-RO", "ro-MD"] - }, - { - value: "sv", - label: "Svenska", - flag: "\u{1F1F8}\u{1F1EA}", - alternativeCodes: ["sv-SE", "sv-FI"] - }, - { - value: "ur", - label: "\u0627\u0631\u062F\u0648", - flag: "\u{1F1F5}\u{1F1F0}", - alternativeCodes: ["ur-PK", "ur-IN"] - }, - { - value: "fa", - label: "\u0641\u0627\u0631\u0633\u06CC", - flag: "\u{1F1EE}\u{1F1F7}", - alternativeCodes: ["fa-IR", "fa-AF"] - } - ]; - return languages.sort((a, b) => a.label.localeCompare(b.label)); + return LANGUAGE_DEFS.map((lang) => lang.value); } function mapUiLanguageToIdentityLanguage(uiLanguageCode) { if (!uiLanguageCode) { return null; } const normalizedCode = uiLanguageCode.toLowerCase(); - const availableLanguages = getAvailableLanguages(); - const exactMatch = availableLanguages.find((lang) => lang.value.toLowerCase() === normalizedCode); + const exactMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === normalizedCode); if (exactMatch) { return exactMatch.value; } - const alternativeMatch = availableLanguages.find( + 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 = availableLanguages.find((lang) => lang.value.toLowerCase() === baseCode); + const baseMatch = LANGUAGE_DEFS.find((lang) => lang.value.toLowerCase() === baseCode); if (baseMatch) { return baseMatch.value; } diff --git a/core/models/scripts/generate-languages.cjs b/core/models/scripts/generate-languages.cjs index 6983f5886..8e059c5e2 100644 --- a/core/models/scripts/generate-languages.cjs +++ b/core/models/scripts/generate-languages.cjs @@ -87,6 +87,45 @@ ${tuples} return $"🌐 {code}"; } + /// + /// Get the flag emoji for an ISO language code. Falls back to a globe flag for unknown languages. + /// + /// The ISO language code (case-insensitive). + /// The flag emoji, e.g. "🇳🇱". + public static string GetFlag(string code) + { + var iso = Normalize(code); + foreach (var (metaCode, flag, _) in Meta) + { + if (metaCode == iso) + { + return flag; + } + } + + return "🌐"; + } + + /// + /// Get the native display label for an ISO language code. Falls back to the raw code for unknown + /// languages. + /// + /// The ISO language code (case-insensitive). + /// The native label, e.g. "Nederlands". + public static string GetLabel(string code) + { + var iso = Normalize(code); + foreach (var (metaCode, _, label) in Meta) + { + if (metaCode == iso) + { + return label; + } + } + + return code; + } + /// /// 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, diff --git a/core/models/src/defaults/Languages.ts b/core/models/src/defaults/Languages.ts index 18c5d8e85..411e99dcf 100644 --- a/core/models/src/defaults/Languages.ts +++ b/core/models/src/defaults/Languages.ts @@ -23,7 +23,7 @@ export const DEFAULT_LANGUAGE_CODE = 'en'; * Covers the AliasVault app UI languages so this list can be reused beyond a single feature. */ export const LANGUAGES: ILanguageInfo[] = [ - { code: 'en', flag: '🇬🇧', label: 'English' }, + { code: 'en', flag: '🇺🇸', label: 'English' }, { code: 'nl', flag: '🇳🇱', label: 'Nederlands' }, { code: 'de', flag: '🇩🇪', label: 'Deutsch' }, { code: 'fr', flag: '🇫🇷', label: 'Français' }, @@ -33,12 +33,14 @@ export const LANGUAGES: ILanguageInfo[] = [ { code: 'fi', flag: '🇫🇮', label: 'Suomi' }, { code: 'he', flag: '🇮🇱', label: 'עברית' }, { code: 'pl', flag: '🇵🇱', label: 'Polski' }, - { code: 'pt', flag: '🇧🇷', label: 'Português' }, + { 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: 'zh', flag: '🇨🇳', label: '简体中文' }, + { code: 'ur', flag: '🇵🇰', label: 'اردو' }, + { code: 'fa', flag: '🇮🇷', label: 'فارسی' }, ]; /** diff --git a/core/typescript/identity-generator/package-lock.json b/core/typescript/identity-generator/package-lock.json index fbb88bb99..f055b9d74 100644 --- a/core/typescript/identity-generator/package-lock.json +++ b/core/typescript/identity-generator/package-lock.json @@ -141,6 +141,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -157,6 +158,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -173,6 +175,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -189,6 +192,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -205,6 +209,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -221,6 +226,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -237,6 +243,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -253,6 +260,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -269,6 +277,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -285,6 +294,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -301,6 +311,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -317,6 +328,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -333,6 +345,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -349,6 +362,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -365,6 +379,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -381,6 +396,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -397,6 +413,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -413,6 +430,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -429,6 +447,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -445,6 +464,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -461,6 +481,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -477,6 +498,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -493,6 +515,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -509,6 +532,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -525,6 +549,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2826,7 +2851,7 @@ "version": "0.25.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/core/typescript/identity-generator/src/__tests__/LanguageProvider.test.ts b/core/typescript/identity-generator/src/__tests__/LanguageProvider.test.ts index c2451fa99..67ce4305a 100644 --- a/core/typescript/identity-generator/src/__tests__/LanguageProvider.test.ts +++ b/core/typescript/identity-generator/src/__tests__/LanguageProvider.test.ts @@ -3,14 +3,15 @@ import { getAvailableLanguages, mapUiLanguageToIdentityLanguage } from '../utils describe('LanguageProvider', () => { describe('getAvailableLanguages', () => { - it('should return a list of available languages', () => { + it('should return a list of available language codes', () => { const languages = getAvailableLanguages(); expect(languages).toBeDefined(); expect(languages.length).toBeGreaterThan(0); - expect(languages[0]).toHaveProperty('value'); - expect(languages[0]).toHaveProperty('label'); - expect(languages[0]).toHaveProperty('flag'); + // 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'); }); }); diff --git a/core/typescript/identity-generator/src/utils/LanguageProvider.ts b/core/typescript/identity-generator/src/utils/LanguageProvider.ts index 9114f934b..31eff5462 100644 --- a/core/typescript/identity-generator/src/utils/LanguageProvider.ts +++ b/core/typescript/identity-generator/src/utils/LanguageProvider.ts @@ -1,108 +1,47 @@ /** - * Represents a language option for identity generation. + * 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. */ -export interface ILanguageOption { +interface ILanguageDef { /** - * The language code (e.g., "en", "nl", "de") + * The language code (e.g., "en", "nl", "de"). */ value: string; - /** - * The display label in the native language (e.g., "English", "Nederlands", "Deutsch") - */ - label: string; - - /** - * The flag emoji for the language (e.g., "🇬🇧", "🇳🇱", "🇩🇪") - */ - flag: 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" + * For example, "en-US", "en-GB", "en-CA" all map to "en". */ alternativeCodes?: string[]; } /** - * Gets all available languages for identity generation. - * Display labels are in the native language, with optional flag emoji that clients can choose to display. - * Languages are sorted alphabetically by label for consistent display across all platforms. - * @returns Array of language options sorted alphabetically by label + * 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`). */ -export function getAvailableLanguages(): ILanguageOption[] { - const languages: ILanguageOption[] = [ - { - value: 'da', - label: 'Dansk', - flag: '🇩🇰', - alternativeCodes: ['da-DK'] - }, - { - value: 'de', - label: 'Deutsch', - flag: '🇩🇪', - alternativeCodes: ['de-DE', 'de-AT', 'de-CH', 'de-LU', 'de-LI'] - }, - { - value: 'en', - label: 'English', - flag: '🇬🇧', - alternativeCodes: ['en-US', 'en-GB', 'en-CA', 'en-AU', 'en-NZ', 'en-IE', 'en-ZA', 'en-SG', 'en-IN'] - }, - { - value: 'es', - label: 'Español', - flag: '🇪🇸', - 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', - label: 'Français', - flag: '🇫🇷', - alternativeCodes: ['fr-FR', 'fr-CA', 'fr-BE', 'fr-CH', 'fr-LU', 'fr-MC'] - }, - { - value: 'it', - label: 'Italiano', - flag: '🇮🇹', - alternativeCodes: ['it-IT', 'it-CH', 'it-SM', 'it-VA'] - }, - { - value: 'nl', - label: 'Nederlands', - flag: '🇳🇱', - alternativeCodes: ['nl-NL', 'nl-BE'] - }, - { - value: 'ro', - label: 'Română', - flag: '🇷🇴', - alternativeCodes: ['ro-RO', 'ro-MD'] - }, - { - value: 'sv', - label: 'Svenska', - flag: '🇸🇪', - alternativeCodes: ['sv-SE', 'sv-FI'] - }, - { - value: 'ur', - label: 'اردو', - flag: '🇵🇰', - alternativeCodes: ['ur-PK', 'ur-IN'] - }, - { - value: 'fa', - label: 'فارسی', - flag: '🇮🇷', - alternativeCodes: ['fa-IR', 'fa-AF'] - } - ]; +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'] }, +]; - // Sort alphabetically by label using locale-aware comparison - return languages.sort((a, b) => a.label.localeCompare(b.label)); +/** + * Gets all available language codes for identity generation. + * @returns Array of ISO language codes (e.g. ["da", "de", "en", ...]). + */ +export function getAvailableLanguages(): string[] { + return LANGUAGE_DEFS.map(lang => lang.value); } /** @@ -116,7 +55,7 @@ export function getAvailableLanguages(): ILanguageOption[] { * mapUiLanguageToIdentityLanguage("en-US") // returns "en" * mapUiLanguageToIdentityLanguage("nl") // returns "nl" * mapUiLanguageToIdentityLanguage("de-CH") // returns "de" - * mapUiLanguageToIdentityLanguage("fr") // returns null (no French identity generator) + * mapUiLanguageToIdentityLanguage("ja") // returns null (no Japanese identity generator) */ export function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null { if (!uiLanguageCode) { @@ -124,16 +63,15 @@ export function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | } const normalizedCode = uiLanguageCode.toLowerCase(); - const availableLanguages = getAvailableLanguages(); // First, try exact match with the primary value - const exactMatch = availableLanguages.find(lang => lang.value.toLowerCase() === normalizedCode); + const exactMatch = LANGUAGE_DEFS.find(lang => lang.value.toLowerCase() === normalizedCode); if (exactMatch) { return exactMatch.value; } // Then, try matching with alternative codes - const alternativeMatch = availableLanguages.find(lang => + const alternativeMatch = LANGUAGE_DEFS.find(lang => lang.alternativeCodes?.some(code => code.toLowerCase() === normalizedCode) ); if (alternativeMatch) { @@ -142,7 +80,7 @@ export function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | // Finally, try matching the base language code (e.g., "en" from "en-US") const baseCode = normalizedCode.split('-')[0]; - const baseMatch = availableLanguages.find(lang => lang.value.toLowerCase() === baseCode); + const baseMatch = LANGUAGE_DEFS.find(lang => lang.value.toLowerCase() === baseCode); if (baseMatch) { return baseMatch.value; }