mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
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:
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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user