diff --git a/messages/de.json b/messages/de.json index 905b13f71..2baa2f32e 100644 --- a/messages/de.json +++ b/messages/de.json @@ -7173,7 +7173,11 @@ "failed": "Das hat nicht geklappt. Prüfe deine Eingabe und versuche es noch einmal." }, "passwordTitle": "Passwort", - "tokenNameLabel": "Token-Name" + "tokenNameLabel": "Token-Name", + "displayCard": { + "title": "Darstellung", + "description": "Sprache, Einheiten und Formate, gespeichert, sobald du sie änderst." + } }, "admin": { "title": "Administration", diff --git a/messages/en.json b/messages/en.json index 158470039..3f2d82afa 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7173,7 +7173,11 @@ "failed": "That did not work. Check what you entered and try again." }, "passwordTitle": "Password", - "tokenNameLabel": "Token name" + "tokenNameLabel": "Token name", + "displayCard": { + "title": "Display", + "description": "Language, units and formats, saved as soon as you change them." + } }, "admin": { "title": "Administration", diff --git a/messages/es.json b/messages/es.json index c1041cb78..be3c7baf3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -7173,7 +7173,11 @@ "failed": "No ha funcionado. Revisa lo que has introducido e inténtalo de nuevo." }, "passwordTitle": "Contraseña", - "tokenNameLabel": "Nombre del token" + "tokenNameLabel": "Nombre del token", + "displayCard": { + "title": "Visualización", + "description": "Idioma, unidades y formatos; se guardan en cuanto los cambias." + } }, "admin": { "title": "Administration", diff --git a/messages/fr.json b/messages/fr.json index c9d7eb79f..b6cf1a77b 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -7173,7 +7173,11 @@ "failed": "Cela n'a pas fonctionné. Vérifiez votre saisie et réessayez." }, "passwordTitle": "Mot de passe", - "tokenNameLabel": "Nom du jeton" + "tokenNameLabel": "Nom du jeton", + "displayCard": { + "title": "Affichage", + "description": "Langue, unités et formats, enregistrés dès que vous les modifiez." + } }, "admin": { "title": "Administration", diff --git a/messages/it.json b/messages/it.json index d7d68ce9d..d3a70449a 100644 --- a/messages/it.json +++ b/messages/it.json @@ -7173,7 +7173,11 @@ "failed": "Non ha funzionato. Controlla quanto inserito e riprova." }, "passwordTitle": "Password", - "tokenNameLabel": "Nome del token" + "tokenNameLabel": "Nome del token", + "displayCard": { + "title": "Visualizzazione", + "description": "Lingua, unità e formati, salvati appena li modifichi." + } }, "admin": { "title": "Administration", diff --git a/messages/ko.json b/messages/ko.json index cbf9cbc05..8c101e880 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -7173,7 +7173,11 @@ "failed": "확인하지 못했습니다. 입력한 내용을 확인하고 다시 시도하세요." }, "passwordTitle": "비밀번호", - "tokenNameLabel": "토큰 이름" + "tokenNameLabel": "토큰 이름", + "displayCard": { + "title": "표시", + "description": "언어, 단위, 형식은 바꾸는 즉시 저장돼요." + } }, "admin": { "title": "관리자", diff --git a/messages/pl.json b/messages/pl.json index f682b317e..d78363ee2 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -7173,7 +7173,11 @@ "failed": "To się nie udało. Sprawdź wprowadzone dane i spróbuj ponownie." }, "passwordTitle": "Hasło", - "tokenNameLabel": "Nazwa tokena" + "tokenNameLabel": "Nazwa tokena", + "displayCard": { + "title": "Wyświetlanie", + "description": "Język, jednostki i formaty zapisywane od razu po zmianie." + } }, "admin": { "title": "Administracja", diff --git a/src/components/settings/account-section/__tests__/display-preferences-card.test.tsx b/src/components/settings/account-section/__tests__/display-preferences-card.test.tsx new file mode 100644 index 000000000..c9e9a88a2 --- /dev/null +++ b/src/components/settings/account-section/__tests__/display-preferences-card.test.tsx @@ -0,0 +1,53 @@ +/** + * Settings → Account: the controls that save on change (language, units, + * glucose unit, hour and date format) sit in their own card with no save + * button, apart from the profile form whose Save does not cover them. + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, expect, it } from "vitest"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { DisplayPreferencesCard } from "../index"; + +function render(): string { + const client = new QueryClient({ + defaultOptions: { queries: { retry: 0, enabled: false } }, + }); + return renderToStaticMarkup( + + + + + , + ); +} + +describe("Settings → Account display card", () => { + it("holds the language select and carries no form or save button", () => { + const html = render(); + expect(html).toContain('data-testid="settings-display-card"'); + expect(html).toContain('id="language-select"'); + expect(html).not.toContain(" { + const src = readFileSync(join(__dirname, "../index.tsx"), "utf8"); + const form = src.slice(src.indexOf("")); + expect(form.length).toBeGreaterThan(0); + for (const control of [ + "language-select", + "UnitPreferenceSelect", + "GlucoseUnitSelect", + "TimeFormatSelect", + "DateFormatSelect", + ]) { + expect(form).not.toContain(control); + } + // The timezone saves with the form, so it stays inside it. + expect(form).toContain("TimezonePicker"); + }); +}); diff --git a/src/components/settings/account-section/index.tsx b/src/components/settings/account-section/index.tsx index a4cde390e..ab8604334 100644 --- a/src/components/settings/account-section/index.tsx +++ b/src/components/settings/account-section/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { Loader2, Save, User } from "lucide-react"; +import { Languages, Loader2, Save, User } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SettingsCardActions } from "@/components/settings/_card-actions"; @@ -62,7 +62,7 @@ import { dateOnlyKey } from "@/lib/tz/date-only"; export { resolveInitialTimezone } from "./account-section-utils"; export function AccountSection() { - const { t, locale, setLocale, pendingLocale } = useTranslations(); + const { t } = useTranslations(); const { user, isLoading, isAuthenticated, refetch } = useAuth(); // v1.16.4 — see `useMounted`: keeps the hydration render identical to // the SSR HTML when this boundary hydrates after `/api/auth/me` @@ -517,12 +517,10 @@ export function AccountSection() { - {/* Date of birth + language share one paired grid row so the - profile form keeps a single rhythm (every row two cells - wide on sm+). Date of birth is the bottom of the - biological-profile block; language is the only UI - preference on this card. They sit together to close the - "single-cell row" gap that broke the form's grid. */} + {/* Date of birth + timezone share one paired grid row. The + timezone stays on this card because it saves with the form + (its own PUT runs beside the profile PUT); the display + preferences that save on change live in their own card below. */}
@@ -544,44 +542,7 @@ export function AccountSection() {

-
- - {/* While a switch waits on its message bundle (dynamic - import per locale), show the target value and lock the - control — the context only flips locale + strings - together once the bundle arrived. */} - setLocale(e.target.value as Locale)} - > - {locales.map((loc) => ( - - ))} - -

