mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +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
@@ -25,7 +25,17 @@ 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", "nl"];
|
||||
const LOCALES: Locale[] = [
|
||||
"de",
|
||||
"en",
|
||||
"fr",
|
||||
"es",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
];
|
||||
|
||||
/** The rules a voice section must state; `[]` when it states them all. */
|
||||
function missingVoiceRules(prompt: string, lang: "de" | "en"): string[] {
|
||||
|
||||
@@ -42,7 +42,17 @@ 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", "nl"] as const;
|
||||
const LOCALES = [
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] as const;
|
||||
|
||||
function bundle(locale: string): Record<string, unknown> {
|
||||
return JSON.parse(readFileSync(join(MESSAGES, `${locale}.json`), "utf8"));
|
||||
|
||||
@@ -166,7 +166,7 @@ describe("dynamic-key exhaustiveness (enum-derived)", () => {
|
||||
expect(LOCALES.length).toBeGreaterThanOrEqual(6);
|
||||
});
|
||||
|
||||
it("discovers all eight shipped locales", () => {
|
||||
it("discovers all nine shipped locales", () => {
|
||||
expect(LOCALES.map((l) => l.locale).sort()).toEqual([
|
||||
"de",
|
||||
"en",
|
||||
@@ -176,6 +176,7 @@ describe("dynamic-key exhaustiveness (enum-derived)", () => {
|
||||
"ko",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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", "nl"] as const;
|
||||
const LEAK_LOCALES = ["es", "fr", "it", "pl", "ko", "nl", "pt-BR"] as const;
|
||||
|
||||
function flattenValues(
|
||||
obj: unknown,
|
||||
|
||||
@@ -20,7 +20,17 @@ 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", "nl"] as const;
|
||||
const LOCALES = [
|
||||
"en",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] as const;
|
||||
const INSIGHTS_PAGES_DIR = join(process.cwd(), "src/app/insights");
|
||||
|
||||
/**
|
||||
|
||||
@@ -93,7 +93,17 @@ 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", "nl"] as const;
|
||||
const LOCALES = [
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] 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. */
|
||||
|
||||
@@ -26,7 +26,17 @@ import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
const ROOT = path.resolve(__dirname, "../..");
|
||||
|
||||
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko", "nl"] as const;
|
||||
const LOCALES = [
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] as const;
|
||||
|
||||
/** Every path the retired bridge occupied. */
|
||||
const REMOVED_PATHS = [
|
||||
|
||||
@@ -50,8 +50,8 @@ describe("managed record settings UI", () => {
|
||||
fromRoot("src/components/settings/record-settings-section-gate.tsx"),
|
||||
"utf8",
|
||||
),
|
||||
...["de", "en", "es", "fr", "it", "pl", "ko", "nl"].map((locale) =>
|
||||
readFile(fromRoot(`messages/${locale}.json`), "utf8"),
|
||||
...["de", "en", "es", "fr", "it", "pl", "ko", "nl", "pt-BR"].map(
|
||||
(locale) => readFile(fromRoot(`messages/${locale}.json`), "utf8"),
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -38,7 +38,17 @@ import {
|
||||
* it — which is the part that rots silently.
|
||||
*/
|
||||
const ROOT = process.cwd();
|
||||
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko", "nl"] as const;
|
||||
const LOCALES = [
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] as const;
|
||||
|
||||
/** The published description of the level, off the Zod schema itself. */
|
||||
function accessDescription(): string {
|
||||
|
||||
@@ -4,7 +4,17 @@ import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const ROOT = join(__dirname, "../..");
|
||||
const LOCALES = ["en", "de", "es", "fr", "it", "pl", "ko", "nl"] as const;
|
||||
const LOCALES = [
|
||||
"en",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] as const;
|
||||
|
||||
const REQUIRED_KEYS = [
|
||||
// Security: adding a passkey now requires an existing sign-in factor.
|
||||
|
||||
Reference in new issue
Block a user