feat: add i18n utils tests and update multi-language survey components

This commit is contained in:
Thomas Brugman
2025-10-24 13:29:19 +02:00
parent 94531953da
commit 478186c79c
5 changed files with 40 additions and 7 deletions

View File

@@ -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);

View File

@@ -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"), {

View File

@@ -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())

View File

@@ -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();
});
});

View File

@@ -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"];
};