- {t("settings.languageDescription")} -

-
-
- - {/* Timezone, unit system, glucose unit and the date/hour formats - share one grid block — all personal display preferences (like - language above). The glucose unit is its own dropdown rather - than a branch of the unit system because metric countries are - split on which unit they read glucose in. Every dropdown here - PATCHes its own endpoint on change; the timezone saves through - the form's submit handler. */} -
- - - -
{/* v1.7.0 — optional patient-identity fields surfaced on the @@ -702,6 +663,8 @@ export function AccountSection() { {recentProof.dialog} + + {/* v1.18.0 (S5) — injection-site exclusions moved to the dedicated Medikamente settings section, where every medication-specific preference now lives. */} @@ -734,3 +697,59 @@ export function AccountSection() { ); } + +/** + * Language, units and formats: every control here saves the moment it + * changes (each PATCHes its own endpoint), so they sit in a card with no save + * button. Inside the profile form they read as if the form's Save covered + * them, and a person who changed the language and then cancelled the form + * found it changed anyway. + */ +export function DisplayPreferencesCard({ + isAuthenticated, +}: { + isAuthenticated: boolean; +}) { + const { t, locale, setLocale, pendingLocale } = useTranslations(); + return ( + + + {/* The glucose unit is its own dropdown rather than a branch of the + unit system because metric countries are split on which unit they + read glucose in. */} +
+
+ + {/* While a switch waits on its message bundle (dynamic import per + locale), show the target value and lock the control — the + context only flips locale + strings together once the bundle + arrived. */} + setLocale(e.target.value as Locale)} + > + {locales.map((loc) => ( + + ))} + +

+ {t("settings.languageDescription")} +

+
+ + + + +
+
+ ); +}