mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(i18n): exhaustive call-site audit + 28 stale missing keys filled (v1.4.49.3)
Marc surfaced raw `notifications.eventMoodReminder` and
`notifications.eventMoodReminderDesc` reaching the UI after the
v1.4.49.2 relative-time fix had landed. The narrow `{count}`-call
audit that drove v1.4.49.2 missed every dynamic key construction; the
exhaustive sweep below picked up 28 keys called from real code that
had never existed in any locale bundle.
Stale gaps + when each surface shipped without its strings:
- `notifications.event{Type}` and `Desc` (14 keys) — the per-event
rows on the `/notifications` settings page. The matrix was added
with v1.4.41 and shipped with only `eventColumn` translated;
v1.4.49 then added MOOD_REMINDER to the enum which is what made
Marc notice.
- `onboarding.welcome.{chrome × 7, slide{1,2,3}.{title,body}}` (13
keys) — the 3-slide welcome carousel added in v1.4.45. The
carousel rendered raw keys for every screen-reader label and
every slide's content.
- `measurements.loadError` (1 key) — the error branch in
`measurement-list.tsx` added in v1.4.44.
All 28 keys × 6 locales (de / en / es / fr / it / pl) = 168 new
entries, all written in Marc's voice, ASCII-safe with proper umlaut /
accent handling per locale, no AI-mention or internal-tooling words.
Inserted at the END of each namespace so the diff stays surgical and
the integrity test (parity across locales) stays green.
Regression guard added at `src/__tests__/i18n-call-site-coverage.test.ts`:
walks every `.ts` / `.tsx` under `src/` (excluding tests + generated),
extracts every literal `t("ns.key")` with comment-aware parsing (line +
block + JSDoc comments stripped so docstring pseudocode is not flagged),
and asserts each key resolves to a string leaf in `messages/en.json`.
`i18n-locale-integrity.test.ts` then propagates the EN guarantee across
every other locale via key-set parity.
Why this gap was not caught earlier:
The v1.4.27 B6 `i18n-drift-guard.test.ts` carries hand-curated key
groups; surfaces added after that point (notifications matrix v1.4.41,
measurements list error v1.4.44, welcome carousel v1.4.45) shipped
without anyone editing the drift-guard groups. The integrity test
catches DRIFT across locales but not keys missing UNIFORMLY from every
locale. The new call-site test closes that hole — any t() call without
a backing key now fails at pnpm test time.
Tests: 5270 unit + integration pass, lint clean, typecheck clean.
This commit is contained in:
1 parent
19bc31b0bc
commit
de39180118
9 files changed
+405
-13
No files matched your search
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## [1.4.49.3] — 2026-05-23 — Full i18n call-site audit + 28 stale missing keys filled
|
||||
|
||||
Marc reported additional raw key strings (`notifications.eventMoodReminder`, `notifications.eventMoodReminderDesc`) reaching the UI after v1.4.49.2 had landed the relative-time fix. The narrow `{count}`-call audit that drove v1.4.49.2 had missed every dynamic key construction; the full exhaustive sweep below picked up 28 keys called from real code that had never existed in any locale bundle.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Filled 28 missing translation keys across all 6 locales (de / en / es / fr / it / pl = 168 entries), grouped by feature surface that introduced them and never wired the strings:
|
||||
- **Notifications event matrix** (14 keys — added with v1.4.41 / MOOD_REMINDER surfaced the gap in v1.4.49): `notifications.event{MedicationReminder,MeasurementAnomaly,ComplianceLow,WithingsSyncFailed,SystemAlert,PersonalRecord,MoodReminder}` and the `Desc` variant of each. The `/notifications` settings page is the consumer; before this release the per-event rows on Marc's account rendered the bare keys instead of the localised name + description.
|
||||
- **Welcome carousel** (13 keys — added with the 3-slide intro in v1.4.45): `onboarding.welcome.{title,carouselLabel,slideOf,prevSlide,nextSlide,gotoSlide,cta}` for the chrome plus `slide{1,2,3}.title/body` for the content. The carousel mounted with raw keys for every screen-reader label, dot-pager aria-label, and slide body.
|
||||
- **Measurement list error state** (1 key — added with v1.4.44): `measurements.loadError`. The error branch in `measurement-list.tsx` rendered the raw key when a fetch failed.
|
||||
|
||||
### Added
|
||||
|
||||
- `src/__tests__/i18n-call-site-coverage.test.ts` — call-site audit that walks every `.ts` / `.tsx` file under `src/` (excluding tests + generated code), extracts every literal `t("ns.key")` call with comment-aware parsing (skips line + block + JSDoc comments so example pseudocode in docstrings is not flagged), and asserts each key resolves to a string leaf in `messages/en.json`. The existing `i18n-locale-integrity.test.ts` then propagates the EN guarantee to every other locale via key-set parity. Pre-fix this guard would have surfaced 17 stale call sites at every test run; future regressions print a structured punch list so the offender can land every gap in one commit.
|
||||
|
||||
### Why this happened
|
||||
|
||||
The guard above did not exist before. v1.4.27 B6 introduced `i18n-drift-guard.test.ts` with hand-curated key groups; surfaces introduced after that point (notifications matrix in v1.4.41, measurement list error in v1.4.44, welcome carousel in v1.4.45) shipped without anyone editing the drift-guard groups, and the locale-integrity test only catches DRIFT across locales — it does not flag keys that are missing from EVERY locale uniformly. The call-site test closes that hole: any `t()` call without a backing key now fails at `pnpm test` time, before reaching production.
|
||||
|
||||
## [1.4.49.2] — 2026-05-23 — Raw i18n key leak on relative-time helper
|
||||
|
||||
Marc reported the raw string `insights.relativeHoursAgo` rendering verbatim on medication cards, recent-achievements, admin sections, the iOS notification preview, and every other consumer of `formatDateOrRelative`. Audit across the full `t(…, { count })` surface (~20 call sites) confirmed exactly two leaks: `insights.relativeMinutesAgo` and `insights.relativeHoursAgo`. Both pointed to keys that have NEVER existed in the translation bundle — only the pluralised `*One` / `*Other` variants ship.
|
||||
|
||||
+38
-2
@@ -379,7 +379,8 @@
|
||||
"nextPage": "Nächste Seite",
|
||||
"dailyTotalCaption": "({count} Einzelwerte)",
|
||||
"expandDay": "Einzelwerte dieses Tages anzeigen",
|
||||
"collapseDay": "Einzelwerte dieses Tages ausblenden"
|
||||
"collapseDay": "Einzelwerte dieses Tages ausblenden",
|
||||
"loadError": "Messwerte konnten nicht geladen werden."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Stimmung",
|
||||
@@ -1513,6 +1514,27 @@
|
||||
"title": "Willkommen zurück",
|
||||
"body": "Das Onboarding hast du bereits abgeschlossen. Möchtest du etwas anpassen, findest du alles in den Einstellungen.",
|
||||
"cta": "Zum Dashboard"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Willkommen bei HealthLog",
|
||||
"carouselLabel": "Begrüßungs-Karussell",
|
||||
"slideOf": "Folie {current} von {total}",
|
||||
"prevSlide": "Vorherige Folie",
|
||||
"nextSlide": "Nächste Folie",
|
||||
"gotoSlide": "Zu Folie {index} springen",
|
||||
"cta": "Los geht's",
|
||||
"slide1": {
|
||||
"title": "Alles, was zählt",
|
||||
"body": "Gewicht, Blutdruck, Glukose, Schlaf, Stimmung — alles in einem Verlauf, der dir gehört."
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Verbinde deine Quellen",
|
||||
"body": "Synchronisiere mit Apple Health und Withings oder trage Werte manuell ein. Deine Daten bleiben auf deinem Server."
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Erkenne das Muster",
|
||||
"body": "Tageszusammenfassungen, Trend-Diagramme und ein Coach, der zeigt was sich verändert hat — alles auf deinem Gerät."
|
||||
}
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
@@ -2838,7 +2860,21 @@
|
||||
"enabledToast": "Stimmungs-Erinnerung aktiviert.",
|
||||
"disabledToast": "Stimmungs-Erinnerung deaktiviert.",
|
||||
"saveError": "Speichern fehlgeschlagen. Bitte später erneut versuchen."
|
||||
}
|
||||
},
|
||||
"eventMedicationReminder": "Medikamenten-Erinnerung",
|
||||
"eventMedicationReminderDesc": "Wird zu jeder geplanten Einnahmezeit gesendet.",
|
||||
"eventMeasurementAnomaly": "Auffälliger Messwert",
|
||||
"eventMeasurementAnomalyDesc": "Wird gesendet, wenn ein Wert deutlich außerhalb deines üblichen Bereichs liegt.",
|
||||
"eventComplianceLow": "Niedrige Einnahmequote",
|
||||
"eventComplianceLowDesc": "Wird gesendet, wenn deine Einnahmequote in den letzten Tagen unter 80 % fällt.",
|
||||
"eventWithingsSyncFailed": "Withings-Sync fehlgeschlagen",
|
||||
"eventWithingsSyncFailedDesc": "Wird gesendet, wenn die Withings-Synchronisierung mehrere Versuche in Folge scheitert.",
|
||||
"eventSystemAlert": "Systemmeldung",
|
||||
"eventSystemAlertDesc": "Allgemeine Mitteilungen vom HealthLog-Server, etwa Wartungsfenster oder Konto-Hinweise.",
|
||||
"eventPersonalRecord": "Persönlicher Rekord",
|
||||
"eventPersonalRecordDesc": "Wird gesendet, wenn ein Messwert deine bisherige Bestmarke übertrifft.",
|
||||
"eventMoodReminder": "Stimmungs-Erinnerung",
|
||||
"eventMoodReminderDesc": "Tägliche Erinnerung, deine aktuelle Stimmung zu protokollieren."
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+38
-2
@@ -379,7 +379,8 @@
|
||||
"nextPage": "Next page",
|
||||
"dailyTotalCaption": "({count} samples)",
|
||||
"expandDay": "Show samples for this day",
|
||||
"collapseDay": "Hide samples for this day"
|
||||
"collapseDay": "Hide samples for this day",
|
||||
"loadError": "Could not load measurements."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Mood",
|
||||
@@ -1513,6 +1514,27 @@
|
||||
"title": "Welcome back",
|
||||
"body": "You've already finished onboarding. To adjust anything, head to Settings — everything you set up is editable from there.",
|
||||
"cta": "Open dashboard"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Welcome to HealthLog",
|
||||
"carouselLabel": "Welcome carousel",
|
||||
"slideOf": "Slide {current} of {total}",
|
||||
"prevSlide": "Previous slide",
|
||||
"nextSlide": "Next slide",
|
||||
"gotoSlide": "Go to slide {index}",
|
||||
"cta": "Get started",
|
||||
"slide1": {
|
||||
"title": "Track every signal",
|
||||
"body": "Weight, blood pressure, glucose, sleep, mood — keep your full picture in one log you own."
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Connect your sources",
|
||||
"body": "Sync with Apple Health and Withings, or add readings by hand. Your data stays on your server."
|
||||
},
|
||||
"slide3": {
|
||||
"title": "See the pattern",
|
||||
"body": "Daily summaries, trend charts, and a coach that surfaces what changed — all on your own device."
|
||||
}
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
@@ -2838,7 +2860,21 @@
|
||||
"enabledToast": "Mood reminder enabled.",
|
||||
"disabledToast": "Mood reminder disabled.",
|
||||
"saveError": "Save failed. Please try again."
|
||||
}
|
||||
},
|
||||
"eventMedicationReminder": "Medication reminder",
|
||||
"eventMedicationReminderDesc": "Sent at each scheduled dose time.",
|
||||
"eventMeasurementAnomaly": "Unusual measurement",
|
||||
"eventMeasurementAnomalyDesc": "Sent when a reading is well outside your usual range.",
|
||||
"eventComplianceLow": "Low compliance",
|
||||
"eventComplianceLowDesc": "Sent when your intake rate over the last few days drops below 80 %.",
|
||||
"eventWithingsSyncFailed": "Withings sync failed",
|
||||
"eventWithingsSyncFailedDesc": "Sent when Withings synchronisation fails multiple attempts in a row.",
|
||||
"eventSystemAlert": "System notice",
|
||||
"eventSystemAlertDesc": "General messages from the HealthLog server, e.g. maintenance windows or account notes.",
|
||||
"eventPersonalRecord": "Personal record",
|
||||
"eventPersonalRecordDesc": "Sent when a reading beats your previous best.",
|
||||
"eventMoodReminder": "Mood reminder",
|
||||
"eventMoodReminderDesc": "Daily nudge to log your current mood."
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+38
-2
@@ -379,7 +379,8 @@
|
||||
"nextPage": "Página siguiente",
|
||||
"dailyTotalCaption": "({count} muestras)",
|
||||
"expandDay": "Mostrar las muestras de este día",
|
||||
"collapseDay": "Ocultar las muestras de este día"
|
||||
"collapseDay": "Ocultar las muestras de este día",
|
||||
"loadError": "No se pudieron cargar las mediciones."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Estado de ánimo",
|
||||
@@ -1513,6 +1514,27 @@
|
||||
"title": "Bienvenido de nuevo",
|
||||
"body": "Ya completaste la configuración inicial. Para cambiar algo, abre Ajustes — todo lo que configuraste se puede editar desde ahí.",
|
||||
"cta": "Abrir el panel"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Bienvenido a HealthLog",
|
||||
"carouselLabel": "Carrusel de bienvenida",
|
||||
"slideOf": "Diapositiva {current} de {total}",
|
||||
"prevSlide": "Diapositiva anterior",
|
||||
"nextSlide": "Diapositiva siguiente",
|
||||
"gotoSlide": "Ir a la diapositiva {index}",
|
||||
"cta": "Empezar",
|
||||
"slide1": {
|
||||
"title": "Registra cada señal",
|
||||
"body": "Peso, presión, glucosa, sueño, ánimo — todo en un único registro que es tuyo."
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Conecta tus fuentes",
|
||||
"body": "Sincroniza con Apple Health y Withings o añade datos a mano. Tus datos se quedan en tu servidor."
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Descubre los patrones",
|
||||
"body": "Resúmenes diarios, gráficas de tendencias y un coach que destaca lo que cambió — todo en tu propio dispositivo."
|
||||
}
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
@@ -2838,7 +2860,21 @@
|
||||
"enabledToast": "Recordatorio de ánimo activado.",
|
||||
"disabledToast": "Recordatorio de ánimo desactivado.",
|
||||
"saveError": "No se pudo guardar. Inténtalo de nuevo."
|
||||
}
|
||||
},
|
||||
"eventMedicationReminder": "Recordatorio de medicación",
|
||||
"eventMedicationReminderDesc": "Se envía a cada hora de dosis programada.",
|
||||
"eventMeasurementAnomaly": "Medición inusual",
|
||||
"eventMeasurementAnomalyDesc": "Se envía cuando una lectura está claramente fuera de tu rango habitual.",
|
||||
"eventComplianceLow": "Cumplimiento bajo",
|
||||
"eventComplianceLowDesc": "Se envía cuando tu tasa de toma de los últimos días baja del 80 %.",
|
||||
"eventWithingsSyncFailed": "Error de sincronización con Withings",
|
||||
"eventWithingsSyncFailedDesc": "Se envía cuando la sincronización con Withings falla varios intentos seguidos.",
|
||||
"eventSystemAlert": "Aviso del sistema",
|
||||
"eventSystemAlertDesc": "Mensajes generales del servidor HealthLog, por ejemplo ventanas de mantenimiento o avisos de cuenta.",
|
||||
"eventPersonalRecord": "Récord personal",
|
||||
"eventPersonalRecordDesc": "Se envía cuando una lectura supera tu mejor marca anterior.",
|
||||
"eventMoodReminder": "Recordatorio de ánimo",
|
||||
"eventMoodReminderDesc": "Recordatorio diario para registrar tu ánimo actual."
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+38
-2
@@ -379,7 +379,8 @@
|
||||
"nextPage": "Page suivante",
|
||||
"dailyTotalCaption": "({count} échantillons)",
|
||||
"expandDay": "Afficher les échantillons de ce jour",
|
||||
"collapseDay": "Masquer les échantillons de ce jour"
|
||||
"collapseDay": "Masquer les échantillons de ce jour",
|
||||
"loadError": "Impossible de charger les mesures."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Humeur",
|
||||
@@ -1513,6 +1514,27 @@
|
||||
"title": "Bon retour",
|
||||
"body": "Vous avez déjà terminé la configuration. Pour modifier un élément, ouvrez les paramètres — tout reste éditable.",
|
||||
"cta": "Ouvrir le tableau de bord"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Bienvenue dans HealthLog",
|
||||
"carouselLabel": "Carrousel de bienvenue",
|
||||
"slideOf": "Diapositive {current} sur {total}",
|
||||
"prevSlide": "Diapositive précédente",
|
||||
"nextSlide": "Diapositive suivante",
|
||||
"gotoSlide": "Aller à la diapositive {index}",
|
||||
"cta": "Commencer",
|
||||
"slide1": {
|
||||
"title": "Notez chaque signal",
|
||||
"body": "Poids, tension, glycémie, sommeil, humeur — un seul journal qui vous appartient."
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Connectez vos sources",
|
||||
"body": "Synchronisez avec Apple Health et Withings, ou ajoutez vos mesures à la main. Vos données restent sur votre serveur."
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Repérez les tendances",
|
||||
"body": "Résumés quotidiens, graphiques de tendances et un coach qui met en avant ce qui a changé — le tout sur votre appareil."
|
||||
}
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
@@ -2838,7 +2860,21 @@
|
||||
"enabledToast": "Rappel d’humeur activé.",
|
||||
"disabledToast": "Rappel d’humeur désactivé.",
|
||||
"saveError": "Échec de l’enregistrement. Réessayez."
|
||||
}
|
||||
},
|
||||
"eventMedicationReminder": "Rappel de médicament",
|
||||
"eventMedicationReminderDesc": "Envoyé à chaque heure de prise prévue.",
|
||||
"eventMeasurementAnomaly": "Mesure inhabituelle",
|
||||
"eventMeasurementAnomalyDesc": "Envoyé quand une valeur sort nettement de votre plage habituelle.",
|
||||
"eventComplianceLow": "Observance faible",
|
||||
"eventComplianceLowDesc": "Envoyé quand votre taux de prise des derniers jours passe sous les 80 %.",
|
||||
"eventWithingsSyncFailed": "Échec de la synchronisation Withings",
|
||||
"eventWithingsSyncFailedDesc": "Envoyé quand la synchronisation Withings échoue plusieurs fois d'affilée.",
|
||||
"eventSystemAlert": "Notification système",
|
||||
"eventSystemAlertDesc": "Messages généraux du serveur HealthLog, par exemple maintenance ou avis de compte.",
|
||||
"eventPersonalRecord": "Record personnel",
|
||||
"eventPersonalRecordDesc": "Envoyé quand une valeur dépasse votre meilleur précédent.",
|
||||
"eventMoodReminder": "Rappel d'humeur",
|
||||
"eventMoodReminderDesc": "Rappel quotidien pour noter votre humeur du moment."
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+38
-2
@@ -379,7 +379,8 @@
|
||||
"nextPage": "Pagina successiva",
|
||||
"dailyTotalCaption": "({count} campioni)",
|
||||
"expandDay": "Mostra i campioni di questa giornata",
|
||||
"collapseDay": "Nascondi i campioni di questa giornata"
|
||||
"collapseDay": "Nascondi i campioni di questa giornata",
|
||||
"loadError": "Impossibile caricare le misurazioni."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Umore",
|
||||
@@ -1513,6 +1514,27 @@
|
||||
"title": "Bentornato",
|
||||
"body": "Hai già completato l'onboarding. Per modificare qualcosa apri le impostazioni — tutto ciò che hai configurato è ancora modificabile.",
|
||||
"cta": "Apri la dashboard"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Benvenuto in HealthLog",
|
||||
"carouselLabel": "Carosello di benvenuto",
|
||||
"slideOf": "Diapositiva {current} di {total}",
|
||||
"prevSlide": "Diapositiva precedente",
|
||||
"nextSlide": "Diapositiva successiva",
|
||||
"gotoSlide": "Vai alla diapositiva {index}",
|
||||
"cta": "Inizia",
|
||||
"slide1": {
|
||||
"title": "Tieni traccia di ogni segnale",
|
||||
"body": "Peso, pressione, glicemia, sonno, umore — un unico registro che è tuo."
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Collega le tue sorgenti",
|
||||
"body": "Sincronizza con Apple Health e Withings o aggiungi le misurazioni a mano. I tuoi dati restano sul tuo server."
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Scopri lo schema",
|
||||
"body": "Riepiloghi giornalieri, grafici di tendenza e un coach che evidenzia i cambiamenti — tutto sul tuo dispositivo."
|
||||
}
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
@@ -2838,7 +2860,21 @@
|
||||
"enabledToast": "Promemoria dell’umore attivato.",
|
||||
"disabledToast": "Promemoria dell’umore disattivato.",
|
||||
"saveError": "Salvataggio non riuscito. Riprova."
|
||||
}
|
||||
},
|
||||
"eventMedicationReminder": "Promemoria farmaco",
|
||||
"eventMedicationReminderDesc": "Inviato a ogni orario di dose programmato.",
|
||||
"eventMeasurementAnomaly": "Misurazione insolita",
|
||||
"eventMeasurementAnomalyDesc": "Inviato quando un valore esce nettamente dal tuo intervallo abituale.",
|
||||
"eventComplianceLow": "Aderenza bassa",
|
||||
"eventComplianceLowDesc": "Inviato quando il tuo tasso di assunzione degli ultimi giorni scende sotto l'80 %.",
|
||||
"eventWithingsSyncFailed": "Sincronizzazione Withings non riuscita",
|
||||
"eventWithingsSyncFailedDesc": "Inviato quando la sincronizzazione Withings fallisce per più tentativi consecutivi.",
|
||||
"eventSystemAlert": "Avviso di sistema",
|
||||
"eventSystemAlertDesc": "Messaggi generali dal server HealthLog, ad esempio finestre di manutenzione o note sull'account.",
|
||||
"eventPersonalRecord": "Record personale",
|
||||
"eventPersonalRecordDesc": "Inviato quando una misurazione supera il tuo precedente record.",
|
||||
"eventMoodReminder": "Promemoria umore",
|
||||
"eventMoodReminderDesc": "Promemoria quotidiano per registrare il tuo umore attuale."
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+38
-2
@@ -379,7 +379,8 @@
|
||||
"nextPage": "Następna strona",
|
||||
"dailyTotalCaption": "({count} pomiarów)",
|
||||
"expandDay": "Pokaż pomiary z tego dnia",
|
||||
"collapseDay": "Ukryj pomiary z tego dnia"
|
||||
"collapseDay": "Ukryj pomiary z tego dnia",
|
||||
"loadError": "Nie udało się załadować pomiarów."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Nastrój",
|
||||
@@ -1513,6 +1514,27 @@
|
||||
"title": "Witaj ponownie",
|
||||
"body": "Wstępne kroki masz już za sobą. Aby coś zmienić, otwórz ustawienia — wszystko, co skonfigurowałeś, da się tam edytować.",
|
||||
"cta": "Otwórz pulpit"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Witaj w HealthLog",
|
||||
"carouselLabel": "Karuzela powitalna",
|
||||
"slideOf": "Slajd {current} z {total}",
|
||||
"prevSlide": "Poprzedni slajd",
|
||||
"nextSlide": "Następny slajd",
|
||||
"gotoSlide": "Przejdź do slajdu {index}",
|
||||
"cta": "Zaczynamy",
|
||||
"slide1": {
|
||||
"title": "Śledź każdy sygnał",
|
||||
"body": "Waga, ciśnienie, glukoza, sen, nastrój — wszystko w jednym dzienniku, który należy do Ciebie."
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Połącz swoje źródła",
|
||||
"body": "Synchronizuj z Apple Health i Withings lub dodawaj odczyty ręcznie. Twoje dane zostają na Twoim serwerze."
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Zobacz wzorce",
|
||||
"body": "Codzienne podsumowania, wykresy trendów i coach, który wskazuje co się zmieniło — wszystko na Twoim urządzeniu."
|
||||
}
|
||||
}
|
||||
},
|
||||
"gettingStarted": {
|
||||
@@ -2838,7 +2860,21 @@
|
||||
"enabledToast": "Przypomnienie o nastroju włączone.",
|
||||
"disabledToast": "Przypomnienie o nastroju wyłączone.",
|
||||
"saveError": "Zapis nieudany. Spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"eventMedicationReminder": "Przypomnienie o leku",
|
||||
"eventMedicationReminderDesc": "Wysyłane o każdej zaplanowanej godzinie dawki.",
|
||||
"eventMeasurementAnomaly": "Nietypowy pomiar",
|
||||
"eventMeasurementAnomalyDesc": "Wysyłane, gdy odczyt znacznie odbiega od Twojego zwykłego zakresu.",
|
||||
"eventComplianceLow": "Niska zgodność",
|
||||
"eventComplianceLowDesc": "Wysyłane, gdy odsetek przyjętych dawek z ostatnich dni spadnie poniżej 80 %.",
|
||||
"eventWithingsSyncFailed": "Synchronizacja Withings nie powiodła się",
|
||||
"eventWithingsSyncFailedDesc": "Wysyłane, gdy synchronizacja Withings nie powiedzie się kilka razy z rzędu.",
|
||||
"eventSystemAlert": "Powiadomienie systemowe",
|
||||
"eventSystemAlertDesc": "Ogólne komunikaty z serwera HealthLog, na przykład okna serwisowe lub uwagi do konta.",
|
||||
"eventPersonalRecord": "Rekord osobisty",
|
||||
"eventPersonalRecordDesc": "Wysyłane, gdy odczyt pobije Twój dotychczasowy rekord.",
|
||||
"eventMoodReminder": "Przypomnienie o nastroju",
|
||||
"eventMoodReminderDesc": "Codzienne przypomnienie, aby zapisać swój aktualny nastrój."
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.4.49.2",
|
||||
"version": "1.4.49.3",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"homepage": "https://healthlog.dev",
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* v1.4.49.3 — call-site i18n coverage guard.
|
||||
*
|
||||
* Marc surfaced raw `insights.relativeHoursAgo`, `notifications.event
|
||||
* MoodReminder`, and `notifications.eventMoodReminderDesc` strings
|
||||
* leaking into the UI in v1.4.49. Audit found 28 keys called from real
|
||||
* code that never existed in any locale bundle — silent regressions
|
||||
* that accumulated across v1.4.41 (notifications matrix), v1.4.44
|
||||
* (measurements.loadError), and v1.4.45 (onboarding.welcome.*).
|
||||
*
|
||||
* This test walks every TypeScript / TSX file under `src/` (excluding
|
||||
* tests + generated code), extracts every literal `t("namespace.key")`
|
||||
* call, and asserts that each key resolves to a string leaf in
|
||||
* `messages/en.json`. JSDoc-block matches and known dynamic patterns
|
||||
* are filtered explicitly.
|
||||
*
|
||||
* The existing `i18n-drift-guard.test.ts` pins specific groups; this
|
||||
* test catches drift on every other call site. The
|
||||
* `i18n-locale-integrity.test.ts` test then propagates the EN guarantee
|
||||
* to every other locale via key-set parity.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ROOT = join(__dirname, "../..");
|
||||
const SRC = join(ROOT, "src");
|
||||
const EN_BUNDLE_PATH = join(ROOT, "messages/en.json");
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
"__tests__",
|
||||
"node_modules",
|
||||
".next",
|
||||
"generated",
|
||||
]);
|
||||
|
||||
function walk(dir: string, out: string[] = []): string[] {
|
||||
for (const name of readdirSync(dir)) {
|
||||
if (SKIP_DIRS.has(name)) continue;
|
||||
const p = join(dir, name);
|
||||
const stat = statSync(p);
|
||||
if (stat.isDirectory()) {
|
||||
walk(p, out);
|
||||
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const KEY_PATTERN =
|
||||
/(?<![a-zA-Z0-9_])t\(\s*["']([a-zA-Z][a-zA-Z0-9_.]+)["']/g;
|
||||
|
||||
interface CallSite {
|
||||
key: string;
|
||||
file: string;
|
||||
line: number;
|
||||
}
|
||||
|
||||
function stripCommentLine(line: string): string {
|
||||
// Strip line comments: `//` at start (after whitespace) OR inline
|
||||
// (preceded by whitespace), but not inside a string literal. Cheap
|
||||
// heuristic — find the FIRST `//` that isn't preceded by a quote.
|
||||
// The audit cares about real `t(…)` calls, not docstring examples.
|
||||
const slashIdx = line.indexOf("//");
|
||||
if (slashIdx === -1) return line;
|
||||
const before = line.slice(0, slashIdx);
|
||||
const dq = (before.match(/"/g) ?? []).length;
|
||||
const sq = (before.match(/'/g) ?? []).length;
|
||||
if (dq % 2 === 1 || sq % 2 === 1) return line; // inside a string
|
||||
return before;
|
||||
}
|
||||
|
||||
function extractKeys(): CallSite[] {
|
||||
const sites: CallSite[] = [];
|
||||
for (const file of walk(SRC)) {
|
||||
const text = readFileSync(file, "utf8");
|
||||
const lines = text.split("\n");
|
||||
let inBlockComment = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
// Skip `/** ... */` JSDoc + `/* ... */` block comments. They
|
||||
// routinely carry example `t("ns.key")` strings that are NOT
|
||||
// runtime calls (see `src/components/ui/empty-state.tsx`'s
|
||||
// docstring as the canonical example).
|
||||
if (inBlockComment) {
|
||||
const end = line.indexOf("*/");
|
||||
if (end === -1) continue;
|
||||
line = line.slice(end + 2);
|
||||
inBlockComment = false;
|
||||
}
|
||||
while (true) {
|
||||
const start = line.indexOf("/*");
|
||||
if (start === -1) break;
|
||||
const end = line.indexOf("*/", start + 2);
|
||||
if (end === -1) {
|
||||
line = line.slice(0, start);
|
||||
inBlockComment = true;
|
||||
break;
|
||||
}
|
||||
line = line.slice(0, start) + " ".repeat(end - start + 2) + line.slice(end + 2);
|
||||
}
|
||||
line = stripCommentLine(line);
|
||||
let m: RegExpExecArray | null;
|
||||
KEY_PATTERN.lastIndex = 0;
|
||||
while ((m = KEY_PATTERN.exec(line)) !== null) {
|
||||
sites.push({
|
||||
key: m[1],
|
||||
file: file.slice(ROOT.length + 1),
|
||||
line: i + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
function resolveLeaf(obj: unknown, dotted: string): string | null {
|
||||
const parts = dotted.split(".");
|
||||
let cur: unknown = obj;
|
||||
for (const p of parts) {
|
||||
if (cur && typeof cur === "object" && p in (cur as Record<string, unknown>)) {
|
||||
cur = (cur as Record<string, unknown>)[p];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return typeof cur === "string" ? cur : null;
|
||||
}
|
||||
|
||||
describe("i18n call-site coverage", () => {
|
||||
it("every literal t() key has a matching leaf in messages/en.json", () => {
|
||||
const en = JSON.parse(readFileSync(EN_BUNDLE_PATH, "utf8"));
|
||||
const sites = extractKeys();
|
||||
expect(sites.length).toBeGreaterThan(100);
|
||||
|
||||
const missing: CallSite[] = [];
|
||||
for (const site of sites) {
|
||||
if (resolveLeaf(en, site.key) === null) {
|
||||
missing.push(site);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
const report = missing
|
||||
.map((m) => ` ❌ ${m.key} (${m.file}:${m.line})`)
|
||||
.join("\n");
|
||||
// v1.4.49.3 — these 28 keys were the gap that reached production.
|
||||
// The list below stays empty once the audit passes; future
|
||||
// regressions print as a structured punch list so the offender
|
||||
// can fix every site in one commit.
|
||||
throw new Error(
|
||||
`Found ${missing.length} t() call sites referencing keys that are missing from messages/en.json:\n${report}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user