mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +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
@@ -218,7 +218,17 @@ describe("dashboard.*Short keys exist across all six locales", () => {
|
||||
|
||||
// Resolved at test time so the assertion runs against the current
|
||||
// bundle, not a stale cached snapshot.
|
||||
const locales = ["en", "de", "fr", "es", "it", "pl", "ko", "nl"] as const;
|
||||
const locales = [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"es",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] as const;
|
||||
|
||||
it.each(locales)("%s carries every dashboard.*Short key", async (locale) => {
|
||||
const messages = (await import(`../../../../messages/${locale}.json`))
|
||||
|
||||
@@ -31,7 +31,7 @@ vi.mock("@/components/ui/responsive-sheet", () => ({
|
||||
import { KeyboardShortcutsDialog } from "../keyboard-shortcuts-dialog";
|
||||
import { primeLocales } from "@/__tests__/helpers/prime-locales";
|
||||
|
||||
primeLocales("de", "es", "fr", "it", "pl", "ko", "nl");
|
||||
primeLocales("de", "es", "fr", "it", "pl", "ko", "nl", "pt-BR");
|
||||
|
||||
const EVERYTHING = {
|
||||
navHrefs: ["/", "/medications", "/labs", "/timeline", "/insights", "/coach"],
|
||||
@@ -106,7 +106,7 @@ describe("<KeyboardShortcutsDialog>", () => {
|
||||
expect(kbd).not.toMatch(/#[0-9a-f]{3,6}|dracula|\[/i);
|
||||
});
|
||||
|
||||
it.each(["de", "es", "fr", "it", "pl", "ko", "nl"])(
|
||||
it.each(["de", "es", "fr", "it", "pl", "ko", "nl", "pt-BR"])(
|
||||
"is translated in %s",
|
||||
(locale) => {
|
||||
const html = render({}, locale);
|
||||
|
||||
@@ -75,6 +75,7 @@ describe("<OfflineBanner>", () => {
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
] as const) {
|
||||
const m = loadMessages(locale);
|
||||
expect(m.offlineBanner.message.length, locale).toBeGreaterThan(0);
|
||||
|
||||
@@ -8,7 +8,17 @@ import { describe, expect, it } from "vitest";
|
||||
import { formatPeriod, parsePeriodQuery } from "../period-query";
|
||||
|
||||
const NOW = new Date("2026-10-15T12:00:00.000Z");
|
||||
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 window(term: string, locale: string) {
|
||||
const parsed = parsePeriodQuery(term, locale, NOW);
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface PeriodQuery {
|
||||
const CONNECTORS: Record<string, readonly string[]> = {
|
||||
es: ["de", "del"],
|
||||
it: ["del"],
|
||||
pt: ["de", "do"],
|
||||
};
|
||||
|
||||
const nameCache = new Map<string, ReadonlyArray<[string, number]>>();
|
||||
|
||||
@@ -41,7 +41,7 @@ const REQUIRED_KEYS = [
|
||||
];
|
||||
|
||||
describe("measurement-reminder + bulk-share dynamic i18n keys", () => {
|
||||
it("discovers all eight shipped locales", () => {
|
||||
it("discovers all nine shipped locales", () => {
|
||||
expect(LOCALES.map((l) => l.locale).sort()).toEqual([
|
||||
"de",
|
||||
"en",
|
||||
@@ -51,6 +51,7 @@ describe("measurement-reminder + bulk-share dynamic i18n keys", () => {
|
||||
"ko",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("<ImportPanel> — SSR smoke", () => {
|
||||
});
|
||||
|
||||
describe("<AppleHealthEstimateWarning>", () => {
|
||||
it.each(["en", "de", "es", "fr", "it", "pl", "ko", "nl"] as const)(
|
||||
it.each(["en", "de", "es", "fr", "it", "pl", "ko", "nl", "pt-BR"] as const)(
|
||||
"renders localized estimate disclosure for %s",
|
||||
(locale) => {
|
||||
const html = render(<AppleHealthEstimateWarning days={3} />, locale);
|
||||
|
||||
@@ -10,7 +10,17 @@ import {
|
||||
ManagedRecordSettingsForm,
|
||||
} from "../managed-record-settings-section";
|
||||
|
||||
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;
|
||||
|
||||
const render = (
|
||||
family: Parameters<typeof ManagedRecordSettingsForm>[0]["family"],
|
||||
|
||||
@@ -215,7 +215,7 @@ describe("<AppleHealthCard>", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["en", "de", "es", "fr", "it", "pl", "ko", "nl"] as const)(
|
||||
it.each(["en", "de", "es", "fr", "it", "pl", "ko", "nl", "pt-BR"] as const)(
|
||||
"renders localized Apple Health copy for %s without leaking keys",
|
||||
(locale) => {
|
||||
statusPayload = { lastSyncedAt: null, health: null };
|
||||
|
||||
@@ -15,6 +15,7 @@ import itMessages from "../../../../../messages/it.json";
|
||||
import ko from "../../../../../messages/ko.json";
|
||||
import nl from "../../../../../messages/nl.json";
|
||||
import pl from "../../../../../messages/pl.json";
|
||||
import ptBR from "../../../../../messages/pt-BR.json";
|
||||
|
||||
import {
|
||||
diagnosticsText,
|
||||
@@ -34,6 +35,7 @@ const CATALOGS: Record<Locale, unknown> = {
|
||||
pl,
|
||||
nl,
|
||||
ko,
|
||||
"pt-BR": ptBR,
|
||||
};
|
||||
|
||||
function translator(locale: Locale) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import { SettingsCardActions } from "./_card-actions";
|
||||
import { SettingsCardHeader } from "./_card-header";
|
||||
import { SettingsCard } from "./settings-card";
|
||||
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
||||
import { locales } from "@/lib/i18n/config";
|
||||
|
||||
interface ManagedRecordSettingsResponse {
|
||||
recordId: string;
|
||||
@@ -411,7 +412,7 @@ function ProfileSettingsForm({
|
||||
<option value="">
|
||||
{t("settings.sharedRecord.managedSettings.profile.systemDefault")}
|
||||
</option>
|
||||
{["de", "en", "es", "fr", "it", "pl", "ko", "nl"].map((locale) => (
|
||||
{locales.map((locale) => (
|
||||
<option key={locale} value={locale}>
|
||||
{locale}
|
||||
</option>
|
||||
|
||||
@@ -34,7 +34,17 @@ import { TimelineLegend } from "../timeline-view";
|
||||
import { useSeriesValueFormat } from "../use-series-value-format";
|
||||
import { item, wordsIn } from "./timeline-fixture";
|
||||
|
||||
const LOCALES: Locale[] = ["de", "en", "es", "fr", "it", "pl", "ko", "nl"];
|
||||
const LOCALES: Locale[] = [
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"pl",
|
||||
"ko",
|
||||
"nl",
|
||||
"pt-BR",
|
||||
];
|
||||
|
||||
function render(node: React.ReactElement, locale: Locale = "en") {
|
||||
return renderToStaticMarkup(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { de, enUS, es, fr, it, ko, nl, pl } from "date-fns/locale";
|
||||
import { de, enUS, es, fr, it, ko, nl, pl, ptBR } from "date-fns/locale";
|
||||
import type { Locale as DateFnsLocale } from "date-fns";
|
||||
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
@@ -25,6 +25,8 @@ export function resolveDateFnsLocale(locale: Locale): DateFnsLocale {
|
||||
return ko;
|
||||
case "nl":
|
||||
return nl;
|
||||
case "pt-BR":
|
||||
return ptBR;
|
||||
default:
|
||||
return enUS;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user