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

+1 -14
View File
@@ -23,7 +23,7 @@ import type {
ChannelType,
NotificationPayload,
} from "@/lib/notifications/types";
import { defaultLocale, type Locale } from "@/lib/i18n/config";
import { defaultLocale, isLocale, type Locale } from "@/lib/i18n/config";
import { getServerTranslator } from "@/lib/i18n/server-translator";
export const dynamic = "force-dynamic";
@@ -111,19 +111,6 @@ function failedSend(
};
}
function isLocale(value: string | null | undefined): value is Locale {
return (
value === "de" ||
value === "en" ||
value === "fr" ||
value === "es" ||
value === "it" ||
value === "pl" ||
value === "ko" ||
value === "nl"
);
}
export const POST = apiHandler(async () => {
const { user } = await requireAdmin();
annotate({ action: { name: "admin.notifications.test" } });
+2 -1
View File
@@ -91,6 +91,7 @@ import {
medicationExtractionSchema,
type MedicationExtractionResult,
} from "@/lib/ai/coach/medication-extract-prompt";
import { locales } from "@/lib/i18n/config";
/**
* How the extractor refuses with no provider. 503 predates the capability
@@ -118,7 +119,7 @@ const requestSchema = z
.object({
text: z.string().min(1).max(MAX_TEXT_LENGTH),
/** Optional UI locale; informational hint for the model. */
locale: z.enum(["en", "de", "es", "fr", "it", "pl", "ko", "nl"]).optional(),
locale: z.enum(locales).optional(),
/**
* Optional override of the reference date used to resolve relative
* phrases ("tomorrow", "next Monday"). Defaults to the server's
+1 -14
View File
@@ -19,25 +19,12 @@ import { apiSuccess, apiError } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { sendViaApns } from "@/lib/notifications/senders/apns";
import { defaultLocale, type Locale } from "@/lib/i18n/config";
import { defaultLocale, isLocale, type Locale } from "@/lib/i18n/config";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import { prisma } from "@/lib/db";
export const dynamic = "force-dynamic";
function isLocale(value: string | null | undefined): value is Locale {
return (
value === "de" ||
value === "en" ||
value === "fr" ||
value === "es" ||
value === "it" ||
value === "pl" ||
value === "ko" ||
value === "nl"
);
}
export const POST = apiHandler(async (request: NextRequest) => {
void request;
const { user } = await requireAuth();
+1 -14
View File
@@ -5,22 +5,9 @@ import { sendTelegramMessage } from "@/lib/telegram";
import { checkRateLimit } from "@/lib/rate-limit";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { defaultLocale, type Locale } from "@/lib/i18n/config";
import { defaultLocale, isLocale, type Locale } from "@/lib/i18n/config";
import { getServerTranslator } from "@/lib/i18n/server-translator";
function isLocale(value: string | null | undefined): value is Locale {
return (
value === "de" ||
value === "en" ||
value === "fr" ||
value === "es" ||
value === "it" ||
value === "pl" ||
value === "ko" ||
value === "nl"
);
}
/**
* Send a test Telegram message to verify the bot token and chat ID.
*/