feat(coach): message catalog for the dialog in all seven languages

Adds every string the dialog needs up front: step labels, table titles,
columns and controls, the method line, the follow-up chips and their
setting, the clarification card, and the caption of an answer reused from
a stored table. A key catalog shared by server and client names each one;
a test holds the catalog and the bundles equal in both directions.
This commit is contained in:
Marc-André Bombeck committed 2026-09-27 10:50:49 +02:00
1 parent ad72ae3e4c
commit fe0a03761e
9 files changed
+1630 -7

No files matched your search

+174 -1
View File
@@ -9480,7 +9480,180 @@
"promptInjection": "In deiner Nachricht stehen Anweisungen, die meine Vorgaben überschreiben sollen. Ich bin der HealthLog-Coach und fasse ausschließlich deine eigenen Gesundheitsdaten zusammen — formuliere deine Frage bitte in diesem Rahmen neu.",
"conversationPoisoned": "Eine frühere Nachricht in dieser Unterhaltung enthält Anweisungen, die meine Vorgaben überschreiben sollen. Beginne bitte eine neue Unterhaltung."
},
"noRecordedData": "Zu deiner Frage sind bei dir keine Messwerte erfasst, also kann ich dir dazu keine Zahl nennen. Sobald du etwas eingetragen hast, frag mich noch einmal — dann rechne ich mit deinen eigenen Werten."
"noRecordedData": "Zu deiner Frage sind bei dir keine Messwerte erfasst, also kann ich dir dazu keine Zahl nennen. Sobald du etwas eingetragen hast, frag mich noch einmal — dann rechne ich mit deinen eigenen Werten.",
"step": {
"read": "Lese {domain}",
"readWindow": "Lese {domain}, {window}",
"snapshot": "Lese die Übersicht deiner Daten",
"showResult": "Öffne eine frühere Tabelle",
"reuse": "Nutze die Tabelle von vorhin",
"headerActive": "{label}…",
"headerDoneOne": "1 Quelle angesehen",
"headerDoneFew": "{count} Quellen angesehen",
"headerDoneOther": "{count} Quellen angesehen",
"listLabel": "Was der Coach angesehen hat",
"toggleShow": "Anzeigen, was der Coach angesehen hat",
"toggleHide": "Ausblenden, was der Coach angesehen hat",
"announceDone": "Fertig: {label}",
"readingsOne": "1 Messwert",
"readingsFew": "{count} Messwerte",
"readingsOther": "{count} Messwerte",
"rowsOne": "1 Zeile",
"rowsFew": "{count} Zeilen",
"rowsOther": "{count} Zeilen",
"metricsOne": "1 Messgröße",
"metricsFew": "{count} Messgrößen",
"metricsOther": "{count} Messgrößen",
"reason": {
"noData": "keine Messwerte",
"outsideWindow": "keine Messwerte in diesem Zeitraum",
"moduleDisabled": "ausgeschaltet",
"retrievalFailed": "konnte nicht gelesen werden",
"invalidArguments": "Anfrage nicht verstanden"
},
"domain": {
"labs": "Laborwerte",
"illness": "Krankheit und Erholung",
"cycle": "Zyklus",
"correlations": "Zusammenhänge zwischen Messgrößen",
"snapshot": "Datenübersicht"
},
"window": {
"last7days": "letzte 7 Tage",
"last30days": "letzte 30 Tage",
"last90days": "letzte 90 Tage",
"lastYear": "letzte 12 Monate",
"allTime": "gesamter Zeitraum"
},
"period": {
"current": "dieser Zeitraum",
"previous": "der Zeitraum davor",
"yearAgo": "ein Jahr zuvor"
},
"granularity": {
"day": "nach Tag",
"week": "nach Woche",
"month": "nach Monat"
}
},
"result": {
"title": {
"byDay": "{metric} nach Tag",
"byWeek": "{metric} nach Woche",
"byMonth": "{metric} nach Monat",
"previousPeriod": "{metric}, Zeitraum davor",
"yearAgo": "{metric}, ein Jahr zuvor",
"workoutsBySport": "Trainings nach Sportart",
"labsLatest": "Neueste Laborwerte",
"sleepByNight": "Schlaf nach Nacht",
"complianceByDay": "Medikamenten-Einnahmetreue nach Tag",
"distribution": "{metric}: wie oft welcher Bereich vorkam"
},
"column": {
"day": "Tag",
"week": "Woche",
"month": "Monat",
"night": "Nacht",
"sport": "Sportart",
"sessions": "Einheiten",
"analyte": "Laborwert",
"value": "Wert",
"unit": "Einheit",
"date": "Datum",
"referenceRange": "Referenzbereich",
"systolic": "Systolisch",
"diastolic": "Diastolisch",
"mean": "Durchschnitt",
"min": "Niedrigster",
"max": "Höchster",
"readings": "Messwerte",
"rate": "Einnahmetreue",
"duration": "Dauer",
"range": "Bereich",
"count": "Anzahl"
},
"other": "Sonstige",
"noReading": "kein Messwert",
"showAll": "Alle anzeigen ({count})",
"showFewer": "Weniger anzeigen",
"copyMenu": "Tabelle kopieren",
"copyForSpreadsheet": "Für Tabellenkalkulation kopieren",
"copyAsText": "Als Text kopieren",
"copied": "Kopiert",
"copyFailed": "Die Tabelle konnte nicht kopiert werden",
"viewLabel": "Ansicht",
"viewChart": "Diagramm",
"viewTable": "Tabelle",
"chartSummary": "{title} als Diagramm. Die Tabellenansicht listet jeden Wert auf.",
"dataUsed": "Verwendete Daten ({count})",
"truncated": "Die ersten {shown} von {total} Zeilen",
"withheld": {
"moduleDisabled": "Diese Tabelle ist ausgeblendet, weil ihr Modul ausgeschaltet ist.",
"unavailable": "Diese Tabelle konnte nicht geladen werden."
},
"loading": "Tabelle wird geladen…",
"loadFailed": "Die Tabellen konnten nicht geladen werden.",
"histogramBin": "{from}–{to} {unit}"
},
"method": {
"label": "Wie das ermittelt wurde",
"entry": "{domain}, {window}: {detail}",
"entryNoWindow": "{domain}: {detail}",
"readingsOne": "1 Messwert",
"readingsFew": "{count} Messwerte",
"readingsOther": "{count} Messwerte",
"aggregation": {
"mean": "Durchschnitt",
"median": "Median",
"latest": "letzter Messwert",
"sum": "Summe",
"count": "Anzahl",
"rate": "Quote"
},
"averages": {
"day": "Tagesdurchschnitte",
"week": "Wochendurchschnitte",
"month": "Monatsdurchschnitte"
},
"totals": {
"day": "Tagessummen",
"week": "Wochensummen",
"month": "Monatssummen"
},
"absent": {
"noData": "keine Messwerte",
"outsideWindow": "keine Messwerte in diesem Zeitraum",
"moduleDisabled": "ausgeschaltet"
}
},
"followUp": {
"groupLabel": "Vorgeschlagene Anschlussfragen",
"widenWindow": "Weiter zurückschauen",
"previousPeriod": "Mit dem Zeitraum davor vergleichen",
"yearAgo": "Mit dem Vorjahr vergleichen",
"asChart": "Als Diagramm zeigen",
"asTable": "Als Tabelle zeigen",
"relatedMetric": "Auch {metric} ansehen",
"continue": "Weitersuchen",
"settingLabel": "Anschlussfragen vorschlagen",
"settingHint": "Zeigt unter der letzten Antwort des Coaches bis zu drei Vorschläge für die nächste Frage."
},
"clarify": {
"cardLabel": "Der Coach hat eine Rückfrage",
"choicesLabel": "Mögliche Antworten",
"freeTextHint": "Du kannst auch selbst eine Antwort schreiben.",
"contextHint": "Schreib deine Antwort unten.",
"window": {
"last7days": "Letzte 7 Tage",
"last30days": "Letzte 30 Tage",
"last90days": "Letzte 90 Tage",
"lastYear": "Letzte 12 Monate",
"allTime": "Gesamter Zeitraum"
}
},
"reuse": {
"caption": "Hier sind dieselben Werte aus meiner letzten Antwort, unverändert."
}
},
"safety": {
"floor": {
+174 -1
View File
@@ -9480,7 +9480,180 @@
"promptInjection": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.",
"conversationPoisoned": "An earlier message in this conversation contains wording that overrides my instructions. Please start a new conversation."
},
"noRecordedData": "I don't have any recorded readings for what you asked about, so there is nothing I can give you a figure from. Once you log some, ask me again and I'll work from your own numbers."
"noRecordedData": "I don't have any recorded readings for what you asked about, so there is nothing I can give you a figure from. Once you log some, ask me again and I'll work from your own numbers.",
"step": {
"read": "Reading {domain}",
"readWindow": "Reading {domain}, {window}",
"snapshot": "Reading your record summary",
"showResult": "Opening an earlier table",
"reuse": "Using the table from before",
"headerActive": "{label}…",
"headerDoneOne": "Looked at 1 source",
"headerDoneFew": "Looked at {count} sources",
"headerDoneOther": "Looked at {count} sources",
"listLabel": "What the Coach looked at",
"toggleShow": "Show what the Coach looked at",
"toggleHide": "Hide what the Coach looked at",
"announceDone": "Done: {label}",
"readingsOne": "1 reading",
"readingsFew": "{count} readings",
"readingsOther": "{count} readings",
"rowsOne": "1 row",
"rowsFew": "{count} rows",
"rowsOther": "{count} rows",
"metricsOne": "1 metric",
"metricsFew": "{count} metrics",
"metricsOther": "{count} metrics",
"reason": {
"noData": "no readings",
"outsideWindow": "no readings in this window",
"moduleDisabled": "switched off",
"retrievalFailed": "couldn't be read",
"invalidArguments": "request not understood"
},
"domain": {
"labs": "Lab results",
"illness": "Illness and recovery",
"cycle": "Cycle",
"correlations": "Connections between metrics",
"snapshot": "Record summary"
},
"window": {
"last7days": "last 7 days",
"last30days": "last 30 days",
"last90days": "last 90 days",
"lastYear": "last 12 months",
"allTime": "all time"
},
"period": {
"current": "this period",
"previous": "the period before",
"yearAgo": "a year earlier"
},
"granularity": {
"day": "by day",
"week": "by week",
"month": "by month"
}
},
"result": {
"title": {
"byDay": "{metric} by day",
"byWeek": "{metric} by week",
"byMonth": "{metric} by month",
"previousPeriod": "{metric}, the period before",
"yearAgo": "{metric}, a year earlier",
"workoutsBySport": "Workouts by sport",
"labsLatest": "Latest lab results",
"sleepByNight": "Sleep by night",
"complianceByDay": "Medication adherence by day",
"distribution": "{metric}: how often each range came up"
},
"column": {
"day": "Day",
"week": "Week",
"month": "Month",
"night": "Night",
"sport": "Sport",
"sessions": "Sessions",
"analyte": "Test",
"value": "Value",
"unit": "Unit",
"date": "Date",
"referenceRange": "Reference range",
"systolic": "Systolic",
"diastolic": "Diastolic",
"mean": "Average",
"min": "Lowest",
"max": "Highest",
"readings": "Readings",
"rate": "Adherence",
"duration": "Duration",
"range": "Range",
"count": "Times"
},
"other": "Other",
"noReading": "no reading",
"showAll": "Show all ({count})",
"showFewer": "Show fewer",
"copyMenu": "Copy table",
"copyForSpreadsheet": "Copy for spreadsheet",
"copyAsText": "Copy as text",
"copied": "Copied",
"copyFailed": "Couldn't copy the table",
"viewLabel": "View",
"viewChart": "Chart",
"viewTable": "Table",
"chartSummary": "{title}, shown as a chart. The table view lists every value.",
"dataUsed": "Data used ({count})",
"truncated": "Showing the first {shown} of {total} rows",
"withheld": {
"moduleDisabled": "This table is hidden because its module is switched off.",
"unavailable": "This table couldn't be loaded."
},
"loading": "Loading table…",
"loadFailed": "Couldn't load the tables.",
"histogramBin": "{from}–{to} {unit}"
},
"method": {
"label": "How this was worked out",
"entry": "{domain}, {window}: {detail}",
"entryNoWindow": "{domain}: {detail}",
"readingsOne": "1 reading",
"readingsFew": "{count} readings",
"readingsOther": "{count} readings",
"aggregation": {
"mean": "average",
"median": "median",
"latest": "latest reading",
"sum": "total",
"count": "count",
"rate": "rate"
},
"averages": {
"day": "daily averages",
"week": "weekly averages",
"month": "monthly averages"
},
"totals": {
"day": "daily totals",
"week": "weekly totals",
"month": "monthly totals"
},
"absent": {
"noData": "no readings",
"outsideWindow": "no readings in this window",
"moduleDisabled": "switched off"
}
},
"followUp": {
"groupLabel": "Suggested follow-ups",
"widenWindow": "Look further back",
"previousPeriod": "Compare with the period before",
"yearAgo": "Compare with a year earlier",
"asChart": "Show as a chart",
"asTable": "Show as a table",
"relatedMetric": "Look at {metric} too",
"continue": "Keep looking",
"settingLabel": "Suggest follow-up questions",
"settingHint": "Show up to three follow-up suggestions under the Coach's latest answer."
},
"clarify": {
"cardLabel": "The Coach has a question",
"choicesLabel": "Possible answers",
"freeTextHint": "You can also type your own answer.",
"contextHint": "Type your answer below.",
"window": {
"last7days": "Last 7 days",
"last30days": "Last 30 days",
"last90days": "Last 90 days",
"lastYear": "Last 12 months",
"allTime": "All time"
}
},
"reuse": {
"caption": "Here are the same figures from my last answer, unchanged."
}
},
"safety": {
"floor": {
+174 -1
View File
@@ -9480,7 +9480,180 @@
"promptInjection": "He detectado instrucciones que intentan anular mis directrices. Soy el Coach de HealthLog y solo resumo tus propios datos de salud; reformula tu pregunta en esos términos, por favor.",
"conversationPoisoned": "Un mensaje anterior de esta conversación contiene instrucciones que intentan anular mis directrices. Empieza una conversación nueva, por favor."
},
"noRecordedData": "No tienes ninguna medición registrada sobre lo que preguntas, así que no puedo darte ninguna cifra. Cuando registres algo, vuelve a preguntarme y trabajaré con tus propios datos."
"noRecordedData": "No tienes ninguna medición registrada sobre lo que preguntas, así que no puedo darte ninguna cifra. Cuando registres algo, vuelve a preguntarme y trabajaré con tus propios datos.",
"step": {
"read": "Leyendo {domain}",
"readWindow": "Leyendo {domain}, {window}",
"snapshot": "Leyendo el resumen de tus datos",
"showResult": "Abriendo una tabla anterior",
"reuse": "Usando la tabla de antes",
"headerActive": "{label}…",
"headerDoneOne": "Consultó 1 fuente",
"headerDoneFew": "Consultó {count} fuentes",
"headerDoneOther": "Consultó {count} fuentes",
"listLabel": "Lo que consultó el Coach",
"toggleShow": "Mostrar lo que consultó el Coach",
"toggleHide": "Ocultar lo que consultó el Coach",
"announceDone": "Listo: {label}",
"readingsOne": "1 medición",
"readingsFew": "{count} mediciones",
"readingsOther": "{count} mediciones",
"rowsOne": "1 fila",
"rowsFew": "{count} filas",
"rowsOther": "{count} filas",
"metricsOne": "1 métrica",
"metricsFew": "{count} métricas",
"metricsOther": "{count} métricas",
"reason": {
"noData": "sin mediciones",
"outsideWindow": "sin mediciones en este periodo",
"moduleDisabled": "desactivado",
"retrievalFailed": "no se pudo leer",
"invalidArguments": "solicitud no entendida"
},
"domain": {
"labs": "Resultados de laboratorio",
"illness": "Enfermedad y recuperación",
"cycle": "Ciclo",
"correlations": "Relaciones entre métricas",
"snapshot": "Resumen de datos"
},
"window": {
"last7days": "últimos 7 días",
"last30days": "últimos 30 días",
"last90days": "últimos 90 días",
"lastYear": "últimos 12 meses",
"allTime": "todo el periodo"
},
"period": {
"current": "este periodo",
"previous": "el periodo anterior",
"yearAgo": "un año antes"
},
"granularity": {
"day": "por día",
"week": "por semana",
"month": "por mes"
}
},
"result": {
"title": {
"byDay": "{metric} por día",
"byWeek": "{metric} por semana",
"byMonth": "{metric} por mes",
"previousPeriod": "{metric}, periodo anterior",
"yearAgo": "{metric}, un año antes",
"workoutsBySport": "Entrenamientos por deporte",
"labsLatest": "Resultados de laboratorio más recientes",
"sleepByNight": "Sueño por noche",
"complianceByDay": "Adherencia a la medicación por día",
"distribution": "{metric}: con qué frecuencia apareció cada rango"
},
"column": {
"day": "Día",
"week": "Semana",
"month": "Mes",
"night": "Noche",
"sport": "Deporte",
"sessions": "Sesiones",
"analyte": "Prueba",
"value": "Valor",
"unit": "Unidad",
"date": "Fecha",
"referenceRange": "Rango de referencia",
"systolic": "Sistólica",
"diastolic": "Diastólica",
"mean": "Promedio",
"min": "Mínimo",
"max": "Máximo",
"readings": "Mediciones",
"rate": "Adherencia",
"duration": "Duración",
"range": "Rango",
"count": "Veces"
},
"other": "Otros",
"noReading": "sin medición",
"showAll": "Mostrar todo ({count})",
"showFewer": "Mostrar menos",
"copyMenu": "Copiar tabla",
"copyForSpreadsheet": "Copiar para hoja de cálculo",
"copyAsText": "Copiar como texto",
"copied": "Copiado",
"copyFailed": "No se pudo copiar la tabla",
"viewLabel": "Vista",
"viewChart": "Gráfico",
"viewTable": "Tabla",
"chartSummary": "{title}, mostrado como gráfico. La vista de tabla recoge cada valor.",
"dataUsed": "Datos usados ({count})",
"truncated": "Se muestran las primeras {shown} de {total} filas",
"withheld": {
"moduleDisabled": "Esta tabla está oculta porque su módulo está desactivado.",
"unavailable": "No se pudo cargar esta tabla."
},
"loading": "Cargando tabla…",
"loadFailed": "No se pudieron cargar las tablas.",
"histogramBin": "{from}–{to} {unit}"
},
"method": {
"label": "Cómo se obtuvo",
"entry": "{domain}, {window}: {detail}",
"entryNoWindow": "{domain}: {detail}",
"readingsOne": "1 medición",
"readingsFew": "{count} mediciones",
"readingsOther": "{count} mediciones",
"aggregation": {
"mean": "promedio",
"median": "mediana",
"latest": "última medición",
"sum": "suma",
"count": "recuento",
"rate": "tasa"
},
"averages": {
"day": "promedios diarios",
"week": "promedios semanales",
"month": "promedios mensuales"
},
"totals": {
"day": "totales diarios",
"week": "totales semanales",
"month": "totales mensuales"
},
"absent": {
"noData": "sin mediciones",
"outsideWindow": "sin mediciones en este periodo",
"moduleDisabled": "desactivado"
}
},
"followUp": {
"groupLabel": "Preguntas sugeridas",
"widenWindow": "Mirar más atrás",
"previousPeriod": "Comparar con el periodo anterior",
"yearAgo": "Comparar con un año antes",
"asChart": "Mostrar como gráfico",
"asTable": "Mostrar como tabla",
"relatedMetric": "Ver también {metric}",
"continue": "Seguir buscando",
"settingLabel": "Sugerir preguntas de seguimiento",
"settingHint": "Muestra hasta tres sugerencias bajo la última respuesta del Coach."
},
"clarify": {
"cardLabel": "El Coach tiene una pregunta",
"choicesLabel": "Respuestas posibles",
"freeTextHint": "También puedes escribir tu propia respuesta.",
"contextHint": "Escribe tu respuesta abajo.",
"window": {
"last7days": "Últimos 7 días",
"last30days": "Últimos 30 días",
"last90days": "Últimos 90 días",
"lastYear": "Últimos 12 meses",
"allTime": "Todo el periodo"
}
},
"reuse": {
"caption": "Aquí están las mismas cifras de mi última respuesta, sin cambios."
}
},
"safety": {
"floor": {
+174 -1
View File
@@ -9480,7 +9480,180 @@
"promptInjection": "J'ai repéré des instructions qui cherchent à contourner mes consignes. Je suis le Coach HealthLog et je résume uniquement tes propres données de santé — reformule ta question dans ce cadre, s'il te plaît.",
"conversationPoisoned": "Un message précédent de cette conversation contient des instructions qui cherchent à contourner mes consignes. Démarre une nouvelle conversation, s'il te plaît."
},
"noRecordedData": "Aucune mesure n'est enregistrée pour ce que tu demandes, je ne peux donc te donner aucun chiffre. Dès que tu en auras saisi, repose-moi la question et je partirai de tes propres valeurs."
"noRecordedData": "Aucune mesure n'est enregistrée pour ce que tu demandes, je ne peux donc te donner aucun chiffre. Dès que tu en auras saisi, repose-moi la question et je partirai de tes propres valeurs.",
"step": {
"read": "Lecture : {domain}",
"readWindow": "Lecture : {domain}, {window}",
"snapshot": "Lecture du résumé de tes données",
"showResult": "Ouverture d'un tableau précédent",
"reuse": "Reprise du tableau précédent",
"headerActive": "{label}…",
"headerDoneOne": "1 source consultée",
"headerDoneFew": "{count} sources consultées",
"headerDoneOther": "{count} sources consultées",
"listLabel": "Ce que le Coach a consulté",
"toggleShow": "Afficher ce que le Coach a consulté",
"toggleHide": "Masquer ce que le Coach a consulté",
"announceDone": "Terminé : {label}",
"readingsOne": "1 mesure",
"readingsFew": "{count} mesures",
"readingsOther": "{count} mesures",
"rowsOne": "1 ligne",
"rowsFew": "{count} lignes",
"rowsOther": "{count} lignes",
"metricsOne": "1 indicateur",
"metricsFew": "{count} indicateurs",
"metricsOther": "{count} indicateurs",
"reason": {
"noData": "aucune mesure",
"outsideWindow": "aucune mesure sur cette période",
"moduleDisabled": "désactivé",
"retrievalFailed": "lecture impossible",
"invalidArguments": "demande non comprise"
},
"domain": {
"labs": "Résultats d'analyses",
"illness": "Maladie et récupération",
"cycle": "Cycle",
"correlations": "Liens entre indicateurs",
"snapshot": "Résumé des données"
},
"window": {
"last7days": "7 derniers jours",
"last30days": "30 derniers jours",
"last90days": "90 derniers jours",
"lastYear": "12 derniers mois",
"allTime": "toute la période"
},
"period": {
"current": "cette période",
"previous": "la période précédente",
"yearAgo": "un an plus tôt"
},
"granularity": {
"day": "par jour",
"week": "par semaine",
"month": "par mois"
}
},
"result": {
"title": {
"byDay": "{metric} par jour",
"byWeek": "{metric} par semaine",
"byMonth": "{metric} par mois",
"previousPeriod": "{metric}, période précédente",
"yearAgo": "{metric}, un an plus tôt",
"workoutsBySport": "Entraînements par sport",
"labsLatest": "Derniers résultats d'analyses",
"sleepByNight": "Sommeil par nuit",
"complianceByDay": "Observance des médicaments par jour",
"distribution": "{metric} : fréquence de chaque plage"
},
"column": {
"day": "Jour",
"week": "Semaine",
"month": "Mois",
"night": "Nuit",
"sport": "Sport",
"sessions": "Séances",
"analyte": "Analyse",
"value": "Valeur",
"unit": "Unité",
"date": "Date",
"referenceRange": "Plage de référence",
"systolic": "Systolique",
"diastolic": "Diastolique",
"mean": "Moyenne",
"min": "Minimum",
"max": "Maximum",
"readings": "Mesures",
"rate": "Observance",
"duration": "Durée",
"range": "Plage",
"count": "Occurrences"
},
"other": "Autres",
"noReading": "aucune mesure",
"showAll": "Tout afficher ({count})",
"showFewer": "Afficher moins",
"copyMenu": "Copier le tableau",
"copyForSpreadsheet": "Copier pour un tableur",
"copyAsText": "Copier en texte",
"copied": "Copié",
"copyFailed": "Impossible de copier le tableau",
"viewLabel": "Affichage",
"viewChart": "Graphique",
"viewTable": "Tableau",
"chartSummary": "{title}, sous forme de graphique. La vue tableau liste chaque valeur.",
"dataUsed": "Données utilisées ({count})",
"truncated": "Affichage des {shown} premières lignes sur {total}",
"withheld": {
"moduleDisabled": "Ce tableau est masqué car son module est désactivé.",
"unavailable": "Impossible de charger ce tableau."
},
"loading": "Chargement du tableau…",
"loadFailed": "Impossible de charger les tableaux.",
"histogramBin": "{from}–{to} {unit}"
},
"method": {
"label": "Comment cela a été calculé",
"entry": "{domain}, {window} : {detail}",
"entryNoWindow": "{domain} : {detail}",
"readingsOne": "1 mesure",
"readingsFew": "{count} mesures",
"readingsOther": "{count} mesures",
"aggregation": {
"mean": "moyenne",
"median": "médiane",
"latest": "dernière mesure",
"sum": "somme",
"count": "nombre",
"rate": "taux"
},
"averages": {
"day": "moyennes journalières",
"week": "moyennes hebdomadaires",
"month": "moyennes mensuelles"
},
"totals": {
"day": "totaux journaliers",
"week": "totaux hebdomadaires",
"month": "totaux mensuels"
},
"absent": {
"noData": "aucune mesure",
"outsideWindow": "aucune mesure sur cette période",
"moduleDisabled": "désactivé"
}
},
"followUp": {
"groupLabel": "Questions suggérées",
"widenWindow": "Remonter plus loin",
"previousPeriod": "Comparer avec la période précédente",
"yearAgo": "Comparer avec l'an dernier",
"asChart": "Afficher en graphique",
"asTable": "Afficher en tableau",
"relatedMetric": "Regarder aussi {metric}",
"continue": "Continuer à chercher",
"settingLabel": "Suggérer des questions de suivi",
"settingHint": "Affiche jusqu'à trois suggestions sous la dernière réponse du Coach."
},
"clarify": {
"cardLabel": "Le Coach a une question",
"choicesLabel": "Réponses possibles",
"freeTextHint": "Tu peux aussi écrire ta propre réponse.",
"contextHint": "Écris ta réponse ci-dessous.",
"window": {
"last7days": "7 derniers jours",
"last30days": "30 derniers jours",
"last90days": "90 derniers jours",
"lastYear": "12 derniers mois",
"allTime": "Toute la période"
}
},
"reuse": {
"caption": "Voici les mêmes chiffres que dans ma dernière réponse, sans changement."
}
},
"safety": {
"floor": {
+174 -1
View File
@@ -9480,7 +9480,180 @@
"promptInjection": "Ho notato istruzioni che cercano di aggirare le mie direttive. Sono il Coach di HealthLog e riassumo soltanto i tuoi dati di salute — riformula la domanda in questi termini, per favore.",
"conversationPoisoned": "Un messaggio precedente di questa conversazione contiene istruzioni che cercano di aggirare le mie direttive. Inizia una nuova conversazione, per favore."
},
"noRecordedData": "Non hai misurazioni registrate su quello che chiedi, quindi non posso darti alcun valore. Appena ne inserisci qualcuna, richiedimelo e userò i tuoi dati."
"noRecordedData": "Non hai misurazioni registrate su quello che chiedi, quindi non posso darti alcun valore. Appena ne inserisci qualcuna, richiedimelo e userò i tuoi dati.",
"step": {
"read": "Lettura di {domain}",
"readWindow": "Lettura di {domain}, {window}",
"snapshot": "Lettura del riepilogo dei tuoi dati",
"showResult": "Apertura di una tabella precedente",
"reuse": "Uso della tabella di prima",
"headerActive": "{label}…",
"headerDoneOne": "1 fonte consultata",
"headerDoneFew": "{count} fonti consultate",
"headerDoneOther": "{count} fonti consultate",
"listLabel": "Cosa ha consultato il Coach",
"toggleShow": "Mostra cosa ha consultato il Coach",
"toggleHide": "Nascondi cosa ha consultato il Coach",
"announceDone": "Fatto: {label}",
"readingsOne": "1 misurazione",
"readingsFew": "{count} misurazioni",
"readingsOther": "{count} misurazioni",
"rowsOne": "1 riga",
"rowsFew": "{count} righe",
"rowsOther": "{count} righe",
"metricsOne": "1 parametro",
"metricsFew": "{count} parametri",
"metricsOther": "{count} parametri",
"reason": {
"noData": "nessuna misurazione",
"outsideWindow": "nessuna misurazione in questo periodo",
"moduleDisabled": "disattivato",
"retrievalFailed": "lettura non riuscita",
"invalidArguments": "richiesta non compresa"
},
"domain": {
"labs": "Esami di laboratorio",
"illness": "Malattia e recupero",
"cycle": "Ciclo",
"correlations": "Relazioni tra parametri",
"snapshot": "Riepilogo dei dati"
},
"window": {
"last7days": "ultimi 7 giorni",
"last30days": "ultimi 30 giorni",
"last90days": "ultimi 90 giorni",
"lastYear": "ultimi 12 mesi",
"allTime": "intero periodo"
},
"period": {
"current": "questo periodo",
"previous": "il periodo precedente",
"yearAgo": "un anno prima"
},
"granularity": {
"day": "per giorno",
"week": "per settimana",
"month": "per mese"
}
},
"result": {
"title": {
"byDay": "{metric} per giorno",
"byWeek": "{metric} per settimana",
"byMonth": "{metric} per mese",
"previousPeriod": "{metric}, periodo precedente",
"yearAgo": "{metric}, un anno prima",
"workoutsBySport": "Allenamenti per sport",
"labsLatest": "Esami di laboratorio più recenti",
"sleepByNight": "Sonno per notte",
"complianceByDay": "Aderenza ai farmaci per giorno",
"distribution": "{metric}: quanto spesso è comparso ogni intervallo"
},
"column": {
"day": "Giorno",
"week": "Settimana",
"month": "Mese",
"night": "Notte",
"sport": "Sport",
"sessions": "Sessioni",
"analyte": "Esame",
"value": "Valore",
"unit": "Unità",
"date": "Data",
"referenceRange": "Intervallo di riferimento",
"systolic": "Sistolica",
"diastolic": "Diastolica",
"mean": "Media",
"min": "Minimo",
"max": "Massimo",
"readings": "Misurazioni",
"rate": "Aderenza",
"duration": "Durata",
"range": "Intervallo",
"count": "Volte"
},
"other": "Altro",
"noReading": "nessuna misurazione",
"showAll": "Mostra tutto ({count})",
"showFewer": "Mostra meno",
"copyMenu": "Copia tabella",
"copyForSpreadsheet": "Copia per foglio di calcolo",
"copyAsText": "Copia come testo",
"copied": "Copiato",
"copyFailed": "Impossibile copiare la tabella",
"viewLabel": "Vista",
"viewChart": "Grafico",
"viewTable": "Tabella",
"chartSummary": "{title}, mostrato come grafico. La vista tabella elenca ogni valore.",
"dataUsed": "Dati usati ({count})",
"truncated": "Prime {shown} righe su {total}",
"withheld": {
"moduleDisabled": "Questa tabella è nascosta perché il suo modulo è disattivato.",
"unavailable": "Impossibile caricare questa tabella."
},
"loading": "Caricamento della tabella…",
"loadFailed": "Impossibile caricare le tabelle.",
"histogramBin": "{from}–{to} {unit}"
},
"method": {
"label": "Come è stato ricavato",
"entry": "{domain}, {window}: {detail}",
"entryNoWindow": "{domain}: {detail}",
"readingsOne": "1 misurazione",
"readingsFew": "{count} misurazioni",
"readingsOther": "{count} misurazioni",
"aggregation": {
"mean": "media",
"median": "mediana",
"latest": "ultima misurazione",
"sum": "somma",
"count": "conteggio",
"rate": "tasso"
},
"averages": {
"day": "medie giornaliere",
"week": "medie settimanali",
"month": "medie mensili"
},
"totals": {
"day": "totali giornalieri",
"week": "totali settimanali",
"month": "totali mensili"
},
"absent": {
"noData": "nessuna misurazione",
"outsideWindow": "nessuna misurazione in questo periodo",
"moduleDisabled": "disattivato"
}
},
"followUp": {
"groupLabel": "Domande suggerite",
"widenWindow": "Guarda più indietro",
"previousPeriod": "Confronta con il periodo precedente",
"yearAgo": "Confronta con un anno prima",
"asChart": "Mostra come grafico",
"asTable": "Mostra come tabella",
"relatedMetric": "Guarda anche {metric}",
"continue": "Continua a cercare",
"settingLabel": "Suggerisci domande di approfondimento",
"settingHint": "Mostra fino a tre suggerimenti sotto l'ultima risposta del Coach."
},
"clarify": {
"cardLabel": "Il Coach ha una domanda",
"choicesLabel": "Risposte possibili",
"freeTextHint": "Puoi anche scrivere una tua risposta.",
"contextHint": "Scrivi la tua risposta qui sotto.",
"window": {
"last7days": "Ultimi 7 giorni",
"last30days": "Ultimi 30 giorni",
"last90days": "Ultimi 90 giorni",
"lastYear": "Ultimi 12 mesi",
"allTime": "Intero periodo"
}
},
"reuse": {
"caption": "Ecco gli stessi valori della mia ultima risposta, invariati."
}
},
"safety": {
"floor": {
+174 -1
View File
@@ -9480,7 +9480,180 @@
"promptInjection": "제 지침을 무시하게 만들려는 표현이 보였어요. 저는 HealthLog 코치이고 본인의 건강 데이터만 정리해 드려요 — 그 범위에 맞게 다시 질문해 주세요.",
"conversationPoisoned": "이 대화의 앞선 메시지에 제 지침을 무시하게 만드는 표현이 있어요. 새 대화를 시작해 주세요."
},
"noRecordedData": "물어보신 내용에 대해 기록된 측정값이 없어서 수치를 알려드릴 수 없어요. 기록을 남기신 뒤에 다시 물어보시면 본인 수치로 답해 드릴게요."
"noRecordedData": "물어보신 내용에 대해 기록된 측정값이 없어서 수치를 알려드릴 수 없어요. 기록을 남기신 뒤에 다시 물어보시면 본인 수치로 답해 드릴게요.",
"step": {
"read": "{domain} 확인 중",
"readWindow": "{domain}, {window} 확인 중",
"snapshot": "기록 요약 확인 중",
"showResult": "이전 표 여는 중",
"reuse": "앞서 만든 표 사용 중",
"headerActive": "{label}…",
"headerDoneOne": "출처 1개 확인함",
"headerDoneFew": "출처 {count}개 확인함",
"headerDoneOther": "출처 {count}개 확인함",
"listLabel": "코치가 확인한 내용",
"toggleShow": "코치가 확인한 내용 보기",
"toggleHide": "코치가 확인한 내용 숨기기",
"announceDone": "완료: {label}",
"readingsOne": "측정값 1개",
"readingsFew": "측정값 {count}개",
"readingsOther": "측정값 {count}개",
"rowsOne": "1행",
"rowsFew": "{count}행",
"rowsOther": "{count}행",
"metricsOne": "지표 1개",
"metricsFew": "지표 {count}개",
"metricsOther": "지표 {count}개",
"reason": {
"noData": "측정값 없음",
"outsideWindow": "이 기간에는 측정값 없음",
"moduleDisabled": "꺼져 있음",
"retrievalFailed": "읽지 못함",
"invalidArguments": "요청을 이해하지 못함"
},
"domain": {
"labs": "검사 결과",
"illness": "질병과 회복",
"cycle": "생리 주기",
"correlations": "지표 간 연관성",
"snapshot": "기록 요약"
},
"window": {
"last7days": "최근 7일",
"last30days": "최근 30일",
"last90days": "최근 90일",
"lastYear": "최근 12개월",
"allTime": "전체 기간"
},
"period": {
"current": "이번 기간",
"previous": "이전 기간",
"yearAgo": "1년 전"
},
"granularity": {
"day": "일별",
"week": "주별",
"month": "월별"
}
},
"result": {
"title": {
"byDay": "일별 {metric}",
"byWeek": "주별 {metric}",
"byMonth": "월별 {metric}",
"previousPeriod": "{metric}, 이전 기간",
"yearAgo": "{metric}, 1년 전",
"workoutsBySport": "종목별 운동",
"labsLatest": "최근 검사 결과",
"sleepByNight": "밤별 수면",
"complianceByDay": "일별 복약 순응도",
"distribution": "{metric}: 구간별 빈도"
},
"column": {
"day": "날짜",
"week": "주",
"month": "월",
"night": "밤",
"sport": "종목",
"sessions": "횟수",
"analyte": "검사 항목",
"value": "값",
"unit": "단위",
"date": "일자",
"referenceRange": "참고 범위",
"systolic": "수축기",
"diastolic": "이완기",
"mean": "평균",
"min": "최저",
"max": "최고",
"readings": "측정 횟수",
"rate": "순응도",
"duration": "시간",
"range": "구간",
"count": "빈도"
},
"other": "기타",
"noReading": "측정값 없음",
"showAll": "모두 보기 ({count})",
"showFewer": "간단히 보기",
"copyMenu": "표 복사",
"copyForSpreadsheet": "스프레드시트용으로 복사",
"copyAsText": "텍스트로 복사",
"copied": "복사됨",
"copyFailed": "표를 복사하지 못했어요",
"viewLabel": "보기 방식",
"viewChart": "차트",
"viewTable": "표",
"chartSummary": "{title}을(를) 차트로 표시해요. 표 보기에서 모든 값을 확인할 수 있어요.",
"dataUsed": "사용한 데이터 ({count})",
"truncated": "전체 {total}행 중 처음 {shown}행 표시",
"withheld": {
"moduleDisabled": "해당 모듈이 꺼져 있어 이 표를 숨겼어요.",
"unavailable": "이 표를 불러오지 못했어요."
},
"loading": "표 불러오는 중…",
"loadFailed": "표를 불러오지 못했어요.",
"histogramBin": "{from}–{to} {unit}"
},
"method": {
"label": "산출 방법",
"entry": "{domain}, {window}: {detail}",
"entryNoWindow": "{domain}: {detail}",
"readingsOne": "측정값 1개",
"readingsFew": "측정값 {count}개",
"readingsOther": "측정값 {count}개",
"aggregation": {
"mean": "평균",
"median": "중앙값",
"latest": "최근 측정값",
"sum": "합계",
"count": "개수",
"rate": "비율"
},
"averages": {
"day": "일 평균",
"week": "주 평균",
"month": "월 평균"
},
"totals": {
"day": "일 합계",
"week": "주 합계",
"month": "월 합계"
},
"absent": {
"noData": "측정값 없음",
"outsideWindow": "이 기간에는 측정값 없음",
"moduleDisabled": "꺼져 있음"
}
},
"followUp": {
"groupLabel": "추천 후속 질문",
"widenWindow": "더 이전까지 보기",
"previousPeriod": "이전 기간과 비교",
"yearAgo": "1년 전과 비교",
"asChart": "차트로 보기",
"asTable": "표로 보기",
"relatedMetric": "{metric}도 보기",
"continue": "계속 찾아보기",
"settingLabel": "후속 질문 추천",
"settingHint": "코치의 가장 최근 답변 아래에 후속 질문을 최대 세 개까지 보여줘요."
},
"clarify": {
"cardLabel": "코치가 질문이 있어요",
"choicesLabel": "선택할 수 있는 답변",
"freeTextHint": "직접 답을 입력해도 돼요.",
"contextHint": "아래에 답을 입력해 주세요.",
"window": {
"last7days": "최근 7일",
"last30days": "최근 30일",
"last90days": "최근 90일",
"lastYear": "최근 12개월",
"allTime": "전체 기간"
}
},
"reuse": {
"caption": "지난 답변과 같은 수치를 그대로 보여드릴게요."
}
},
"safety": {
"floor": {
+174 -1
View File
@@ -9480,7 +9480,180 @@
"promptInjection": "Zauważyłem polecenia, które próbują obejść moje wytyczne. Jestem Coachem HealthLog i podsumowuję wyłącznie Twoje własne dane zdrowotne — sformułuj pytanie w tych ramach.",
"conversationPoisoned": "Wcześniejsza wiadomość w tej rozmowie zawiera polecenia, które próbują obejść moje wytyczne. Rozpocznij nową rozmowę."
},
"noRecordedData": "Nie masz zapisanych żadnych pomiarów dla tego, o co pytasz, więc nie mogę podać Ci żadnej liczby. Gdy coś zapiszesz, zapytaj ponownie — wtedy oprę się na Twoich własnych wartościach."
"noRecordedData": "Nie masz zapisanych żadnych pomiarów dla tego, o co pytasz, więc nie mogę podać Ci żadnej liczby. Gdy coś zapiszesz, zapytaj ponownie — wtedy oprę się na Twoich własnych wartościach.",
"step": {
"read": "Czytam: {domain}",
"readWindow": "Czytam: {domain}, {window}",
"snapshot": "Czytam podsumowanie Twoich danych",
"showResult": "Otwieram wcześniejszą tabelę",
"reuse": "Korzystam z tabeli z poprzedniej odpowiedzi",
"headerActive": "{label}…",
"headerDoneOne": "Sprawdzono 1 źródło",
"headerDoneFew": "Sprawdzono {count} źródła",
"headerDoneOther": "Sprawdzono {count} źródeł",
"listLabel": "Co sprawdził Coach",
"toggleShow": "Pokaż, co sprawdził Coach",
"toggleHide": "Ukryj, co sprawdził Coach",
"announceDone": "Gotowe: {label}",
"readingsOne": "1 pomiar",
"readingsFew": "{count} pomiary",
"readingsOther": "{count} pomiarów",
"rowsOne": "1 wiersz",
"rowsFew": "{count} wiersze",
"rowsOther": "{count} wierszy",
"metricsOne": "1 wskaźnik",
"metricsFew": "{count} wskaźniki",
"metricsOther": "{count} wskaźników",
"reason": {
"noData": "brak pomiarów",
"outsideWindow": "brak pomiarów w tym okresie",
"moduleDisabled": "wyłączone",
"retrievalFailed": "nie udało się odczytać",
"invalidArguments": "nie zrozumiano zapytania"
},
"domain": {
"labs": "Wyniki badań",
"illness": "Choroba i powrót do zdrowia",
"cycle": "Cykl",
"correlations": "Związki między wskaźnikami",
"snapshot": "Podsumowanie danych"
},
"window": {
"last7days": "ostatnie 7 dni",
"last30days": "ostatnie 30 dni",
"last90days": "ostatnie 90 dni",
"lastYear": "ostatnie 12 miesięcy",
"allTime": "cały okres"
},
"period": {
"current": "ten okres",
"previous": "poprzedni okres",
"yearAgo": "rok wcześniej"
},
"granularity": {
"day": "według dni",
"week": "według tygodni",
"month": "według miesięcy"
}
},
"result": {
"title": {
"byDay": "{metric} według dni",
"byWeek": "{metric} według tygodni",
"byMonth": "{metric} według miesięcy",
"previousPeriod": "{metric}, poprzedni okres",
"yearAgo": "{metric}, rok wcześniej",
"workoutsBySport": "Treningi według dyscypliny",
"labsLatest": "Najnowsze wyniki badań",
"sleepByNight": "Sen według nocy",
"complianceByDay": "Przyjmowanie leków według dni",
"distribution": "{metric}: jak często występował każdy zakres"
},
"column": {
"day": "Dzień",
"week": "Tydzień",
"month": "Miesiąc",
"night": "Noc",
"sport": "Dyscyplina",
"sessions": "Sesje",
"analyte": "Badanie",
"value": "Wartość",
"unit": "Jednostka",
"date": "Data",
"referenceRange": "Zakres referencyjny",
"systolic": "Skurczowe",
"diastolic": "Rozkurczowe",
"mean": "Średnia",
"min": "Najniższa",
"max": "Najwyższa",
"readings": "Pomiary",
"rate": "Regularność",
"duration": "Czas trwania",
"range": "Zakres",
"count": "Liczba razy"
},
"other": "Inne",
"noReading": "brak pomiaru",
"showAll": "Pokaż wszystko ({count})",
"showFewer": "Pokaż mniej",
"copyMenu": "Kopiuj tabelę",
"copyForSpreadsheet": "Kopiuj do arkusza kalkulacyjnego",
"copyAsText": "Kopiuj jako tekst",
"copied": "Skopiowano",
"copyFailed": "Nie udało się skopiować tabeli",
"viewLabel": "Widok",
"viewChart": "Wykres",
"viewTable": "Tabela",
"chartSummary": "{title} w formie wykresu. Widok tabeli pokazuje każdą wartość.",
"dataUsed": "Użyte dane ({count})",
"truncated": "Pierwsze {shown} z {total} wierszy",
"withheld": {
"moduleDisabled": "Ta tabela jest ukryta, bo jej moduł jest wyłączony.",
"unavailable": "Nie udało się wczytać tej tabeli."
},
"loading": "Wczytywanie tabeli…",
"loadFailed": "Nie udało się wczytać tabel.",
"histogramBin": "{from}–{to} {unit}"
},
"method": {
"label": "Jak to ustalono",
"entry": "{domain}, {window}: {detail}",
"entryNoWindow": "{domain}: {detail}",
"readingsOne": "1 pomiar",
"readingsFew": "{count} pomiary",
"readingsOther": "{count} pomiarów",
"aggregation": {
"mean": "średnia",
"median": "mediana",
"latest": "ostatni pomiar",
"sum": "suma",
"count": "liczba",
"rate": "odsetek"
},
"averages": {
"day": "średnie dzienne",
"week": "średnie tygodniowe",
"month": "średnie miesięczne"
},
"totals": {
"day": "sumy dzienne",
"week": "sumy tygodniowe",
"month": "sumy miesięczne"
},
"absent": {
"noData": "brak pomiarów",
"outsideWindow": "brak pomiarów w tym okresie",
"moduleDisabled": "wyłączone"
}
},
"followUp": {
"groupLabel": "Proponowane pytania",
"widenWindow": "Sięgnij dalej wstecz",
"previousPeriod": "Porównaj z poprzednim okresem",
"yearAgo": "Porównaj z tym samym okresem rok wcześniej",
"asChart": "Pokaż jako wykres",
"asTable": "Pokaż jako tabelę",
"relatedMetric": "Sprawdź też: {metric}",
"continue": "Szukaj dalej",
"settingLabel": "Proponuj kolejne pytania",
"settingHint": "Pokazuje do trzech propozycji pod ostatnią odpowiedzią Coacha."
},
"clarify": {
"cardLabel": "Coach ma pytanie",
"choicesLabel": "Możliwe odpowiedzi",
"freeTextHint": "Możesz też wpisać własną odpowiedź.",
"contextHint": "Wpisz odpowiedź poniżej.",
"window": {
"last7days": "Ostatnie 7 dni",
"last30days": "Ostatnie 30 dni",
"last90days": "Ostatnie 90 dni",
"lastYear": "Ostatnie 12 miesięcy",
"allTime": "Cały okres"
}
},
"reuse": {
"caption": "Oto te same wartości z mojej poprzedniej odpowiedzi, bez zmian."
}
},
"safety": {
"floor": {
@@ -0,0 +1,157 @@
/**
* The dialog's key catalog and the seven bundles agree, both ways.
*
* Forward: every key the catalog can produce — every label map entry, every
* domain, window, period and granularity, every plural tier — resolves in
* every locale. Reverse: every leaf under the namespaces the catalog owns
* (`coach.step`, `coach.result`, `coach.method`, `coach.followUp`,
* `coach.clarify`, `coach.reuse`) is one the catalog can produce, so a
* string cannot sit in the bundles with nothing able to reach it.
*
* Checked by breaking it: deleting `coach.result.copied` from `ko.json`
* fails the forward test by locale and key; adding an unused leaf under
* `coach.step` fails the reverse test by name.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { locales, type Locale } from "@/lib/i18n/config";
import {
coachScopeWindowSchema,
type CoachResultGranularity,
type CoachResultPeriod,
} from "@/lib/ai/coach/types";
import { coachStepDomainSchema } from "@/lib/ai/coach/stream-events";
import {
COACH_CLARIFY_UI_KEYS,
COACH_FOLLOW_UP_KEYS,
COACH_FOLLOW_UP_UI_KEYS,
COACH_METHOD_ABSENT_KEYS,
COACH_METHOD_AGGREGATION_KEYS,
COACH_METHOD_KEYS,
COACH_RESULT_COLUMN_KEYS,
COACH_RESULT_TITLE_KEYS,
COACH_RESULT_UI_KEYS,
COACH_RESULT_WITHHELD_KEYS,
COACH_REUSE_CAPTION_KEY,
COACH_STEP_LABEL_KEYS,
COACH_STEP_REASON_KEYS,
COACH_STEP_UI_KEYS,
clarifyWindowLabelKey,
coachDomainLabelKey,
coachGranularityLabelKey,
coachPeriodLabelKey,
coachWindowLabelKey,
methodAveragesKey,
methodReadingsKey,
methodTotalsKey,
stepHeaderDoneKey,
stepMetricsKey,
stepReadingsKey,
stepRowsKey,
} from "@/lib/ai/coach/dialog-keys";
const ROOT = join(__dirname, "../../../../..");
function bundle(locale: Locale): Record<string, unknown> {
return JSON.parse(
readFileSync(join(ROOT, "messages", `${locale}.json`), "utf8"),
) as Record<string, unknown>;
}
function leaves(node: unknown, prefix: string, out: Map<string, unknown>) {
if (node && typeof node === "object") {
for (const [key, value] of Object.entries(node)) {
leaves(value, prefix ? `${prefix}.${key}` : key, out);
}
} else {
out.set(prefix, node);
}
return out;
}
const PERIODS: CoachResultPeriod[] = ["current", "previous", "yearAgo"];
const GRANULARITIES: CoachResultGranularity[] = ["day", "week", "month"];
/** Counts that land on every plural tier some locale uses. */
const COUNTS = [0, 1, 2, 3, 5, 12, 22];
function catalogKeys(): Set<string> {
const keys = new Set<string>([
...Object.values(COACH_STEP_LABEL_KEYS),
...Object.values(COACH_STEP_UI_KEYS),
...Object.values(COACH_STEP_REASON_KEYS),
...Object.values(COACH_RESULT_TITLE_KEYS),
...Object.values(COACH_RESULT_COLUMN_KEYS),
...Object.values(COACH_RESULT_UI_KEYS),
...Object.values(COACH_RESULT_WITHHELD_KEYS),
...Object.values(COACH_METHOD_KEYS),
...Object.values(COACH_METHOD_AGGREGATION_KEYS),
...Object.values(COACH_METHOD_ABSENT_KEYS),
...Object.values(COACH_FOLLOW_UP_KEYS),
...Object.values(COACH_FOLLOW_UP_UI_KEYS),
...Object.values(COACH_CLARIFY_UI_KEYS),
COACH_REUSE_CAPTION_KEY,
]);
for (const domain of coachStepDomainSchema.options) {
keys.add(coachDomainLabelKey(domain));
}
for (const window of coachScopeWindowSchema.options) {
keys.add(coachWindowLabelKey(window));
keys.add(clarifyWindowLabelKey(window));
}
for (const period of PERIODS) {
keys.add(coachPeriodLabelKey(period));
}
for (const granularity of GRANULARITIES) {
keys.add(coachGranularityLabelKey(granularity));
keys.add(methodAveragesKey(granularity));
keys.add(methodTotalsKey(granularity));
}
for (const locale of locales) {
for (const count of COUNTS) {
keys.add(stepHeaderDoneKey(count, locale));
keys.add(stepReadingsKey(count, locale));
keys.add(stepRowsKey(count, locale));
keys.add(stepMetricsKey(count, locale));
keys.add(methodReadingsKey(count, locale));
}
}
return keys;
}
const OWNED = [
"coach.step",
"coach.result",
"coach.method",
"coach.followUp",
"coach.clarify",
"coach.reuse",
];
describe("Coach dialog keys", () => {
const keys = catalogKeys();
it("produces a non-trivial catalog", () => {
expect(keys.size).toBeGreaterThan(130);
});
it.each(locales)("every catalog key resolves in %s", (locale) => {
const strings = leaves(bundle(locale), "", new Map());
const missing = [...keys].filter(
(key) => typeof strings.get(key) !== "string" || strings.get(key) === "",
);
expect(missing).toEqual([]);
});
it("every leaf under the owned namespaces is reachable from the catalog", () => {
const strings = leaves(bundle("en"), "", new Map());
const owned = [...strings.keys()].filter((key) =>
OWNED.some((ns) => key.startsWith(`${ns}.`)),
);
expect(owned.length).toBeGreaterThan(0);
expect(owned.filter((key) => !keys.has(key))).toEqual([]);
});
});
+255
View File
@@ -0,0 +1,255 @@
/**
* The message keys of the Coach dialog: live steps, result tables, the
* method line, follow-up chips, clarifying questions, the reuse caption.
*
* One closed catalog, shared by the server (which renders every `label` in
* the request locale) and the web client (which renders `labelKey` when it
* has the string, and the server's `label` when it does not). A label never
* carries model text: it is one of these keys plus server-chosen values
* (a domain, a window, a count).
*
* Every key here exists in all seven bundles; `__tests__/dialog-keys.test.ts`
* holds that, in both directions for the namespaces this file owns.
*
* Client-safe: no server import.
*/
import type { Locale } from "@/lib/i18n/config";
import { pluralKey } from "@/lib/i18n/plural";
import {
coachScopeSourceSchema,
type CoachFollowUpKind,
type CoachMethodEntry,
type CoachResultGranularity,
type CoachResultPeriod,
type CoachScopeWindow,
type CoachStepDomain,
type CoachStepReason,
} from "@/lib/ai/coach/types";
// ── Steps ─────────────────────────────────────────────────────────────────
/** The step label templates (`{domain}`, `{window}`). */
export const COACH_STEP_LABEL_KEYS = {
read: "coach.step.read",
readWindow: "coach.step.readWindow",
snapshot: "coach.step.snapshot",
showResult: "coach.step.showResult",
reuse: "coach.step.reuse",
} as const;
/** The step list's own chrome. */
export const COACH_STEP_UI_KEYS = {
headerActive: "coach.step.headerActive",
listLabel: "coach.step.listLabel",
toggleShow: "coach.step.toggleShow",
toggleHide: "coach.step.toggleHide",
announceDone: "coach.step.announceDone",
} as const;
/** "Looked at N sources", by plural tier. */
export function stepHeaderDoneKey(count: number, locale: Locale): string {
return pluralKey("coach.step.headerDone", count, locale);
}
/** "N readings" on a step row, by plural tier. */
export function stepReadingsKey(count: number, locale: Locale): string {
return pluralKey("coach.step.readings", count, locale);
}
/** "N rows" on a step row, by plural tier. */
export function stepRowsKey(count: number, locale: Locale): string {
return pluralKey("coach.step.rows", count, locale);
}
/** "N metrics" on the snapshot step, by plural tier. */
export function stepMetricsKey(count: number, locale: Locale): string {
return pluralKey("coach.step.metrics", count, locale);
}
export const COACH_STEP_REASON_KEYS: Readonly<Record<CoachStepReason, string>> =
{
no_data: "coach.step.reason.noData",
outside_window: "coach.step.reason.outsideWindow",
module_disabled: "coach.step.reason.moduleDisabled",
retrieval_failed: "coach.step.reason.retrievalFailed",
invalid_arguments: "coach.step.reason.invalidArguments",
};
const SCOPE_SOURCES: ReadonlySet<string> = new Set(
coachScopeSourceSchema.options,
);
/**
* A domain's display name. A measurement-backed scope source reuses the
* metric names the sources rail already shows; the domains read as a whole
* have their own.
*/
export function coachDomainLabelKey(domain: CoachStepDomain): string {
return SCOPE_SOURCES.has(domain)
? `insights.coach.metric.${domain}`
: `coach.step.domain.${domain}`;
}
/** A window in running text ("last 30 days"). */
export function coachWindowLabelKey(window: CoachScopeWindow): string {
return `coach.step.window.${window}`;
}
export function coachPeriodLabelKey(period: CoachResultPeriod): string {
return `coach.step.period.${period}`;
}
export function coachGranularityLabelKey(
granularity: CoachResultGranularity,
): string {
return `coach.step.granularity.${granularity}`;
}
// ── Results ───────────────────────────────────────────────────────────────
/** Table titles; `{metric}` where the title names one. */
export const COACH_RESULT_TITLE_KEYS = {
byDay: "coach.result.title.byDay",
byWeek: "coach.result.title.byWeek",
byMonth: "coach.result.title.byMonth",
previousPeriod: "coach.result.title.previousPeriod",
yearAgo: "coach.result.title.yearAgo",
workoutsBySport: "coach.result.title.workoutsBySport",
labsLatest: "coach.result.title.labsLatest",
sleepByNight: "coach.result.title.sleepByNight",
complianceByDay: "coach.result.title.complianceByDay",
distribution: "coach.result.title.distribution",
} as const;
/** Column headings. */
export const COACH_RESULT_COLUMN_KEYS = {
day: "coach.result.column.day",
week: "coach.result.column.week",
month: "coach.result.column.month",
night: "coach.result.column.night",
sport: "coach.result.column.sport",
sessions: "coach.result.column.sessions",
analyte: "coach.result.column.analyte",
value: "coach.result.column.value",
unit: "coach.result.column.unit",
date: "coach.result.column.date",
referenceRange: "coach.result.column.referenceRange",
systolic: "coach.result.column.systolic",
diastolic: "coach.result.column.diastolic",
mean: "coach.result.column.mean",
min: "coach.result.column.min",
max: "coach.result.column.max",
readings: "coach.result.column.readings",
rate: "coach.result.column.rate",
duration: "coach.result.column.duration",
range: "coach.result.column.range",
count: "coach.result.column.count",
} as const;
/** The table's own chrome: paging, copy, the chart/table toggle, notices. */
export const COACH_RESULT_UI_KEYS = {
other: "coach.result.other",
noReading: "coach.result.noReading",
showAll: "coach.result.showAll",
showFewer: "coach.result.showFewer",
copyMenu: "coach.result.copyMenu",
copyForSpreadsheet: "coach.result.copyForSpreadsheet",
copyAsText: "coach.result.copyAsText",
copied: "coach.result.copied",
copyFailed: "coach.result.copyFailed",
viewLabel: "coach.result.viewLabel",
viewChart: "coach.result.viewChart",
viewTable: "coach.result.viewTable",
chartSummary: "coach.result.chartSummary",
dataUsed: "coach.result.dataUsed",
truncated: "coach.result.truncated",
loading: "coach.result.loading",
loadFailed: "coach.result.loadFailed",
histogramBin: "coach.result.histogramBin",
} as const;
/** Why a stored table is not shown. */
export const COACH_RESULT_WITHHELD_KEYS = {
module_disabled: "coach.result.withheld.moduleDisabled",
unavailable: "coach.result.withheld.unavailable",
} as const;
// ── Method ────────────────────────────────────────────────────────────────
export const COACH_METHOD_KEYS = {
label: "coach.method.label",
entry: "coach.method.entry",
entryNoWindow: "coach.method.entryNoWindow",
} as const;
/** "N readings" in the method line, by plural tier. */
export function methodReadingsKey(count: number, locale: Locale): string {
return pluralKey("coach.method.readings", count, locale);
}
export const COACH_METHOD_AGGREGATION_KEYS: Readonly<
Record<NonNullable<CoachMethodEntry["aggregation"]>, string>
> = {
mean: "coach.method.aggregation.mean",
median: "coach.method.aggregation.median",
latest: "coach.method.aggregation.latest",
sum: "coach.method.aggregation.sum",
count: "coach.method.aggregation.count",
rate: "coach.method.aggregation.rate",
};
/** "weekly averages" and "weekly totals", by granularity. */
export function methodAveragesKey(granularity: CoachResultGranularity): string {
return `coach.method.averages.${granularity}`;
}
export function methodTotalsKey(granularity: CoachResultGranularity): string {
return `coach.method.totals.${granularity}`;
}
export const COACH_METHOD_ABSENT_KEYS: Readonly<
Record<NonNullable<CoachMethodEntry["absent"]>, string>
> = {
no_data: "coach.method.absent.noData",
outside_window: "coach.method.absent.outsideWindow",
module_disabled: "coach.method.absent.moduleDisabled",
};
// ── Follow-ups ────────────────────────────────────────────────────────────
/** One label per chip kind; `{metric}` on a related-metric chip. */
export const COACH_FOLLOW_UP_KEYS: Readonly<Record<CoachFollowUpKind, string>> =
{
widen_window: "coach.followUp.widenWindow",
previous_period: "coach.followUp.previousPeriod",
year_ago: "coach.followUp.yearAgo",
as_chart: "coach.followUp.asChart",
as_table: "coach.followUp.asTable",
related_metric: "coach.followUp.relatedMetric",
continue: "coach.followUp.continue",
};
export const COACH_FOLLOW_UP_UI_KEYS = {
groupLabel: "coach.followUp.groupLabel",
settingLabel: "coach.followUp.settingLabel",
settingHint: "coach.followUp.settingHint",
} as const;
// ── Clarification ─────────────────────────────────────────────────────────
export const COACH_CLARIFY_UI_KEYS = {
cardLabel: "coach.clarify.cardLabel",
choicesLabel: "coach.clarify.choicesLabel",
freeTextHint: "coach.clarify.freeTextHint",
contextHint: "coach.clarify.contextHint",
} as const;
/** A window choice on a clarification card ("Last 30 days"). */
export function clarifyWindowLabelKey(window: CoachScopeWindow): string {
return `coach.clarify.window.${window}`;
}
// ── Reuse ─────────────────────────────────────────────────────────────────
/** The prose of a turn answered from a stored table without a model call. */
export const COACH_REUSE_CAPTION_KEY = "coach.reuse.caption";