Merge pull request #1246 from robdegraaf/feat/i18n-nl

This commit is contained in:
Marc-André Bombeck committed 2026-10-20 00:14:49 +02:00
commit 7be3714ac2
83 files changed
+13832 -74

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ import { gradeVoice } from "@/lib/ai/coach/eval/grade-voice";
import { DEFAULT_COACH_PREFS } from "@/lib/validations/coach-prefs";
import type { Locale } from "@/lib/i18n/config";
const LOCALES: Locale[] = ["de", "en", "fr", "es", "it", "pl", "ko"];
const LOCALES: Locale[] = ["de", "en", "fr", "es", "it", "pl", "ko", "nl"];
/** The rules a voice section must state; `[]` when it states them all. */
function missingVoiceRules(prompt: string, lang: "de" | "en"): string[] {
@@ -42,7 +42,7 @@ import { describe, expect, it } from "vitest";
import { consentNeededForProviders } from "@/lib/ai/capabilities/resolve";
const MESSAGES = join(__dirname, "../../messages");
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const;
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko", "nl"] as const;
function bundle(locale: string): Record<string, unknown> {
return JSON.parse(readFileSync(join(MESSAGES, `${locale}.json`), "utf8"));
@@ -81,6 +81,7 @@ const FINALITY_PHRASES: Record<string, readonly string[]> = {
],
pl: ["nie można cofnąć", "nie można tego cofnąć", "trwale", "nieodwracaln"],
ko: ["되돌릴 수 없", "복구할 수 없", "영구", "완전히 삭제"],
nl: ["niet ongedaan", "definitief", "permanent", "onherroepelijk"],
};
/** Per-locale phrase that promises the short undo window. */
@@ -92,6 +93,7 @@ const UNDO_PHRASES: Record<string, string> = {
it: "annullare",
pl: "cofnąć",
ko: "되돌릴 수 있",
nl: "ongedaan maken",
};
/** Confirmation bodies whose surface renders an Undo affordance. */
@@ -176,6 +178,7 @@ describe("sleep debt is labelled as a balance, not a summed deficit", () => {
it: ["cumulato", "cumulata", "accumulato", "deficit"],
pl: ["skumulowan", "niedobór", "niedoboru"],
ko: ["누적", "부족분", "적자", "쌓인"],
nl: ["cumulatief", "opgeteld", "tekort", "opgebouwd"],
};
for (const locale of LOCALES) {
@@ -302,6 +305,17 @@ describe("the mood prognosis never states a cause", () => {
],
pl: ["ponieważ", "powoduje", "z powodu", "wywołuje", "poprawia twój"],
ko: ["때문", "탓", "원인", "유발", "초래", "야기", "덕분", "인해"],
nl: [
"omdat",
"veroorzaakt",
"leidt tot",
"zorgt voor",
"vanwege",
"verbetert je",
"verslechtert je",
"verlaagt je",
"verhoogt je",
],
};
/** Every leaf string under `insights.mood.prognosis`, flattened. */
@@ -401,6 +415,11 @@ describe("the MCP token copy never claims the mint is read-only", () => {
it: ["token in sola lettura"],
pl: ["token tylko do odczytu"],
ko: ["읽기 전용 토큰", "읽기전용 토큰"],
nl: [
"alleen-lezen token",
"alleen-lezentoken",
"token met alleen leesrechten",
],
};
for (const locale of LOCALES) {
@@ -450,6 +469,7 @@ describe("AI consent withdrawal copy matches what withdrawing does", () => {
it: ["tua chiave", "modello locale"],
pl: ["własnym kluczu", "lokalnym modelu"],
ko: ["자신의 키", "로컬 모델"],
nl: ["eigen sleutel", "lokaal model"],
};
for (const locale of LOCALES) {
@@ -166,7 +166,7 @@ describe("dynamic-key exhaustiveness (enum-derived)", () => {
expect(LOCALES.length).toBeGreaterThanOrEqual(6);
});
it("discovers all seven shipped locales", () => {
it("discovers all eight shipped locales", () => {
expect(LOCALES.map((l) => l.locale).sort()).toEqual([
"de",
"en",
@@ -174,6 +174,7 @@ describe("dynamic-key exhaustiveness (enum-derived)", () => {
"fr",
"it",
"ko",
"nl",
"pl",
]);
});
+11 -1
View File
@@ -33,7 +33,7 @@ const EN_PATH = join(MESSAGES_DIR, "en.json");
// intentionally excluded — it carries accepted anglicisms (e.g. "Health
// Score") that would be noise here, and is covered by its own pinned checks in
// `i18n-locale-integrity`.
const LEAK_LOCALES = ["es", "fr", "it", "pl", "ko"] as const;
const LEAK_LOCALES = ["es", "fr", "it", "pl", "ko", "nl"] as const;
function flattenValues(
obj: unknown,
@@ -209,6 +209,16 @@ const LEGIT_IDENTICAL = new Set<string>([
"settings.nightscoutUrl",
"admin.aiServerKey.baseUrlLabel",
"admin.webPushVapidSubject",
// Dutch spells these exactly as English does: "In bed" and "capsule(s)" are
// ordinary Dutch, and the clinical index names and the fatty-acid
// abbreviations are written in their international form.
"mood.dayContext.linkedInBed",
"insights.sleep.stages.inBed",
"medications.wizard.steps.step3.unit.capsules",
"doctorReport.glucoseClinical.gmi",
"insights.bloodGlucose.clinical.advanced.jIndex.label",
"nutrients.names.dha",
"nutrients.names.epa",
]);
describe("i18n English-leak guard", () => {
@@ -20,7 +20,7 @@ import { join } from "node:path";
* the key is a prop, not a call argument. Both halves are asserted here.
*/
const LOCALES = ["en", "de", "es", "fr", "it", "pl", "ko"] as const;
const LOCALES = ["en", "de", "es", "fr", "it", "pl", "ko", "nl"] as const;
const INSIGHTS_PAGES_DIR = join(process.cwd(), "src/app/insights");
/**
@@ -93,7 +93,7 @@ const NOT_IN_THE_FEED = [
/** What an action with no sentence of its own renders as. */
const FALLBACK_KEY = "recordSharing.activity.acted";
/** The six bundles the product ships. */
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const;
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko", "nl"] as const;
/** Every `route.ts` under `src/app/api`, walked rather than globbed: a glob's
* `*` skips a leading dot and this tree has dot-directories. */
+1 -1
View File
@@ -25,7 +25,7 @@ import { INTEGRATION_DISPLAY_NAMES } from "@/components/settings/integrations/in
const ROOT = path.resolve(__dirname, "../..");
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const;
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko", "nl"] as const;
/** Every path the retired bridge occupied. */
const REMOVED_PATHS = [
+1 -1
View File
@@ -50,7 +50,7 @@ describe("managed record settings UI", () => {
fromRoot("src/components/settings/record-settings-section-gate.tsx"),
"utf8",
),
...["de", "en", "es", "fr", "it", "pl", "ko"].map((locale) =>
...["de", "en", "es", "fr", "it", "pl", "ko", "nl"].map((locale) =>
readFile(fromRoot(`messages/${locale}.json`), "utf8"),
),
]);
+1 -1
View File
@@ -38,7 +38,7 @@ import {
* it — which is the part that rots silently.
*/
const ROOT = process.cwd();
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const;
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko", "nl"] as const;
/** The published description of the level, off the Zod schema itself. */
function accessDescription(): string {
+1 -1
View File
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
const ROOT = join(__dirname, "../..");
const LOCALES = ["en", "de", "es", "fr", "it", "pl", "ko"] as const;
const LOCALES = ["en", "de", "es", "fr", "it", "pl", "ko", "nl"] as const;
const REQUIRED_KEYS = [
// Security: adding a passkey now requires an existing sign-in factor.