mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Label every ingest source instead of leaking its enum value
Settings, Sources listed GOOGLE_HEALTH, OURA, POLAR and STRAVA by their internal name: the catalogue was written for seven sources and every provider added since fell through to the raw value. The measurement list carried the same fall-through in its source filter and its table column, and the medication intake history read "via APPLE_HEALTH" for a dose synced from Apple Health. The three catalogues move into one module keyed by the enum, so an omission is a type error rather than a raw value on screen, and the sleep discrepancy tooltip drops the private brand-name map it had grown to work around the gap.
This commit is contained in:
1 parent
72117104d5
commit
0548c0265e
15 files changed
+269
-78
No files matched your search
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Settings, Sources: four of the sources in the priority ladder were listed
|
||||
by their internal name. `GOOGLE_HEALTH`, `OURA`, `POLAR` and `STRAVA`
|
||||
appeared exactly like that instead of Google Health, Oura, Polar and
|
||||
Strava. The label catalogue was written when there were seven sources and
|
||||
every provider added since fell through to the raw value. The same
|
||||
fall-through sat in the measurement list, where the source filter offers
|
||||
every source there is and the table paints one per row, and on the
|
||||
medication intake history, where a dose synced from Apple Health read
|
||||
"via APPLE_HEALTH". All three catalogues are complete in all seven
|
||||
languages now, and a test reads the source enums out of the schema and
|
||||
fails when a member has no label, so the next provider cannot ship
|
||||
without its name.
|
||||
|
||||
## [1.38.2] — 2026-09-02
|
||||
|
||||
Anthropic keys work on current Claude models again, and the daily briefing
|
||||
|
||||
+16
-1
@@ -731,6 +731,13 @@
|
||||
"sourceComputed": "Berechnet",
|
||||
"sourceWhoop": "WHOOP",
|
||||
"sourceFitbit": "Fitbit",
|
||||
"sourceNightscout": "Nightscout",
|
||||
"sourcePolar": "Polar",
|
||||
"sourceOura": "Oura",
|
||||
"sourceTelegram": "Telegram",
|
||||
"sourceMcp": "MCP",
|
||||
"sourceGoogleHealth": "Google Health",
|
||||
"sourceStrava": "Strava",
|
||||
"bulkDeleteSuccess": "{count} Messwerte gelöscht",
|
||||
"bulkDeleteSuccessOne": "Messwert gelöscht",
|
||||
"bulkDeleteError": "Fehler beim Löschen der ausgewählten Messungen",
|
||||
@@ -1630,6 +1637,7 @@
|
||||
"sourceApi": "iOS-App",
|
||||
"sourceReminder": "Erinnerung",
|
||||
"sourceImport": "Import",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"intakeSourceVia": "über {origin}",
|
||||
"intakeHistoryEmptyTitle": "Noch keine Einnahmen erfasst",
|
||||
"intakeHistoryEmptyDescription": "Trage eine Einnahme auf der Tagesübersicht ein, um den Verlauf aufzubauen.",
|
||||
@@ -4889,7 +4897,14 @@
|
||||
"IMPORT": "Import",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Berechnet"
|
||||
"COMPUTED": "Berechnet",
|
||||
"NIGHTSCOUT": "Nightscout",
|
||||
"POLAR": "Polar",
|
||||
"OURA": "Oura",
|
||||
"TELEGRAM": "Telegram",
|
||||
"MCP": "MCP",
|
||||
"GOOGLE_HEALTH": "Google Health",
|
||||
"STRAVA": "Strava"
|
||||
},
|
||||
"deviceLabels": {
|
||||
"watch": "Uhr",
|
||||
|
||||
+16
-1
@@ -731,6 +731,13 @@
|
||||
"sourceComputed": "Computed",
|
||||
"sourceWhoop": "WHOOP",
|
||||
"sourceFitbit": "Fitbit",
|
||||
"sourceNightscout": "Nightscout",
|
||||
"sourcePolar": "Polar",
|
||||
"sourceOura": "Oura",
|
||||
"sourceTelegram": "Telegram",
|
||||
"sourceMcp": "MCP",
|
||||
"sourceGoogleHealth": "Google Health",
|
||||
"sourceStrava": "Strava",
|
||||
"bulkDeleteSuccess": "{count} measurements deleted",
|
||||
"bulkDeleteSuccessOne": "Measurement deleted",
|
||||
"bulkDeleteError": "Error deleting the selected measurements",
|
||||
@@ -1630,6 +1637,7 @@
|
||||
"sourceApi": "iOS App",
|
||||
"sourceReminder": "Reminder",
|
||||
"sourceImport": "Import",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"intakeSourceVia": "via {origin}",
|
||||
"intakeHistoryEmptyTitle": "No intakes recorded yet",
|
||||
"intakeHistoryEmptyDescription": "Log a dose on the daily intake page to start building the history.",
|
||||
@@ -4889,7 +4897,14 @@
|
||||
"IMPORT": "Import",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Computed"
|
||||
"COMPUTED": "Computed",
|
||||
"NIGHTSCOUT": "Nightscout",
|
||||
"POLAR": "Polar",
|
||||
"OURA": "Oura",
|
||||
"TELEGRAM": "Telegram",
|
||||
"MCP": "MCP",
|
||||
"GOOGLE_HEALTH": "Google Health",
|
||||
"STRAVA": "Strava"
|
||||
},
|
||||
"deviceLabels": {
|
||||
"watch": "Watch",
|
||||
|
||||
+16
-1
@@ -731,6 +731,13 @@
|
||||
"sourceComputed": "Calculado",
|
||||
"sourceWhoop": "WHOOP",
|
||||
"sourceFitbit": "Fitbit",
|
||||
"sourceNightscout": "Nightscout",
|
||||
"sourcePolar": "Polar",
|
||||
"sourceOura": "Oura",
|
||||
"sourceTelegram": "Telegram",
|
||||
"sourceMcp": "MCP",
|
||||
"sourceGoogleHealth": "Google Health",
|
||||
"sourceStrava": "Strava",
|
||||
"bulkDeleteSuccess": "{count} mediciones eliminadas",
|
||||
"bulkDeleteSuccessOne": "Medición eliminada",
|
||||
"bulkDeleteError": "Error al eliminar las mediciones seleccionadas",
|
||||
@@ -1630,6 +1637,7 @@
|
||||
"sourceApi": "App de iOS",
|
||||
"sourceReminder": "Recordatorio",
|
||||
"sourceImport": "Importación",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"intakeSourceVia": "vía {origin}",
|
||||
"intakeHistoryEmptyTitle": "Aún no hay tomas",
|
||||
"intakeHistoryEmptyDescription": "En cuanto registres una toma, aparecerá aquí con la hora y la fuente.",
|
||||
@@ -4889,7 +4897,14 @@
|
||||
"IMPORT": "Importar",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Calculado"
|
||||
"COMPUTED": "Calculado",
|
||||
"NIGHTSCOUT": "Nightscout",
|
||||
"POLAR": "Polar",
|
||||
"OURA": "Oura",
|
||||
"TELEGRAM": "Telegram",
|
||||
"MCP": "MCP",
|
||||
"GOOGLE_HEALTH": "Google Health",
|
||||
"STRAVA": "Strava"
|
||||
},
|
||||
"deviceLabels": {
|
||||
"watch": "Reloj",
|
||||
|
||||
+16
-1
@@ -731,6 +731,13 @@
|
||||
"sourceComputed": "Calculé",
|
||||
"sourceWhoop": "WHOOP",
|
||||
"sourceFitbit": "Fitbit",
|
||||
"sourceNightscout": "Nightscout",
|
||||
"sourcePolar": "Polar",
|
||||
"sourceOura": "Oura",
|
||||
"sourceTelegram": "Telegram",
|
||||
"sourceMcp": "MCP",
|
||||
"sourceGoogleHealth": "Google Health",
|
||||
"sourceStrava": "Strava",
|
||||
"bulkDeleteSuccess": "{count} mesures supprimées",
|
||||
"bulkDeleteSuccessOne": "Mesure supprimée",
|
||||
"bulkDeleteError": "Erreur lors de la suppression des mesures sélectionnées",
|
||||
@@ -1630,6 +1637,7 @@
|
||||
"sourceApi": "App iOS",
|
||||
"sourceReminder": "Rappel",
|
||||
"sourceImport": "Import",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"intakeSourceVia": "via {origin}",
|
||||
"intakeHistoryEmptyTitle": "Aucune prise pour l’instant",
|
||||
"intakeHistoryEmptyDescription": "Dès que tu enregistreras une prise, elle apparaîtra ici avec l’heure et la source.",
|
||||
@@ -4889,7 +4897,14 @@
|
||||
"IMPORT": "Importer",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Calculé"
|
||||
"COMPUTED": "Calculé",
|
||||
"NIGHTSCOUT": "Nightscout",
|
||||
"POLAR": "Polar",
|
||||
"OURA": "Oura",
|
||||
"TELEGRAM": "Telegram",
|
||||
"MCP": "MCP",
|
||||
"GOOGLE_HEALTH": "Google Health",
|
||||
"STRAVA": "Strava"
|
||||
},
|
||||
"deviceLabels": {
|
||||
"watch": "Montre",
|
||||
|
||||
+16
-1
@@ -731,6 +731,13 @@
|
||||
"sourceComputed": "Calcolato",
|
||||
"sourceWhoop": "WHOOP",
|
||||
"sourceFitbit": "Fitbit",
|
||||
"sourceNightscout": "Nightscout",
|
||||
"sourcePolar": "Polar",
|
||||
"sourceOura": "Oura",
|
||||
"sourceTelegram": "Telegram",
|
||||
"sourceMcp": "MCP",
|
||||
"sourceGoogleHealth": "Google Health",
|
||||
"sourceStrava": "Strava",
|
||||
"bulkDeleteSuccess": "{count} misurazioni eliminate",
|
||||
"bulkDeleteSuccessOne": "Misurazione eliminata",
|
||||
"bulkDeleteError": "Errore durante l'eliminazione delle misurazioni selezionate",
|
||||
@@ -1630,6 +1637,7 @@
|
||||
"sourceApi": "App iOS",
|
||||
"sourceReminder": "Promemoria",
|
||||
"sourceImport": "Importazione",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"intakeSourceVia": "tramite {origin}",
|
||||
"intakeHistoryEmptyTitle": "Ancora nessuna assunzione",
|
||||
"intakeHistoryEmptyDescription": "Appena registrerai un’assunzione, comparirà qui con orario e sorgente.",
|
||||
@@ -4889,7 +4897,14 @@
|
||||
"IMPORT": "Importa",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Calcolato"
|
||||
"COMPUTED": "Calcolato",
|
||||
"NIGHTSCOUT": "Nightscout",
|
||||
"POLAR": "Polar",
|
||||
"OURA": "Oura",
|
||||
"TELEGRAM": "Telegram",
|
||||
"MCP": "MCP",
|
||||
"GOOGLE_HEALTH": "Google Health",
|
||||
"STRAVA": "Strava"
|
||||
},
|
||||
"deviceLabels": {
|
||||
"watch": "Orologio",
|
||||
|
||||
+16
-1
@@ -731,6 +731,13 @@
|
||||
"sourceComputed": "계산값",
|
||||
"sourceWhoop": "WHOOP",
|
||||
"sourceFitbit": "Fitbit",
|
||||
"sourceNightscout": "Nightscout",
|
||||
"sourcePolar": "Polar",
|
||||
"sourceOura": "Oura",
|
||||
"sourceTelegram": "Telegram",
|
||||
"sourceMcp": "MCP",
|
||||
"sourceGoogleHealth": "Google Health",
|
||||
"sourceStrava": "Strava",
|
||||
"bulkDeleteSuccess": "{count}개 측정을 삭제했어요",
|
||||
"bulkDeleteSuccessOne": "측정을 삭제했어요",
|
||||
"bulkDeleteError": "선택한 측정을 삭제하지 못했어요",
|
||||
@@ -1630,6 +1637,7 @@
|
||||
"sourceApi": "iOS 앱",
|
||||
"sourceReminder": "알림",
|
||||
"sourceImport": "불러오기",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"intakeSourceVia": "{origin} 경유",
|
||||
"intakeHistoryEmptyTitle": "아직 복용 기록이 없어요",
|
||||
"intakeHistoryEmptyDescription": "일일 복용 페이지에서 용량을 기록하면 이력이 쌓여요.",
|
||||
@@ -4889,7 +4897,14 @@
|
||||
"IMPORT": "불러오기",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "계산값"
|
||||
"COMPUTED": "계산값",
|
||||
"NIGHTSCOUT": "Nightscout",
|
||||
"POLAR": "Polar",
|
||||
"OURA": "Oura",
|
||||
"TELEGRAM": "Telegram",
|
||||
"MCP": "MCP",
|
||||
"GOOGLE_HEALTH": "Google Health",
|
||||
"STRAVA": "Strava"
|
||||
},
|
||||
"deviceLabels": {
|
||||
"watch": "워치",
|
||||
|
||||
+16
-1
@@ -731,6 +731,13 @@
|
||||
"sourceComputed": "Obliczone",
|
||||
"sourceWhoop": "WHOOP",
|
||||
"sourceFitbit": "Fitbit",
|
||||
"sourceNightscout": "Nightscout",
|
||||
"sourcePolar": "Polar",
|
||||
"sourceOura": "Oura",
|
||||
"sourceTelegram": "Telegram",
|
||||
"sourceMcp": "MCP",
|
||||
"sourceGoogleHealth": "Google Health",
|
||||
"sourceStrava": "Strava",
|
||||
"bulkDeleteSuccess": "Usunięto pomiary: {count}",
|
||||
"bulkDeleteSuccessOne": "Usunięto pomiar",
|
||||
"bulkDeleteError": "Błąd podczas usuwania wybranych pomiarów",
|
||||
@@ -1630,6 +1637,7 @@
|
||||
"sourceApi": "Aplikacja iOS",
|
||||
"sourceReminder": "Przypomnienie",
|
||||
"sourceImport": "Import",
|
||||
"sourceAppleHealth": "Apple Health",
|
||||
"intakeSourceVia": "przez {origin}",
|
||||
"intakeHistoryEmptyTitle": "Brak przyjęć",
|
||||
"intakeHistoryEmptyDescription": "Gdy tylko zarejestrujesz przyjęcie, pojawi się tu wraz z godziną i źródłem.",
|
||||
@@ -4889,7 +4897,14 @@
|
||||
"IMPORT": "Importuj",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Obliczone"
|
||||
"COMPUTED": "Obliczone",
|
||||
"NIGHTSCOUT": "Nightscout",
|
||||
"POLAR": "Polar",
|
||||
"OURA": "Oura",
|
||||
"TELEGRAM": "Telegram",
|
||||
"MCP": "MCP",
|
||||
"GOOGLE_HEALTH": "Google Health",
|
||||
"STRAVA": "Strava"
|
||||
},
|
||||
"deviceLabels": {
|
||||
"watch": "Zegarek",
|
||||
|
||||
@@ -110,12 +110,15 @@ const LEGIT_IDENTICAL = new Set<string>([
|
||||
"admin.overview.snapshotOfflineGeoOn",
|
||||
"admin.webPushVapidTitle",
|
||||
"measurements.sourceAppleHealth",
|
||||
"measurements.sourceGoogleHealth",
|
||||
"medications.sourceAppleHealth",
|
||||
"onboarding.source.more.appleHealth",
|
||||
"settings.ai.providerChain.types.anthropic",
|
||||
"settings.ai.providerChain.types.codex",
|
||||
"settings.ai.providerSelect.anthropic",
|
||||
"settings.sections.export.import.appleHealth.title",
|
||||
"settings.sections.sources.sourceLabels.APPLE_HEALTH",
|
||||
"settings.sections.sources.sourceLabels.GOOGLE_HEALTH",
|
||||
"settings.appleHealth.title",
|
||||
"mood.tag.fastFood",
|
||||
// Technical / config labels conventionally kept verbatim.
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { formatDurationMinutes } from "@/lib/i18n/duration";
|
||||
import { MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS } from "@/lib/i18n/source-labels";
|
||||
|
||||
/**
|
||||
* v1.28.x — the discreet "sources disagree" marker, extracted from
|
||||
@@ -30,35 +31,6 @@ export interface SleepSourceDiscrepancyDto {
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Localised label keys per `MeasurementSource` — the same map the
|
||||
* source-priority settings surface uses (`sources-section.tsx`), so both
|
||||
* surfaces humanise a source identically.
|
||||
*/
|
||||
const SOURCE_LABEL_KEYS: Record<string, string> = {
|
||||
WITHINGS: "settings.sections.sources.sourceLabels.WITHINGS",
|
||||
APPLE_HEALTH: "settings.sections.sources.sourceLabels.APPLE_HEALTH",
|
||||
MANUAL: "settings.sections.sources.sourceLabels.MANUAL",
|
||||
IMPORT: "settings.sections.sources.sourceLabels.IMPORT",
|
||||
WHOOP: "settings.sections.sources.sourceLabels.WHOOP",
|
||||
COMPUTED: "settings.sections.sources.sourceLabels.COMPUTED",
|
||||
FITBIT: "settings.sections.sources.sourceLabels.FITBIT",
|
||||
};
|
||||
|
||||
/**
|
||||
* Brand names for sources without a localised settings label — brand
|
||||
* spellings are locale-invariant, so no i18n key is needed for them.
|
||||
*/
|
||||
const SOURCE_BRAND_NAMES: Record<string, string> = {
|
||||
OURA: "Oura",
|
||||
POLAR: "Polar",
|
||||
GOOGLE_HEALTH: "Google Health",
|
||||
NIGHTSCOUT: "Nightscout",
|
||||
STRAVA: "Strava",
|
||||
TELEGRAM: "Telegram",
|
||||
MCP: "MCP",
|
||||
};
|
||||
|
||||
/**
|
||||
* Localised device-type label keys (`Measurement.deviceType`), reused from
|
||||
* the source-priority settings surface. Appended in parentheses so two
|
||||
@@ -79,8 +51,13 @@ export function sourceDisplayName(
|
||||
deviceType: string | null,
|
||||
t: (key: string) => string,
|
||||
): string {
|
||||
const labelKey = SOURCE_LABEL_KEYS[source];
|
||||
const base = labelKey ? t(labelKey) : (SOURCE_BRAND_NAMES[source] ?? source);
|
||||
// The wire carries whatever the row's `source` column held, so an
|
||||
// unknown value still has to render as itself rather than as a missing
|
||||
// key. The catalogue covers every enum member the guard knows about.
|
||||
const labelKey = MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS[
|
||||
source as keyof typeof MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS
|
||||
] as string | undefined;
|
||||
const base = labelKey ? t(labelKey) : source;
|
||||
const deviceKey = deviceType ? DEVICE_TYPE_LABEL_KEYS[deviceType] : undefined;
|
||||
return deviceKey ? `${base} (${t(deviceKey)})` : base;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ import { createPortal } from "react-dom";
|
||||
import { toast } from "sonner";
|
||||
import { formatDateOrRelative, formatDateTime } from "@/lib/format";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { MEASUREMENT_SOURCE_LIST_LABEL_KEYS } from "@/lib/i18n/source-labels";
|
||||
import { CUMULATIVE_DAY_SUM_TYPES } from "@/lib/measurements/cumulative-day-sum";
|
||||
import { rawDisplayFractionDigits } from "@/lib/measurements/display-transform";
|
||||
import { getUnitForType } from "@/lib/validations/measurement";
|
||||
@@ -217,23 +218,22 @@ function toDateTimeLocalValue(isoString: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `MeasurementSource` enum (`MANUAL` / `WITHINGS` / `IMPORT`
|
||||
* / `APPLE_HEALTH`) using the existing `measurements.source*`
|
||||
* translation keys instead of leaking the SCREAMING_SNAKE enum into
|
||||
* the table cell.
|
||||
* Render the `MeasurementSource` enum using the `measurements.source*`
|
||||
* translation keys instead of leaking the SCREAMING_SNAKE enum into the
|
||||
* table cell or the filter dropdown. The catalogue is keyed by the enum,
|
||||
* so a provider added without its label is a type error rather than a row
|
||||
* that reads `GOOGLE_HEALTH`.
|
||||
*/
|
||||
function formatMeasurementSource(
|
||||
source: string,
|
||||
t: ReturnType<typeof useTranslations>["t"],
|
||||
) {
|
||||
if (source === "WITHINGS") return t("measurements.sourceWithings");
|
||||
if (source === "IMPORT") return t("measurements.sourceImport");
|
||||
if (source === "MANUAL") return t("measurements.sourceManual");
|
||||
if (source === "APPLE_HEALTH") return t("measurements.sourceAppleHealth");
|
||||
if (source === "COMPUTED") return t("measurements.sourceComputed");
|
||||
if (source === "WHOOP") return t("measurements.sourceWhoop");
|
||||
if (source === "FITBIT") return t("measurements.sourceFitbit");
|
||||
return source;
|
||||
const key = MEASUREMENT_SOURCE_LIST_LABEL_KEYS[
|
||||
source as keyof typeof MEASUREMENT_SOURCE_LIST_LABEL_KEYS
|
||||
] as string | undefined;
|
||||
// A row written by a server the client does not know yet still renders
|
||||
// as its own value rather than as a missing translation key.
|
||||
return key ? t(key) : source;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,10 @@ import {
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import {
|
||||
INTAKE_SOURCE_LABEL_KEYS,
|
||||
type IntakeSourceValue,
|
||||
} from "@/lib/i18n/source-labels";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -71,23 +75,22 @@ import { apiGet } from "@/lib/api/api-fetch";
|
||||
* outline SkipForward + "Übersprungen" for skipped rows. The
|
||||
* component never labels a row both at once.
|
||||
* - Source badge surfaces where the intake was logged — WEB → "Web",
|
||||
* API → "iOS App", REMINDER → "Reminder", IMPORT → "Import" — wrapped
|
||||
* in a muted "via {origin}" caption next to the status. The labels map
|
||||
* the coarse `IntakeSource` enum to the practical client per the
|
||||
* existing `medications.source*` keys; there is no finer client field.
|
||||
* API → "iOS App", REMINDER → "Reminder", IMPORT → "Import",
|
||||
* APPLE_HEALTH → "Apple Health" — wrapped in a muted "via {origin}"
|
||||
* caption next to the status. The labels map the coarse `IntakeSource`
|
||||
* enum to the practical client per the existing `medications.source*`
|
||||
* keys; there is no finer client field.
|
||||
* - Empty state copy + CTA back to the daily intake page.
|
||||
* - No edit-in-row, no delete buttons.
|
||||
*/
|
||||
|
||||
type IntakeSource = "WEB" | "API" | "REMINDER" | "IMPORT";
|
||||
|
||||
export interface IntakeEvent {
|
||||
id: string;
|
||||
medicationId: string;
|
||||
scheduledFor: string;
|
||||
takenAt: string | null;
|
||||
skipped: boolean;
|
||||
source: IntakeSource;
|
||||
source: IntakeSourceValue;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -205,13 +208,14 @@ export function IntakeHistoryListV2({
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
|
||||
const sourceLabels: Record<IntakeSource, string> = useMemo(
|
||||
() => ({
|
||||
WEB: t("medications.sourceWeb"),
|
||||
API: t("medications.sourceApi"),
|
||||
REMINDER: t("medications.sourceReminder"),
|
||||
IMPORT: t("medications.sourceImport"),
|
||||
}),
|
||||
const sourceLabels: Record<IntakeSourceValue, string> = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(INTAKE_SOURCE_LABEL_KEYS).map(([source, key]) => [
|
||||
source,
|
||||
t(key),
|
||||
]),
|
||||
) as Record<IntakeSourceValue, string>,
|
||||
[t],
|
||||
);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS } from "@/lib/i18n/source-labels";
|
||||
import { queryKeys, refetchInactiveDailyReads } from "@/lib/query-keys";
|
||||
import {
|
||||
DEFAULT_DEVICE_TYPE_PRIORITY,
|
||||
@@ -96,19 +97,6 @@ const METRIC_LABEL_KEYS: Record<SourcePriorityMetricKey, string> = {
|
||||
sleepDebt: "settings.sections.sources.metrics.sleepDebt",
|
||||
};
|
||||
|
||||
const SOURCE_LABEL_KEYS: Record<string, string> = {
|
||||
WITHINGS: "settings.sections.sources.sourceLabels.WITHINGS",
|
||||
APPLE_HEALTH: "settings.sections.sources.sourceLabels.APPLE_HEALTH",
|
||||
MANUAL: "settings.sections.sources.sourceLabels.MANUAL",
|
||||
IMPORT: "settings.sections.sources.sourceLabels.IMPORT",
|
||||
// v1.11.0 — WHOOP native source + the COMPUTED proxy (surfaces in the
|
||||
// `recovery` ladder for native-vs-derived ordering).
|
||||
WHOOP: "settings.sections.sources.sourceLabels.WHOOP",
|
||||
COMPUTED: "settings.sections.sources.sourceLabels.COMPUTED",
|
||||
// v1.12.0 — Fitbit/Pixel native source.
|
||||
FITBIT: "settings.sections.sources.sourceLabels.FITBIT",
|
||||
};
|
||||
|
||||
const DEVICE_TYPE_LABEL_KEYS: Record<DeviceType, string> = {
|
||||
watch: "settings.sections.sources.deviceLabels.watch",
|
||||
band: "settings.sections.sources.deviceLabels.band",
|
||||
@@ -355,7 +343,7 @@ export function SourcesSection() {
|
||||
{index + 1}.
|
||||
</span>
|
||||
<span className="flex-1 text-sm">
|
||||
{t(SOURCE_LABEL_KEYS[source] ?? source)}
|
||||
{t(MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS[source])}
|
||||
</span>
|
||||
{/* v1.4.27 R3d MB2 — stack up/down vertically on
|
||||
narrow viewports so each button keeps the
|
||||
|
||||
@@ -260,6 +260,9 @@ describe("i18n locale file integrity", () => {
|
||||
"medications.wizard.steps.step3.unit.g",
|
||||
// v1.11.0 — "WHOOP" is a brand name, identical across every locale.
|
||||
"settings.sections.sources.sourceLabels.WHOOP",
|
||||
// "MCP" is the protocol's own acronym — the same four letters in every
|
||||
// locale, exactly like the WHOOP entry above.
|
||||
"settings.sections.sources.sourceLabels.MCP",
|
||||
]);
|
||||
|
||||
it.each(ALL_LOCALES)("$locale locale has no empty values", ({ path }) => {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* One place that says which i18n key labels which provenance enum member.
|
||||
*
|
||||
* Three surfaces render a source to the user and each one carried its own
|
||||
* copy of the map: Settings → Sources, the sleep discrepancy tooltip, and
|
||||
* the measurement list (filter dropdown + table badge). Every copy was
|
||||
* written when the enum was shorter, and every copy fell through to the raw
|
||||
* SCREAMING_SNAKE value for the providers added afterwards — the settings
|
||||
* ladder listed `GOOGLE_HEALTH`, `OURA`, `POLAR` and `STRAVA` verbatim.
|
||||
*
|
||||
* The maps live here so a new provider is one edit, and they are keyed by
|
||||
* the enum itself so the edit is not optional: leaving a member out is a
|
||||
* type error, and `measurement-source-label-coverage.test.ts` proves the
|
||||
* keys resolve in the message bundle.
|
||||
*/
|
||||
import { measurementSourceEnum } from "@/lib/validations/measurement";
|
||||
|
||||
export type MeasurementSourceValue =
|
||||
(typeof measurementSourceEnum.options)[number];
|
||||
|
||||
/**
|
||||
* Settings → Sources ("Source priority by metric") and the sleep
|
||||
* source-discrepancy tooltip. Reads as a provenance name: "Manual entry",
|
||||
* "Computed", and the provider's own brand spelling for everything else.
|
||||
*/
|
||||
export const MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS: Record<
|
||||
MeasurementSourceValue,
|
||||
string
|
||||
> = {
|
||||
WITHINGS: "settings.sections.sources.sourceLabels.WITHINGS",
|
||||
APPLE_HEALTH: "settings.sections.sources.sourceLabels.APPLE_HEALTH",
|
||||
MANUAL: "settings.sections.sources.sourceLabels.MANUAL",
|
||||
IMPORT: "settings.sections.sources.sourceLabels.IMPORT",
|
||||
WHOOP: "settings.sections.sources.sourceLabels.WHOOP",
|
||||
COMPUTED: "settings.sections.sources.sourceLabels.COMPUTED",
|
||||
FITBIT: "settings.sections.sources.sourceLabels.FITBIT",
|
||||
NIGHTSCOUT: "settings.sections.sources.sourceLabels.NIGHTSCOUT",
|
||||
POLAR: "settings.sections.sources.sourceLabels.POLAR",
|
||||
OURA: "settings.sections.sources.sourceLabels.OURA",
|
||||
TELEGRAM: "settings.sections.sources.sourceLabels.TELEGRAM",
|
||||
MCP: "settings.sections.sources.sourceLabels.MCP",
|
||||
GOOGLE_HEALTH: "settings.sections.sources.sourceLabels.GOOGLE_HEALTH",
|
||||
STRAVA: "settings.sections.sources.sourceLabels.STRAVA",
|
||||
};
|
||||
|
||||
/**
|
||||
* The measurement list — the source facet in the filter rail and the badge
|
||||
* in the table's source column. Its own catalogue because the badge column
|
||||
* is narrow: "Manual" where the settings ladder says "Manual entry".
|
||||
*/
|
||||
export const MEASUREMENT_SOURCE_LIST_LABEL_KEYS: Record<
|
||||
MeasurementSourceValue,
|
||||
string
|
||||
> = {
|
||||
MANUAL: "measurements.sourceManual",
|
||||
WITHINGS: "measurements.sourceWithings",
|
||||
IMPORT: "measurements.sourceImport",
|
||||
APPLE_HEALTH: "measurements.sourceAppleHealth",
|
||||
COMPUTED: "measurements.sourceComputed",
|
||||
WHOOP: "measurements.sourceWhoop",
|
||||
FITBIT: "measurements.sourceFitbit",
|
||||
NIGHTSCOUT: "measurements.sourceNightscout",
|
||||
POLAR: "measurements.sourcePolar",
|
||||
OURA: "measurements.sourceOura",
|
||||
TELEGRAM: "measurements.sourceTelegram",
|
||||
MCP: "measurements.sourceMcp",
|
||||
GOOGLE_HEALTH: "measurements.sourceGoogleHealth",
|
||||
STRAVA: "measurements.sourceStrava",
|
||||
};
|
||||
|
||||
/**
|
||||
* `IntakeSource` — where a medication intake was logged. Not a Zod enum
|
||||
* anywhere (the write surfaces validate against narrower literals), so the
|
||||
* member list is written out and pinned against `prisma/schema.prisma` by
|
||||
* the coverage guard.
|
||||
*/
|
||||
export const INTAKE_SOURCE_VALUES = [
|
||||
"WEB",
|
||||
"API",
|
||||
"REMINDER",
|
||||
"IMPORT",
|
||||
"APPLE_HEALTH",
|
||||
] as const;
|
||||
|
||||
export type IntakeSourceValue = (typeof INTAKE_SOURCE_VALUES)[number];
|
||||
|
||||
/** The "via {origin}" caption on a medication intake row. */
|
||||
export const INTAKE_SOURCE_LABEL_KEYS: Record<IntakeSourceValue, string> = {
|
||||
WEB: "medications.sourceWeb",
|
||||
API: "medications.sourceApi",
|
||||
REMINDER: "medications.sourceReminder",
|
||||
IMPORT: "medications.sourceImport",
|
||||
APPLE_HEALTH: "medications.sourceAppleHealth",
|
||||
};
|
||||
Reference in new issue
Block a user