feat(i18n): register Brazilian Portuguese as a locale

Add pt-BR to the locale list and everything that hangs off it: the
client loader, Intl and date-fns mapping, the reply-language footers of
the Coach and insights prompts (English body, pt-BR footer, the same
path Korean and Dutch take), the citation-coverage normative words, the
no-key fallback copy, the AUDIT-C standard-drink line, country labels,
period search, CSV yes/no cells and the Telegram bare-word aliases.

The locale enums in the extract, managed-profile and record-settings
schemas now derive from the locale list instead of repeating it, and
the five notification and login-alert routes share one isLocale guard
instead of hand-rolled chains, so a later locale is one edit there.
A language tag is matched exactly and case-insensitively before its
primary subtag, so pt-BR and pt-PT browsers land on the Portuguese
bundle rather than English, and AUDIT-C no longer cuts a region tag to
two letters.

PHQ-9, GAD-7 and WHO-5 are not marked validated for pt-BR: the wording
has not been read against the published Brazilian forms, so readers get
the English items plus the existing note.
This commit is contained in:
Marc-André Bombeck committed 2026-10-22 14:50:00 +02:00
1 parent 1619b2406a
commit 764794496c
72 files changed
+425 -159

No files matched your search

@@ -254,6 +254,7 @@ describe("normative-claim detection across the shipped locales", () => {
pl: "Ciśnienie skurczowe powinno pozostać poniżej celu 130 mmHg.",
nl: "Je bovendruk zou onder de streefwaarde van 130 mmHg moeten blijven.",
ko: "수축기 혈압은 목표인 130 mmHg 아래로 유지해야 해요.",
"pt-BR": "Sua sistólica deveria ficar abaixo da meta de 130 mmHg.",
};
/**
@@ -270,6 +271,8 @@ describe("normative-claim detection across the shipped locales", () => {
pl: "Twoja średnia z 7 dni przekracza medianę z 90 dni o 4 mmHg.",
nl: "Je 7-daagse gemiddelde ligt 4 mmHg hoger dan je 90-daagse mediaan.",
ko: "7일 평균이 90일 중앙값보다 4 mmHg 더 커요.",
"pt-BR":
"Sua média de 7 dias está 4 mmHg mais alta que a sua mediana de 90 dias.",
};
it.each(locales)("carries a non-empty keyword bank for %s", (locale) => {
+16
View File
@@ -162,6 +162,22 @@ const NORMATIVE_KEYWORDS: Record<Locale, readonly string[]> = {
"초과",
"미만",
],
// "meta" alone would also open "metabolismo" and "metade", so the goal noun
// is spelled with its preposition.
"pt-BR": [
"meta de",
"metas de",
"alvo",
"objetivo",
"deveria",
"deve ser",
"devem ser",
"faixa normal",
"acima d",
"abaixo d",
"superior a",
"inferior a",
],
};
/**
@@ -24,7 +24,17 @@ import type { Locale } from "@/lib/i18n/config";
* translated) and only ever appears inside the dose-safety contract.
*/
const ALL_LOCALES: Locale[] = ["de", "en", "fr", "es", "it", "pl", "ko", "nl"];
const ALL_LOCALES: Locale[] = [
"de",
"en",
"fr",
"es",
"it",
"pl",
"ko",
"nl",
"pt-BR",
];
describe("resolved Coach system prompt — grounding + GLP-1 per locale", () => {
for (const locale of ALL_LOCALES) {
+4 -2
View File
@@ -1258,6 +1258,8 @@ const LOCALE_REPLY_FOOTER_FALLBACK: Record<
pl: "\n\nREPLY LANGUAGE: respond in Polish. Mirror the user's register (formal Pan/Pani for medical-adjacent topics); use natural Polish health vocabulary.",
nl: "\n\nREPLY LANGUAGE: respond in Dutch. Use the informal je/jij register throughout (never formal u); use natural Dutch health vocabulary.",
ko: "\n\nREPLY LANGUAGE: respond in Korean. Use the 해요체 register throughout (never 합쇼체 or plain 한다체); use natural Korean health vocabulary.",
"pt-BR":
"\n\nREPLY LANGUAGE: respond in Brazilian Portuguese (pt-BR, never European Portuguese). Address the user as você throughout; use natural Brazilian health vocabulary.",
};
/**
@@ -1295,8 +1297,8 @@ export function getCoachSystemPrompt(
base = COACH_PROMPT_DE;
} else if (locale === "en") {
base = COACH_PROMPT_EN;
} else if (locale === "ko" || locale === "nl") {
// ko and nl ship no safety-contract matrix, so they are not a
} else if (locale === "ko" || locale === "nl" || locale === "pt-BR") {
// ko, nl and pt-BR ship no safety-contract matrix, so they are not a
// NativeCoachLocale.
base = COACH_PROMPT_EN + LOCALE_REPLY_FOOTER_FALLBACK[locale];
} else {
@@ -57,6 +57,7 @@ const DRUG_LEVEL_CONCEPT_TOKENS: Record<Locale, readonly string[]> = {
it: ["livello", "concentrazione", "picco", "valle", "Cmax", "terapeutica"],
pl: ["poziom", "stężeni", "szczyt", "minimum", "Cmax", "terapeutyczn"],
nl: ["spiegel", "concentratie", "piek", "dal", "Cmax", "therapeutisch"],
"pt-BR": ["nível", "concentração", "pico", "vale", "Cmax", "terapêutic"],
ko: ["농도", "혈중", "최고", "최저", "Cmax", "치료"],
};
@@ -41,6 +41,7 @@ const EXPECTED_LANGUAGE_NAME: Record<Locale, string> = {
pl: "Polish",
nl: "Dutch",
ko: "Korean",
"pt-BR": "Brazilian Portuguese",
};
/** Which body each locale is expected to compose. */
@@ -53,6 +54,7 @@ const EXPECTED_BODY: Record<Locale, "de" | "en"> = {
pl: "en",
nl: "en",
ko: "en",
"pt-BR": "en",
};
describe("output-language helper", () => {
+4 -2
View File
@@ -994,6 +994,8 @@ const INSIGHTS_LOCALE_REPLY_FOOTER_FALLBACK: Record<
pl: "\n\nREPLY LANGUAGE: render all user-facing strings in Polish. Use natural Polish health vocabulary with formal Pan/Pani register for medical-adjacent topics. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
nl: "\n\nREPLY LANGUAGE: render all user-facing strings in Dutch, in the informal je/jij register throughout (never formal u). Use natural Dutch health vocabulary. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
ko: "\n\nREPLY LANGUAGE: render all user-facing strings in Korean, in the 해요체 register throughout (never 합쇼체 or plain 한다체). Use natural Korean health vocabulary. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
"pt-BR":
"\n\nREPLY LANGUAGE: render all user-facing strings in Brazilian Portuguese (pt-BR, never European Portuguese), addressing the user as você throughout. Use natural Brazilian health vocabulary. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
};
/**
@@ -1003,9 +1005,9 @@ const INSIGHTS_LOCALE_REPLY_FOOTER_FALLBACK: Record<
export function getStrictInsightsSystemPrompt(locale: Locale): string {
if (locale === "de") return SYSTEM_PROMPT_DE;
if (locale === "en") return SYSTEM_PROMPT_EN;
// ko and nl ship no safety-contract matrix, so they are not a
// ko, nl and pt-BR ship no safety-contract matrix, so they are not a
// NativeCoachLocale.
if (locale === "ko" || locale === "nl") {
if (locale === "ko" || locale === "nl" || locale === "pt-BR") {
return SYSTEM_PROMPT_EN + INSIGHTS_LOCALE_REPLY_FOOTER_FALLBACK[locale];
}
try {
+2
View File
@@ -58,6 +58,8 @@ export function targetLanguageName(locale: Locale): string {
return "Korean";
case "nl":
return "Dutch";
case "pt-BR":
return "Brazilian Portuguese";
default:
return "English";
}