diff --git a/apps/web/modules/auth/lib/utils.test.ts b/apps/web/modules/auth/lib/utils.test.ts index 2aadd12107..18f7c40323 100644 --- a/apps/web/modules/auth/lib/utils.test.ts +++ b/apps/web/modules/auth/lib/utils.test.ts @@ -119,8 +119,9 @@ describe("Auth Utils", () => { }); test("should generate different hashes for same password", async () => { - const hash1 = await hashPassword(password); - const hash2 = await hashPassword(password); + // Hash twice in parallel so the test doesn't incur two full bcrypt rounds sequentially. + // Running them concurrently keeps the assertion meaningful while avoiding unnecessary timeouts. + const [hash1, hash2] = await Promise.all([hashPassword(password), hashPassword(password)]); expect(hash1).not.toBe(hash2); expect(await verifyPassword(password, hash1)).toBe(true); diff --git a/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx b/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx index 91d176232f..0ed62e632c 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx @@ -62,7 +62,10 @@ const validateLanguages = (languages: Language[], t: TFunction) => { return false; } - // Check if the chosen alias matches an ISO identifier of a language that hasn't been added + // Prevent choosing an alias that clashes with the ISO code of some other + // language. Without this guard users could create ambiguous language entries + // (e.g. alias "nl" pointing to a non-Dutch language) which later breaks the + // dropdowns that rely on ISO identifiers. for (const alias of languageAliases) { if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) { toast.error(t("environments.project.languages.conflict_between_selected_alias_and_another_language"), { diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx index 082fa9d79b..e66c431859 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx @@ -43,8 +43,10 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }: setIsOpen(false); }; - const getLabelForLocale = (item: TIso639Language) => - item.label[locale] ?? item.label["en-US"]; + // Most ISO entries don't ship with every locale translation, so fall back to + // English to keep the dropdown readable for locales such as Dutch that were + // added recently. + const getLabelForLocale = (item: TIso639Language) => item.label[locale] ?? item.label["en-US"]; const filteredItems = items.filter((item) => getLabelForLocale(item).toLowerCase().includes(searchTerm.toLowerCase()) diff --git a/packages/i18n-utils/src/utils.test.ts b/packages/i18n-utils/src/utils.test.ts new file mode 100644 index 0000000000..5ce38864e3 --- /dev/null +++ b/packages/i18n-utils/src/utils.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "vitest"; +import { getLanguageLabel } from "./utils"; + +describe("getLanguageLabel", () => { + test("returns locale specific label when available", () => { + expect(getLanguageLabel("de", "de-DE")).toBe("Deutsch"); + }); + + test("falls back to English when locale specific label is missing", () => { + // Language "aa" (Afar) does not currently ship with a Dutch translation. + expect(getLanguageLabel("aa", "nl-NL")).toBe("Afar"); + }); + + test("returns undefined for unknown language codes", () => { + expect(getLanguageLabel("zz", "en-US")).toBeUndefined(); + }); +}); diff --git a/packages/i18n-utils/src/utils.ts b/packages/i18n-utils/src/utils.ts index 9d15939a40..236f216141 100644 --- a/packages/i18n-utils/src/utils.ts +++ b/packages/i18n-utils/src/utils.ts @@ -2613,6 +2613,16 @@ export const iso639Languages: TIso639Language[] = [ export const getLanguageLabel = (languageCode: string, locale: string): string | undefined => { const language = iso639Languages.find((lang) => lang.alpha2 === languageCode); - // Type assertion to tell TypeScript that we know the structure of label - return language?.label[locale as keyof typeof language.label]; + if (!language) { + return undefined; + } + + // Try to read the label for the requested locale. Not every ISO-639 entry + // has translations for every UI locale (for example Dutch strings were added + // later), so we gracefully fall back to English when a localized label is + // missing. Consumers expect a non-empty label in dropdowns and status chips, + // so returning "en-US" keeps the UI readable instead of rendering nothing. + const localizedLabel = language.label[locale as keyof typeof language.label]; + + return localizedLabel ?? language.label["en-US"]; };