From 247ac8494c10980c2be33a01dca648c01fa6fefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 6 Aug 2026 03:40:13 +0200 Subject: [PATCH] fix: complete managed record settings - Expose canonical defaults and reject invalid settings families\n- Localize Guardian controls and refresh dependent caches --- messages/de.json | 70 +++++ messages/en.json | 70 +++++ messages/es.json | 70 +++++ messages/fr.json | 70 +++++ messages/it.json | 70 +++++ messages/pl.json | 70 +++++ .../record-settings-configuration.test.ts | 29 ++ .../record-settings-route-guard.test.ts | 5 + src/__tests__/record-settings-ui.test.tsx | 48 +++- src/app/api/record-settings/[family]/route.ts | 19 +- .../managed-record-settings-section.test.tsx | 42 ++- .../managed-record-settings-section.tsx | 254 +++++++++++++----- src/lib/record-settings/configuration.ts | 27 +- src/lib/record-settings/index.ts | 1 + src/lib/validations/modules.ts | 2 +- .../record-settings-authorization.test.ts | 3 + 16 files changed, 759 insertions(+), 91 deletions(-) diff --git a/messages/de.json b/messages/de.json index faa9ce89e..42cf23cf1 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4434,6 +4434,76 @@ } }, "sharedRecord": { + "managedSettings": { + "profile": { + "title": "Profil", + "displayName": "Anzeigename", + "height": "Größe (cm)", + "dateOfBirth": "Geburtsdatum", + "gender": "Geschlecht", + "notSpecified": "Nicht angegeben", + "male": "Männlich", + "female": "Weiblich", + "other": "Andere Angabe", + "locale": "Sprache", + "systemDefault": "Systemstandard", + "timezone": "Zeitzone", + "units": "Einheiten", + "metric": "Metrisch", + "imperial": "Imperial", + "timeFormat": "Zeitformat", + "automatic": "Automatisch", + "hour12": "12 Stunden", + "hour24": "24 Stunden", + "dateFormat": "Datumsformat", + "dateDmy": "Tag / Monat / Jahr", + "dateMdy": "Monat / Tag / Jahr", + "dateYmd": "Jahr / Monat / Tag" + }, + "modules": { "title": "Module" }, + "notifications": { + "title": "Benachrichtigungen", + "moodReminder": "Stimmungserinnerung aktivieren", + "reminderHour": "Uhrzeit der Stimmungserinnerung", + "lowStockRunwayDays": "Tage bis zum niedrigen Bestand", + "reorderLeadDays": "Vorlaufzeit für Nachbestellung" + }, + "thresholds": { + "title": "Zielwerte", + "metric": "Messwert", + "minimum": "Minimum", + "maximum": "Maximum" + }, + "coach": { + "title": "Coach", + "disable": "Coach deaktivieren", + "tone": "Ton", + "warm": "Herzlich", + "neutral": "Neutral", + "concise": "Knapp", + "verbosity": "Ausführlichkeit", + "brief": "Kurz", + "default": "Standard", + "detailed": "Ausführlich", + "defaultWindow": "Standardzeitraum", + "last7days": "Letzte 7 Tage", + "last30days": "Letzte 30 Tage", + "last90days": "Letzte 90 Tage", + "allTime": "Gesamter Zeitraum", + "showEvidence": "Evidenz standardmäßig anzeigen", + "excludedMetrics": "Ausgeschlossene Messwerte", + "dataClusters": "Datenbereiche", + "customClusters": "Eigene Auswahl von Datenbereichen verwenden", + "value": "{value}" + }, + "insights": { + "title": "Erkenntnisse", + "sections": "Übersichtsbereiche", + "tiles": "Navigationskacheln", + "item": "{id}", + "order": "Reihenfolge" + } + }, "unavailableTitle": "In diesem geteilten Datensatz nicht verfügbar", "unavailableDescription": "Dieser Einstellungsbereich ist beim Anzeigen des Datensatzes einer anderen Person bewusst nicht verfügbar.", "integrationStatusDescription": "Verbindungsstatus für diesen verwalteten Datensatz. Verbindungssteuerungen sind nicht verfügbar." diff --git a/messages/en.json b/messages/en.json index b40757598..1339c30d4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4434,6 +4434,76 @@ } }, "sharedRecord": { + "managedSettings": { + "profile": { + "title": "Profile", + "displayName": "Display name", + "height": "Height (cm)", + "dateOfBirth": "Date of birth", + "gender": "Gender", + "notSpecified": "Not specified", + "male": "Male", + "female": "Female", + "other": "Other", + "locale": "Locale", + "systemDefault": "System default", + "timezone": "Timezone", + "units": "Units", + "metric": "Metric", + "imperial": "Imperial", + "timeFormat": "Time format", + "automatic": "Automatic", + "hour12": "12-hour", + "hour24": "24-hour", + "dateFormat": "Date format", + "dateDmy": "Day / month / year", + "dateMdy": "Month / day / year", + "dateYmd": "Year / month / day" + }, + "modules": { "title": "Modules" }, + "notifications": { + "title": "Notifications", + "moodReminder": "Enable mood reminder", + "reminderHour": "Mood reminder hour", + "lowStockRunwayDays": "Low-stock runway days", + "reorderLeadDays": "Reorder lead days" + }, + "thresholds": { + "title": "Targets", + "metric": "Metric", + "minimum": "Minimum", + "maximum": "Maximum" + }, + "coach": { + "title": "Coach", + "disable": "Disable Coach", + "tone": "Tone", + "warm": "Warm", + "neutral": "Neutral", + "concise": "Concise", + "verbosity": "Verbosity", + "brief": "Brief", + "default": "Default", + "detailed": "Detailed", + "defaultWindow": "Default window", + "last7days": "Last 7 days", + "last30days": "Last 30 days", + "last90days": "Last 90 days", + "allTime": "All time", + "showEvidence": "Show evidence by default", + "excludedMetrics": "Excluded metrics", + "dataClusters": "Data clusters", + "customClusters": "Use a custom cluster selection", + "value": "{value}" + }, + "insights": { + "title": "Insights", + "sections": "Overview sections", + "tiles": "Navigation tiles", + "item": "{id}", + "order": "Order" + } + }, "unavailableTitle": "Unavailable in this shared record", "unavailableDescription": "This Settings area is intentionally unavailable while viewing another person's record.", "integrationStatusDescription": "Connection status for this managed record. Connection controls are unavailable." diff --git a/messages/es.json b/messages/es.json index ca5332aed..3364f7f2a 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4434,6 +4434,76 @@ } }, "sharedRecord": { + "managedSettings": { + "profile": { + "title": "Perfil", + "displayName": "Nombre visible", + "height": "Altura (cm)", + "dateOfBirth": "Fecha de nacimiento", + "gender": "Género", + "notSpecified": "No especificado", + "male": "Hombre", + "female": "Mujer", + "other": "Otro", + "locale": "Idioma", + "systemDefault": "Predeterminado del sistema", + "timezone": "Zona horaria", + "units": "Unidades", + "metric": "Métrico", + "imperial": "Imperial", + "timeFormat": "Formato de hora", + "automatic": "Automático", + "hour12": "12 horas", + "hour24": "24 horas", + "dateFormat": "Formato de fecha", + "dateDmy": "Día / mes / año", + "dateMdy": "Mes / día / año", + "dateYmd": "Año / mes / día" + }, + "modules": { "title": "Módulos" }, + "notifications": { + "title": "Notificaciones", + "moodReminder": "Activar recordatorio de ánimo", + "reminderHour": "Hora del recordatorio de ánimo", + "lowStockRunwayDays": "Días hasta existencias bajas", + "reorderLeadDays": "Días de margen para reponer" + }, + "thresholds": { + "title": "Objetivos", + "metric": "Métrica", + "minimum": "Mínimo", + "maximum": "Máximo" + }, + "coach": { + "title": "Asistente", + "disable": "Desactivar asistente", + "tone": "Tono", + "warm": "Cálido", + "neutral": "Neutro", + "concise": "Conciso", + "verbosity": "Nivel de detalle", + "brief": "Breve", + "default": "Predeterminado", + "detailed": "Detallado", + "defaultWindow": "Periodo predeterminado", + "last7days": "Últimos 7 días", + "last30days": "Últimos 30 días", + "last90days": "Últimos 90 días", + "allTime": "Todo el tiempo", + "showEvidence": "Mostrar evidencia de forma predeterminada", + "excludedMetrics": "Métricas excluidas", + "dataClusters": "Grupos de datos", + "customClusters": "Usar una selección personalizada de grupos", + "value": "{value}" + }, + "insights": { + "title": "Análisis", + "sections": "Secciones del resumen", + "tiles": "Mosaicos de navegación", + "item": "{id}", + "order": "Orden" + } + }, "unavailableTitle": "No disponible en este registro compartido", "unavailableDescription": "Esta sección de Ajustes no está disponible intencionadamente mientras consultas el registro de otra persona.", "integrationStatusDescription": "Estado de conexión de este registro gestionado. Los controles de conexión no están disponibles." diff --git a/messages/fr.json b/messages/fr.json index 34de11f03..601b80743 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4434,6 +4434,76 @@ } }, "sharedRecord": { + "managedSettings": { + "profile": { + "title": "Profil", + "displayName": "Nom affiché", + "height": "Taille (cm)", + "dateOfBirth": "Date de naissance", + "gender": "Genre", + "notSpecified": "Non précisé", + "male": "Homme", + "female": "Femme", + "other": "Autre", + "locale": "Langue", + "systemDefault": "Valeur système", + "timezone": "Fuseau horaire", + "units": "Unités", + "metric": "Métrique", + "imperial": "Impérial", + "timeFormat": "Format de l’heure", + "automatic": "Automatique", + "hour12": "12 heures", + "hour24": "24 heures", + "dateFormat": "Format de date", + "dateDmy": "Jour / mois / année", + "dateMdy": "Mois / jour / année", + "dateYmd": "Année / mois / jour" + }, + "modules": { "title": "Modules" }, + "notifications": { + "title": "Notifications", + "moodReminder": "Activer le rappel d’humeur", + "reminderHour": "Heure du rappel d’humeur", + "lowStockRunwayDays": "Jours avant stock faible", + "reorderLeadDays": "Délai de réapprovisionnement" + }, + "thresholds": { + "title": "Objectifs", + "metric": "Mesure", + "minimum": "Minimum", + "maximum": "Maximum" + }, + "coach": { + "title": "Coach", + "disable": "Désactiver le Coach", + "tone": "Ton", + "warm": "Chaleureux", + "neutral": "Neutre", + "concise": "Concis", + "verbosity": "Niveau de détail", + "brief": "Bref", + "default": "Par défaut", + "detailed": "Détaillé", + "defaultWindow": "Période par défaut", + "last7days": "7 derniers jours", + "last30days": "30 derniers jours", + "last90days": "90 derniers jours", + "allTime": "Depuis toujours", + "showEvidence": "Afficher les sources par défaut", + "excludedMetrics": "Mesures exclues", + "dataClusters": "Groupes de données", + "customClusters": "Utiliser une sélection personnalisée de groupes", + "value": "{value}" + }, + "insights": { + "title": "Analyses", + "sections": "Sections d’aperçu", + "tiles": "Tuiles de navigation", + "item": "{id}", + "order": "Ordre" + } + }, "unavailableTitle": "Indisponible dans ce dossier partagé", "unavailableDescription": "Cette section des réglages est volontairement indisponible lorsque vous consultez le dossier d’une autre personne.", "integrationStatusDescription": "État de connexion pour ce dossier géré. Les commandes de connexion ne sont pas disponibles." diff --git a/messages/it.json b/messages/it.json index 3e2b9d38f..6b0b5d2c2 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4434,6 +4434,76 @@ } }, "sharedRecord": { + "managedSettings": { + "profile": { + "title": "Profilo", + "displayName": "Nome visualizzato", + "height": "Altezza (cm)", + "dateOfBirth": "Data di nascita", + "gender": "Genere", + "notSpecified": "Non specificato", + "male": "Uomo", + "female": "Donna", + "other": "Altro", + "locale": "Lingua", + "systemDefault": "Predefinito di sistema", + "timezone": "Fuso orario", + "units": "Unità", + "metric": "Metrico", + "imperial": "Imperiale", + "timeFormat": "Formato ora", + "automatic": "Automatico", + "hour12": "12 ore", + "hour24": "24 ore", + "dateFormat": "Formato data", + "dateDmy": "Giorno / mese / anno", + "dateMdy": "Mese / giorno / anno", + "dateYmd": "Anno / mese / giorno" + }, + "modules": { "title": "Moduli" }, + "notifications": { + "title": "Notifiche", + "moodReminder": "Attiva promemoria dell’umore", + "reminderHour": "Ora del promemoria dell’umore", + "lowStockRunwayDays": "Giorni prima delle scorte basse", + "reorderLeadDays": "Margine per il riordino" + }, + "thresholds": { + "title": "Obiettivi", + "metric": "Metrica", + "minimum": "Minimo", + "maximum": "Massimo" + }, + "coach": { + "title": "Coach", + "disable": "Disattiva Coach", + "tone": "Tono", + "warm": "Caldo", + "neutral": "Neutro", + "concise": "Conciso", + "verbosity": "Livello di dettaglio", + "brief": "Breve", + "default": "Predefinito", + "detailed": "Dettagliato", + "defaultWindow": "Periodo predefinito", + "last7days": "Ultimi 7 giorni", + "last30days": "Ultimi 30 giorni", + "last90days": "Ultimi 90 giorni", + "allTime": "Tutto il periodo", + "showEvidence": "Mostra le fonti per impostazione predefinita", + "excludedMetrics": "Metriche escluse", + "dataClusters": "Gruppi di dati", + "customClusters": "Usa una selezione personalizzata di gruppi", + "value": "{value}" + }, + "insights": { + "title": "Analisi", + "sections": "Sezioni della panoramica", + "tiles": "Riquadri di navigazione", + "item": "{id}", + "order": "Ordine" + } + }, "unavailableTitle": "Non disponibile in questo record condiviso", "unavailableDescription": "Questa area delle Impostazioni non è disponibile intenzionalmente mentre visualizzi il record di un’altra persona.", "integrationStatusDescription": "Stato di connessione per questo record gestito. I controlli di connessione non sono disponibili." diff --git a/messages/pl.json b/messages/pl.json index ad45b25c4..6f1cdc657 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4434,6 +4434,76 @@ } }, "sharedRecord": { + "managedSettings": { + "profile": { + "title": "Profil", + "displayName": "Wyświetlana nazwa", + "height": "Wzrost (cm)", + "dateOfBirth": "Data urodzenia", + "gender": "Płeć", + "notSpecified": "Nie podano", + "male": "Mężczyzna", + "female": "Kobieta", + "other": "Inna", + "locale": "Język", + "systemDefault": "Ustawienie systemowe", + "timezone": "Strefa czasowa", + "units": "Jednostki", + "metric": "Metryczne", + "imperial": "Imperialne", + "timeFormat": "Format czasu", + "automatic": "Automatyczny", + "hour12": "12-godzinny", + "hour24": "24-godzinny", + "dateFormat": "Format daty", + "dateDmy": "Dzień / miesiąc / rok", + "dateMdy": "Miesiąc / dzień / rok", + "dateYmd": "Rok / miesiąc / dzień" + }, + "modules": { "title": "Moduły" }, + "notifications": { + "title": "Powiadomienia", + "moodReminder": "Włącz przypomnienie nastroju", + "reminderHour": "Godzina przypomnienia nastroju", + "lowStockRunwayDays": "Dni do niskiego stanu zapasów", + "reorderLeadDays": "Wyprzedzenie ponownego zamówienia" + }, + "thresholds": { + "title": "Cele", + "metric": "Metryka", + "minimum": "Minimum", + "maximum": "Maksimum" + }, + "coach": { + "title": "Trener", + "disable": "Wyłącz trenera", + "tone": "Ton", + "warm": "Ciepły", + "neutral": "Neutralny", + "concise": "Zwięzły", + "verbosity": "Szczegółowość", + "brief": "Krótka", + "default": "Domyślna", + "detailed": "Szczegółowa", + "defaultWindow": "Domyślny okres", + "last7days": "Ostatnie 7 dni", + "last30days": "Ostatnie 30 dni", + "last90days": "Ostatnie 90 dni", + "allTime": "Cały okres", + "showEvidence": "Domyślnie pokazuj źródła", + "excludedMetrics": "Wykluczone metryki", + "dataClusters": "Grupy danych", + "customClusters": "Użyj własnego wyboru grup", + "value": "{value}" + }, + "insights": { + "title": "Wnioski", + "sections": "Sekcje przeglądu", + "tiles": "Kafelki nawigacji", + "item": "{id}", + "order": "Kolejność" + } + }, "unavailableTitle": "Niedostępne w tym udostępnionym rekordzie", "unavailableDescription": "Ten obszar Ustawień jest celowo niedostępny podczas przeglądania rekordu innej osoby.", "integrationStatusDescription": "Stan połączenia dla tego zarządzanego rekordu. Elementy sterujące połączeniem są niedostępne." diff --git a/src/__tests__/record-settings-configuration.test.ts b/src/__tests__/record-settings-configuration.test.ts index 27c078dc4..f967fa668 100644 --- a/src/__tests__/record-settings-configuration.test.ts +++ b/src/__tests__/record-settings-configuration.test.ts @@ -1,9 +1,13 @@ import { describe, expect, it } from "vitest"; import { + isManagedRecordSettingsFamily, MANAGED_RECORD_SETTINGS_FIELD_ALLOWLIST, + MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS, + managedModulePreferencesFrom, parseManagedRecordSettingsPatch, } from "@/lib/record-settings/configuration"; +import { WRITABLE_MODULE_KEYS } from "@/lib/validations/modules"; describe("managed record settings configuration contract", () => { it("enumerates the only target-record fields each settings DTO may write", () => { @@ -95,4 +99,29 @@ describe("managed record settings configuration contract", () => { ] as const)("accepts a typed %s patch", (family, patch) => { expect(() => parseManagedRecordSettingsPatch(family, patch)).not.toThrow(); }); + + it("uses the complete canonical module inventory for a fresh record", () => { + expect(Object.keys(MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS)).toEqual( + WRITABLE_MODULE_KEYS, + ); + expect(managedModulePreferencesFrom(null)).toEqual( + MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS, + ); + }); + + it("accepts only the canonical profile gender enum", () => { + expect(() => + parseManagedRecordSettingsPatch("profile", { gender: "MALE" }), + ).not.toThrow(); + expect(() => + parseManagedRecordSettingsPatch("profile", { gender: "UNKNOWN" }), + ).toThrow(); + }); + + it.each(["toString", "constructor", "__proto__", "hasOwnProperty"])( + "rejects inherited family name %s", + (family) => { + expect(isManagedRecordSettingsFamily(family)).toBe(false); + }, + ); }); diff --git a/src/__tests__/record-settings-route-guard.test.ts b/src/__tests__/record-settings-route-guard.test.ts index 9d398a045..6435fadb1 100644 --- a/src/__tests__/record-settings-route-guard.test.ts +++ b/src/__tests__/record-settings-route-guard.test.ts @@ -57,5 +57,10 @@ describe("record settings route guard", () => { for (const family of families) { expect(source).toContain(`case \"${family}\"`); } + expect(source).toContain("if (!isManagedRecordSettingsFamily(rawFamily))"); + expect(source).toContain('apiError("Settings family not found", 404)'); + expect( + source.indexOf("isManagedRecordSettingsFamily(rawFamily)"), + ).toBeLessThan(source.indexOf("const context = await requireGuardianAuth")); }); }); diff --git a/src/__tests__/record-settings-ui.test.tsx b/src/__tests__/record-settings-ui.test.tsx index 92b6fd56e..5d9e17ef3 100644 --- a/src/__tests__/record-settings-ui.test.tsx +++ b/src/__tests__/record-settings-ui.test.tsx @@ -5,6 +5,27 @@ import { describe, expect, it } from "vitest"; const fromRoot = (...segments: string[]) => resolve(process.cwd(), ...segments); +function expectMatchingMessageKeys(expected: unknown, actual: unknown): void { + if ( + expected === null || + typeof expected !== "object" || + actual === null || + typeof actual !== "object" + ) { + expect(typeof actual).toBe(typeof expected); + return; + } + + const expectedRecord = expected as Record; + const actualRecord = actual as Record; + expect(Object.keys(actualRecord).sort()).toEqual( + Object.keys(expectedRecord).sort(), + ); + for (const key of Object.keys(expectedRecord)) { + expectMatchingMessageKeys(expectedRecord[key], actualRecord[key]); + } +} + describe("managed record settings UI", () => { it("gates shared deep links and renders integration status without controls", async () => { const [ @@ -54,10 +75,31 @@ describe("managed record settings UI", () => { expect(managedSettingsView).not.toContain("/api/auth/"); expect(managedSettingsView).not.toContain("/api/user/"); expect(managedSettingsView).not.toContain(", ) => renderToStaticMarkup( - , + + + , ); describe("managed record settings forms", () => { @@ -62,4 +68,26 @@ describe("managed record settings forms", () => { expect(html).not.toContain(" { + const modules = render("modules", { modulePreferences: {} }); + const thresholds = render("thresholds", { + overrides: {}, + effective: Object.fromEntries( + ALL_METRICS.map((metric) => [ + metric, + { range: { greenMin: 10, greenMax: 20 } }, + ]), + ), + }); + + for (const module of Object.keys(MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS)) { + expect(modules).toContain(`name="${module}"`); + } + for (const metric of ALL_METRICS) { + expect(thresholds).toContain(`value="${metric}"`); + } + expect(thresholds).toContain('value="10"'); + expect(thresholds).toContain('value="20"'); + }); }); diff --git a/src/components/settings/managed-record-settings-section.tsx b/src/components/settings/managed-record-settings-section.tsx index ae9fc3608..df97bd33a 100644 --- a/src/components/settings/managed-record-settings-section.tsx +++ b/src/components/settings/managed-record-settings-section.tsx @@ -7,12 +7,19 @@ import { Settings } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { apiGet, apiPatch } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; +import { MODULE_REGISTRY } from "@/lib/modules/registry"; import { queryKeys } from "@/lib/query-keys"; import { assertRecordSettingsResponseForRecord, + MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS, safeParseManagedRecordSettingsPatch, type ManagedRecordSettingsFamily, } from "@/lib/record-settings"; +import { + METRIC_BOUNDS, + type ThresholdMetric, +} from "@/lib/analytics/effective-range"; +import { ALL_METRICS } from "@/lib/validations/thresholds"; import { Button } from "@/components/ui/button"; import { QueryErrorCard } from "@/components/ui/query-error-card"; import { SettingsCardHeader } from "./_card-header"; @@ -60,6 +67,22 @@ const COACH_DATA_CLUSTERS = [ "mobility", "environment", ] as const; +const METRIC_LABEL_KEYS: Record = { + WEIGHT: "thresholds.metricWeight", + BLOOD_PRESSURE_SYS: "thresholds.metricBpSys", + BLOOD_PRESSURE_DIA: "thresholds.metricBpDia", + PULSE: "thresholds.metricPulse", + BODY_FAT: "thresholds.metricBodyFat", + SLEEP_DURATION: "thresholds.metricSleep", + ACTIVITY_STEPS: "thresholds.metricSteps", + BLOOD_GLUCOSE_FASTING: "thresholds.metricGlucoseFasting", + BLOOD_GLUCOSE_POSTPRANDIAL: "thresholds.metricGlucosePostprandial", + BLOOD_GLUCOSE_RANDOM: "thresholds.metricGlucoseRandom", + BLOOD_GLUCOSE_BEDTIME: "thresholds.metricGlucoseBedtime", + TOTAL_BODY_WATER: "thresholds.metricBodyWater", + BONE_MASS: "thresholds.metricBoneMass", + OXYGEN_SATURATION: "thresholds.metricOxygenSaturation", +}; function asRecord(value: unknown): Record { return value !== null && typeof value === "object" && !Array.isArray(value) @@ -100,6 +123,7 @@ function ProfileSettingsForm({ onSave, saveLabel, }: SettingsFormProps) { + const { t } = useTranslations(); function submit(event: FormEvent) { event.preventDefault(); const form = new FormData(event.currentTarget); @@ -121,7 +145,7 @@ function ProfileSettingsForm({ return (
@@ -254,10 +306,12 @@ function ModulesSettingsForm({ onSave, saveLabel, }: SettingsFormProps) { + const { t } = useTranslations(); const preferences = asRecord(settings.modulePreferences); - const entries = Object.entries(preferences).sort(([left], [right]) => - left.localeCompare(right), - ); + const entries = Object.entries(MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS) as [ + keyof typeof MANAGED_RECORD_SETTINGS_MODULE_DEFAULTS, + boolean, + ][]; function submit(event: FormEvent) { event.preventDefault(); @@ -271,15 +325,19 @@ function ModulesSettingsForm({ return ( - {entries.map(([key, enabled]) => ( + {entries.map(([key, defaultEnabled]) => ( ))} @@ -293,6 +351,7 @@ function NotificationsSettingsForm({ onSave, saveLabel, }: SettingsFormProps) { + const { t } = useTranslations(); const preferences = asRecord(settings.notificationPreferences); const medication = asRecord(preferences.medication); const mood = asRecord(preferences.mood); @@ -324,10 +383,10 @@ function NotificationsSettingsForm({ name="moodReminderEnabled" type="checkbox" /> - Enable mood reminder + {t("settings.sharedRecord.managedSettings.notifications.moodReminder")}
- Excluded metrics + + {t("settings.sharedRecord.managedSettings.coach.excludedMetrics")} + {COACH_EXCLUDE_METRICS.map((metric) => ( ))}
- Data clusters + + {t("settings.sharedRecord.managedSettings.coach.dataClusters")} + {COACH_DATA_CLUSTERS.map((cluster) => ( ))}
@@ -622,6 +727,7 @@ function InsightsSettingsForm({ onSave, saveLabel, }: SettingsFormProps) { + const { t } = useTranslations(); const layout = asRecord(settings.layout); const sections = layoutItems(layout.sections); const tiles = layoutItems(layout.tiles); @@ -646,7 +752,9 @@ function InsightsSettingsForm({ const renderItems = (kind: "section" | "tile", items: LayoutItem[]) => (
- {kind === "section" ? "Overview sections" : "Navigation tiles"} + {kind === "section" + ? t("settings.sharedRecord.managedSettings.insights.sections") + : t("settings.sharedRecord.managedSettings.insights.tiles")} {items.map((item) => (
@@ -657,13 +765,15 @@ function InsightsSettingsForm({ name={`${kind}-visible-${item.id}`} type="checkbox" /> - {item.id} + {t("settings.sharedRecord.managedSettings.insights.item", { + id: item.id, + })}