mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
feat(mood): the closed context vocabulary and its six locales
- one module holds the four sections, their key lists and the rating fields; the Zod enums, the capture UI and the analytics all read it, so a value the vocabulary does not name cannot be stored - labels state facts, never judgements, and the contact circles are groups of people rather than a score - a pair guard binds the arrays to all six bundles in both directions, because the reverse-coverage matcher sees a `mood.context.` template and stops being able to tell a live label from a stale one
This commit is contained in:
1 parent
7a4ee19c68
commit
dbfc0d8145
8 files changed
+1005
No files matched your search
@@ -922,6 +922,115 @@
|
||||
"low": "vollständig überfordert",
|
||||
"high": "sehr handlungsfähig"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"section": {
|
||||
"work": "Arbeit",
|
||||
"contacts": "Kontakte",
|
||||
"leisure": "Freizeit",
|
||||
"events": "Ereignis"
|
||||
},
|
||||
"field": {
|
||||
"workStatus": "Arbeitstag",
|
||||
"workMinutes": "Arbeitszeit",
|
||||
"overtimeMinutes": "Überstunden",
|
||||
"workLoad": "Arbeitsbelastung",
|
||||
"workSatisfaction": "Zufriedenheit mit der Arbeit",
|
||||
"contactCircles": "Mit wem Kontakt bestand",
|
||||
"contactForm": "Art des Kontakts",
|
||||
"contactExtent": "Dauer",
|
||||
"contactQuality": "Qualität des Kontakts",
|
||||
"contactSupport": "Empfundene Unterstützung",
|
||||
"leisureCategories": "Womit die Freizeit verbracht wurde",
|
||||
"leisureMinutes": "Zeit für dich",
|
||||
"leisureJoy": "Freude",
|
||||
"leisureRecovery": "Erholung",
|
||||
"eventType": "Was passiert ist",
|
||||
"eventValence": "Wie es angekommen ist",
|
||||
"eventAt": "Wann",
|
||||
"notes": "Notiz"
|
||||
},
|
||||
"workStatus": {
|
||||
"off": "Nicht gearbeitet",
|
||||
"partial": "Teilweise gearbeitet",
|
||||
"regular": "Regulär gearbeitet",
|
||||
"overtime": "Länger als üblich"
|
||||
},
|
||||
"contactCircles": {
|
||||
"partner": "Partnerschaft",
|
||||
"child": "Kind",
|
||||
"family": "Familie",
|
||||
"friends": "Freundeskreis",
|
||||
"colleagues": "Kollegium",
|
||||
"acquaintances": "Bekannte",
|
||||
"professional": "Professionelle Begleitung",
|
||||
"other": "Sonstige"
|
||||
},
|
||||
"contactForm": {
|
||||
"inPerson": "Persönlich",
|
||||
"phone": "Telefon",
|
||||
"video": "Videoanruf",
|
||||
"written": "Schriftlich"
|
||||
},
|
||||
"contactExtent": {
|
||||
"brief": "Kurz",
|
||||
"medium": "Mittel",
|
||||
"extended": "Ausgedehnt"
|
||||
},
|
||||
"leisureCategories": {
|
||||
"film": "Film oder Serie",
|
||||
"reading": "Lesen",
|
||||
"gaming": "Spielen",
|
||||
"music": "Musik",
|
||||
"creative": "Kreatives",
|
||||
"outing": "Unterwegs",
|
||||
"garden": "Garten",
|
||||
"cooking": "Kochen",
|
||||
"timeWithChild": "Zeit mit dem Kind",
|
||||
"relaxation": "Ausruhen",
|
||||
"other": "Sonstiges"
|
||||
},
|
||||
"eventType": {
|
||||
"goodNews": "Gute Nachricht",
|
||||
"badNews": "Schlechte Nachricht",
|
||||
"success": "Etwas ist gelungen",
|
||||
"disappointment": "Eine Enttäuschung",
|
||||
"conflict": "Ein Konflikt",
|
||||
"appointment": "Ein Termin",
|
||||
"familyEvent": "Ein Familienereignis",
|
||||
"change": "Eine Veränderung",
|
||||
"unexpected": "Etwas Unerwartetes"
|
||||
},
|
||||
"rating": {
|
||||
"workLoad": {
|
||||
"low": "kein Druck",
|
||||
"high": "unter starkem Druck"
|
||||
},
|
||||
"workSatisfaction": {
|
||||
"low": "gar nicht zufrieden",
|
||||
"high": "sehr zufrieden"
|
||||
},
|
||||
"contactQuality": {
|
||||
"low": "hat nicht gutgetan",
|
||||
"high": "hat sehr gutgetan"
|
||||
},
|
||||
"contactSupport": {
|
||||
"low": "nicht unterstützt",
|
||||
"high": "gut unterstützt"
|
||||
},
|
||||
"leisureJoy": {
|
||||
"low": "keine Freude",
|
||||
"high": "viel Freude"
|
||||
},
|
||||
"leisureRecovery": {
|
||||
"low": "nicht erholsam",
|
||||
"high": "sehr erholsam"
|
||||
},
|
||||
"eventValence": {
|
||||
"low": "deutlich schlecht",
|
||||
"high": "deutlich gut"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"medications": {
|
||||
|
||||
@@ -922,6 +922,115 @@
|
||||
"low": "completely overwhelmed",
|
||||
"high": "fully able to act"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"section": {
|
||||
"work": "Work",
|
||||
"contacts": "Contact",
|
||||
"leisure": "Free time",
|
||||
"events": "Event"
|
||||
},
|
||||
"field": {
|
||||
"workStatus": "Working day",
|
||||
"workMinutes": "Time worked",
|
||||
"overtimeMinutes": "Overtime",
|
||||
"workLoad": "Workload",
|
||||
"workSatisfaction": "Satisfaction with the work",
|
||||
"contactCircles": "Who the contact was with",
|
||||
"contactForm": "Form of contact",
|
||||
"contactExtent": "How long it lasted",
|
||||
"contactQuality": "Quality of the contact",
|
||||
"contactSupport": "Support felt",
|
||||
"leisureCategories": "What the free time went into",
|
||||
"leisureMinutes": "Time for yourself",
|
||||
"leisureJoy": "Enjoyment",
|
||||
"leisureRecovery": "Recovery",
|
||||
"eventType": "What happened",
|
||||
"eventValence": "How it landed",
|
||||
"eventAt": "When",
|
||||
"notes": "Note"
|
||||
},
|
||||
"workStatus": {
|
||||
"off": "Not working",
|
||||
"partial": "Part of the day",
|
||||
"regular": "A regular day",
|
||||
"overtime": "Longer than usual"
|
||||
},
|
||||
"contactCircles": {
|
||||
"partner": "Partner",
|
||||
"child": "Child",
|
||||
"family": "Family",
|
||||
"friends": "Friends",
|
||||
"colleagues": "Colleagues",
|
||||
"acquaintances": "Acquaintances",
|
||||
"professional": "Professional support",
|
||||
"other": "Others"
|
||||
},
|
||||
"contactForm": {
|
||||
"inPerson": "In person",
|
||||
"phone": "Phone",
|
||||
"video": "Video call",
|
||||
"written": "Written"
|
||||
},
|
||||
"contactExtent": {
|
||||
"brief": "Brief",
|
||||
"medium": "Medium",
|
||||
"extended": "Extended"
|
||||
},
|
||||
"leisureCategories": {
|
||||
"film": "Film or series",
|
||||
"reading": "Reading",
|
||||
"gaming": "Gaming",
|
||||
"music": "Music",
|
||||
"creative": "Something creative",
|
||||
"outing": "Out and about",
|
||||
"garden": "Garden",
|
||||
"cooking": "Cooking",
|
||||
"timeWithChild": "Time with the child",
|
||||
"relaxation": "Rest",
|
||||
"other": "Something else"
|
||||
},
|
||||
"eventType": {
|
||||
"goodNews": "Good news",
|
||||
"badNews": "Bad news",
|
||||
"success": "Something worked out",
|
||||
"disappointment": "A disappointment",
|
||||
"conflict": "A conflict",
|
||||
"appointment": "An appointment",
|
||||
"familyEvent": "A family occasion",
|
||||
"change": "A change",
|
||||
"unexpected": "Something unexpected"
|
||||
},
|
||||
"rating": {
|
||||
"workLoad": {
|
||||
"low": "no pressure",
|
||||
"high": "under heavy pressure"
|
||||
},
|
||||
"workSatisfaction": {
|
||||
"low": "not satisfied at all",
|
||||
"high": "very satisfied"
|
||||
},
|
||||
"contactQuality": {
|
||||
"low": "did not do me good",
|
||||
"high": "did me a lot of good"
|
||||
},
|
||||
"contactSupport": {
|
||||
"low": "not supported",
|
||||
"high": "well supported"
|
||||
},
|
||||
"leisureJoy": {
|
||||
"low": "no enjoyment",
|
||||
"high": "a lot of enjoyment"
|
||||
},
|
||||
"leisureRecovery": {
|
||||
"low": "not restorative",
|
||||
"high": "very restorative"
|
||||
},
|
||||
"eventValence": {
|
||||
"low": "clearly bad",
|
||||
"high": "clearly good"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"medications": {
|
||||
|
||||
@@ -922,6 +922,115 @@
|
||||
"low": "completamente desbordado",
|
||||
"high": "con plena capacidad de acción"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"section": {
|
||||
"work": "Trabajo",
|
||||
"contacts": "Contactos",
|
||||
"leisure": "Tiempo libre",
|
||||
"events": "Acontecimiento"
|
||||
},
|
||||
"field": {
|
||||
"workStatus": "Jornada laboral",
|
||||
"workMinutes": "Tiempo trabajado",
|
||||
"overtimeMinutes": "Horas extra",
|
||||
"workLoad": "Carga de trabajo",
|
||||
"workSatisfaction": "Satisfacción con el trabajo",
|
||||
"contactCircles": "Con quién hubo contacto",
|
||||
"contactForm": "Forma de contacto",
|
||||
"contactExtent": "Duración",
|
||||
"contactQuality": "Calidad del contacto",
|
||||
"contactSupport": "Apoyo percibido",
|
||||
"leisureCategories": "En qué se empleó el tiempo libre",
|
||||
"leisureMinutes": "Tiempo para ti",
|
||||
"leisureJoy": "Disfrute",
|
||||
"leisureRecovery": "Recuperación",
|
||||
"eventType": "Qué ocurrió",
|
||||
"eventValence": "Cómo sentó",
|
||||
"eventAt": "Cuándo",
|
||||
"notes": "Nota"
|
||||
},
|
||||
"workStatus": {
|
||||
"off": "Sin trabajar",
|
||||
"partial": "Parte del día",
|
||||
"regular": "Jornada normal",
|
||||
"overtime": "Más de lo habitual"
|
||||
},
|
||||
"contactCircles": {
|
||||
"partner": "Pareja",
|
||||
"child": "Hijo o hija",
|
||||
"family": "Familia",
|
||||
"friends": "Amistades",
|
||||
"colleagues": "Colegas",
|
||||
"acquaintances": "Conocidos",
|
||||
"professional": "Apoyo profesional",
|
||||
"other": "Otras personas"
|
||||
},
|
||||
"contactForm": {
|
||||
"inPerson": "En persona",
|
||||
"phone": "Teléfono",
|
||||
"video": "Videollamada",
|
||||
"written": "Por escrito"
|
||||
},
|
||||
"contactExtent": {
|
||||
"brief": "Breve",
|
||||
"medium": "Medio",
|
||||
"extended": "Prolongado"
|
||||
},
|
||||
"leisureCategories": {
|
||||
"film": "Cine o series",
|
||||
"reading": "Lectura",
|
||||
"gaming": "Videojuegos",
|
||||
"music": "Música",
|
||||
"creative": "Algo creativo",
|
||||
"outing": "Salida",
|
||||
"garden": "Jardín",
|
||||
"cooking": "Cocinar",
|
||||
"timeWithChild": "Tiempo con el hijo o la hija",
|
||||
"relaxation": "Descanso",
|
||||
"other": "Otra cosa"
|
||||
},
|
||||
"eventType": {
|
||||
"goodNews": "Buenas noticias",
|
||||
"badNews": "Malas noticias",
|
||||
"success": "Algo salió bien",
|
||||
"disappointment": "Una decepción",
|
||||
"conflict": "Un conflicto",
|
||||
"appointment": "Una cita",
|
||||
"familyEvent": "Un acontecimiento familiar",
|
||||
"change": "Un cambio",
|
||||
"unexpected": "Algo inesperado"
|
||||
},
|
||||
"rating": {
|
||||
"workLoad": {
|
||||
"low": "sin presión",
|
||||
"high": "bajo mucha presión"
|
||||
},
|
||||
"workSatisfaction": {
|
||||
"low": "nada satisfecho",
|
||||
"high": "muy satisfecho"
|
||||
},
|
||||
"contactQuality": {
|
||||
"low": "no sentó bien",
|
||||
"high": "sentó muy bien"
|
||||
},
|
||||
"contactSupport": {
|
||||
"low": "sin apoyo",
|
||||
"high": "muy apoyado"
|
||||
},
|
||||
"leisureJoy": {
|
||||
"low": "sin disfrute",
|
||||
"high": "mucho disfrute"
|
||||
},
|
||||
"leisureRecovery": {
|
||||
"low": "nada reparador",
|
||||
"high": "muy reparador"
|
||||
},
|
||||
"eventValence": {
|
||||
"low": "claramente malo",
|
||||
"high": "claramente bueno"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"medications": {
|
||||
|
||||
@@ -922,6 +922,115 @@
|
||||
"low": "complètement dépassé",
|
||||
"high": "pleinement capable d'agir"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"section": {
|
||||
"work": "Travail",
|
||||
"contacts": "Contacts",
|
||||
"leisure": "Temps libre",
|
||||
"events": "Événement"
|
||||
},
|
||||
"field": {
|
||||
"workStatus": "Journée de travail",
|
||||
"workMinutes": "Temps travaillé",
|
||||
"overtimeMinutes": "Heures supplémentaires",
|
||||
"workLoad": "Charge de travail",
|
||||
"workSatisfaction": "Satisfaction au travail",
|
||||
"contactCircles": "Avec qui le contact a eu lieu",
|
||||
"contactForm": "Forme du contact",
|
||||
"contactExtent": "Durée",
|
||||
"contactQuality": "Qualité du contact",
|
||||
"contactSupport": "Soutien ressenti",
|
||||
"leisureCategories": "À quoi le temps libre a servi",
|
||||
"leisureMinutes": "Temps pour soi",
|
||||
"leisureJoy": "Plaisir",
|
||||
"leisureRecovery": "Récupération",
|
||||
"eventType": "Ce qui s'est passé",
|
||||
"eventValence": "Comment cela a été vécu",
|
||||
"eventAt": "Quand",
|
||||
"notes": "Note"
|
||||
},
|
||||
"workStatus": {
|
||||
"off": "Pas de travail",
|
||||
"partial": "Une partie de la journée",
|
||||
"regular": "Journée normale",
|
||||
"overtime": "Plus longtemps que d'habitude"
|
||||
},
|
||||
"contactCircles": {
|
||||
"partner": "Partenaire",
|
||||
"child": "Enfant",
|
||||
"family": "Famille",
|
||||
"friends": "Amis",
|
||||
"colleagues": "Collègues",
|
||||
"acquaintances": "Connaissances",
|
||||
"professional": "Accompagnement professionnel",
|
||||
"other": "Autres"
|
||||
},
|
||||
"contactForm": {
|
||||
"inPerson": "En personne",
|
||||
"phone": "Téléphone",
|
||||
"video": "Appel vidéo",
|
||||
"written": "Par écrit"
|
||||
},
|
||||
"contactExtent": {
|
||||
"brief": "Bref",
|
||||
"medium": "Moyen",
|
||||
"extended": "Prolongé"
|
||||
},
|
||||
"leisureCategories": {
|
||||
"film": "Film ou série",
|
||||
"reading": "Lecture",
|
||||
"gaming": "Jeux vidéo",
|
||||
"music": "Musique",
|
||||
"creative": "Activité créative",
|
||||
"outing": "Sortie",
|
||||
"garden": "Jardin",
|
||||
"cooking": "Cuisine",
|
||||
"timeWithChild": "Temps avec l'enfant",
|
||||
"relaxation": "Repos",
|
||||
"other": "Autre chose"
|
||||
},
|
||||
"eventType": {
|
||||
"goodNews": "Bonne nouvelle",
|
||||
"badNews": "Mauvaise nouvelle",
|
||||
"success": "Quelque chose a réussi",
|
||||
"disappointment": "Une déception",
|
||||
"conflict": "Un conflit",
|
||||
"appointment": "Un rendez-vous",
|
||||
"familyEvent": "Un événement familial",
|
||||
"change": "Un changement",
|
||||
"unexpected": "Quelque chose d'inattendu"
|
||||
},
|
||||
"rating": {
|
||||
"workLoad": {
|
||||
"low": "aucune pression",
|
||||
"high": "sous forte pression"
|
||||
},
|
||||
"workSatisfaction": {
|
||||
"low": "pas du tout satisfait",
|
||||
"high": "très satisfait"
|
||||
},
|
||||
"contactQuality": {
|
||||
"low": "ne m'a pas fait de bien",
|
||||
"high": "m'a fait beaucoup de bien"
|
||||
},
|
||||
"contactSupport": {
|
||||
"low": "pas soutenu",
|
||||
"high": "bien soutenu"
|
||||
},
|
||||
"leisureJoy": {
|
||||
"low": "aucun plaisir",
|
||||
"high": "beaucoup de plaisir"
|
||||
},
|
||||
"leisureRecovery": {
|
||||
"low": "pas reposant",
|
||||
"high": "très reposant"
|
||||
},
|
||||
"eventValence": {
|
||||
"low": "nettement négatif",
|
||||
"high": "nettement positif"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"medications": {
|
||||
|
||||
@@ -922,6 +922,115 @@
|
||||
"low": "completamente sopraffatto",
|
||||
"high": "pienamente capace di agire"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"section": {
|
||||
"work": "Lavoro",
|
||||
"contacts": "Contatti",
|
||||
"leisure": "Tempo libero",
|
||||
"events": "Evento"
|
||||
},
|
||||
"field": {
|
||||
"workStatus": "Giornata lavorativa",
|
||||
"workMinutes": "Tempo lavorato",
|
||||
"overtimeMinutes": "Straordinari",
|
||||
"workLoad": "Carico di lavoro",
|
||||
"workSatisfaction": "Soddisfazione per il lavoro",
|
||||
"contactCircles": "Con chi c'è stato contatto",
|
||||
"contactForm": "Forma del contatto",
|
||||
"contactExtent": "Durata",
|
||||
"contactQuality": "Qualità del contatto",
|
||||
"contactSupport": "Sostegno percepito",
|
||||
"leisureCategories": "Come è stato speso il tempo libero",
|
||||
"leisureMinutes": "Tempo per sé",
|
||||
"leisureJoy": "Piacere",
|
||||
"leisureRecovery": "Recupero",
|
||||
"eventType": "Che cosa è successo",
|
||||
"eventValence": "Come è stato vissuto",
|
||||
"eventAt": "Quando",
|
||||
"notes": "Nota"
|
||||
},
|
||||
"workStatus": {
|
||||
"off": "Nessun lavoro",
|
||||
"partial": "Parte della giornata",
|
||||
"regular": "Giornata regolare",
|
||||
"overtime": "Più del solito"
|
||||
},
|
||||
"contactCircles": {
|
||||
"partner": "Partner",
|
||||
"child": "Figlio o figlia",
|
||||
"family": "Famiglia",
|
||||
"friends": "Amici",
|
||||
"colleagues": "Colleghi",
|
||||
"acquaintances": "Conoscenti",
|
||||
"professional": "Supporto professionale",
|
||||
"other": "Altre persone"
|
||||
},
|
||||
"contactForm": {
|
||||
"inPerson": "Di persona",
|
||||
"phone": "Telefono",
|
||||
"video": "Videochiamata",
|
||||
"written": "Per iscritto"
|
||||
},
|
||||
"contactExtent": {
|
||||
"brief": "Breve",
|
||||
"medium": "Medio",
|
||||
"extended": "Prolungato"
|
||||
},
|
||||
"leisureCategories": {
|
||||
"film": "Film o serie",
|
||||
"reading": "Lettura",
|
||||
"gaming": "Videogiochi",
|
||||
"music": "Musica",
|
||||
"creative": "Attività creativa",
|
||||
"outing": "Uscita",
|
||||
"garden": "Giardino",
|
||||
"cooking": "Cucinare",
|
||||
"timeWithChild": "Tempo con il figlio o la figlia",
|
||||
"relaxation": "Riposo",
|
||||
"other": "Altro"
|
||||
},
|
||||
"eventType": {
|
||||
"goodNews": "Buona notizia",
|
||||
"badNews": "Cattiva notizia",
|
||||
"success": "Qualcosa è riuscito",
|
||||
"disappointment": "Una delusione",
|
||||
"conflict": "Un conflitto",
|
||||
"appointment": "Un appuntamento",
|
||||
"familyEvent": "Un evento familiare",
|
||||
"change": "Un cambiamento",
|
||||
"unexpected": "Qualcosa di inaspettato"
|
||||
},
|
||||
"rating": {
|
||||
"workLoad": {
|
||||
"low": "nessuna pressione",
|
||||
"high": "sotto forte pressione"
|
||||
},
|
||||
"workSatisfaction": {
|
||||
"low": "per niente soddisfatto",
|
||||
"high": "molto soddisfatto"
|
||||
},
|
||||
"contactQuality": {
|
||||
"low": "non ha fatto bene",
|
||||
"high": "ha fatto molto bene"
|
||||
},
|
||||
"contactSupport": {
|
||||
"low": "senza sostegno",
|
||||
"high": "ben sostenuto"
|
||||
},
|
||||
"leisureJoy": {
|
||||
"low": "nessun piacere",
|
||||
"high": "molto piacere"
|
||||
},
|
||||
"leisureRecovery": {
|
||||
"low": "per niente rigenerante",
|
||||
"high": "molto rigenerante"
|
||||
},
|
||||
"eventValence": {
|
||||
"low": "chiaramente negativo",
|
||||
"high": "chiaramente positivo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"medications": {
|
||||
|
||||
@@ -922,6 +922,115 @@
|
||||
"low": "całkowicie przytłoczony",
|
||||
"high": "w pełni zdolny do działania"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"section": {
|
||||
"work": "Praca",
|
||||
"contacts": "Kontakty",
|
||||
"leisure": "Czas wolny",
|
||||
"events": "Wydarzenie"
|
||||
},
|
||||
"field": {
|
||||
"workStatus": "Dzień pracy",
|
||||
"workMinutes": "Czas pracy",
|
||||
"overtimeMinutes": "Nadgodziny",
|
||||
"workLoad": "Obciążenie pracą",
|
||||
"workSatisfaction": "Zadowolenie z pracy",
|
||||
"contactCircles": "Z kim był kontakt",
|
||||
"contactForm": "Forma kontaktu",
|
||||
"contactExtent": "Czas trwania",
|
||||
"contactQuality": "Jakość kontaktu",
|
||||
"contactSupport": "Odczuwane wsparcie",
|
||||
"leisureCategories": "Na co poszedł czas wolny",
|
||||
"leisureMinutes": "Czas dla siebie",
|
||||
"leisureJoy": "Radość",
|
||||
"leisureRecovery": "Regeneracja",
|
||||
"eventType": "Co się wydarzyło",
|
||||
"eventValence": "Jak zostało odebrane",
|
||||
"eventAt": "Kiedy",
|
||||
"notes": "Notatka"
|
||||
},
|
||||
"workStatus": {
|
||||
"off": "Bez pracy",
|
||||
"partial": "Część dnia",
|
||||
"regular": "Zwykły dzień",
|
||||
"overtime": "Dłużej niż zwykle"
|
||||
},
|
||||
"contactCircles": {
|
||||
"partner": "Partner",
|
||||
"child": "Dziecko",
|
||||
"family": "Rodzina",
|
||||
"friends": "Przyjaciele",
|
||||
"colleagues": "Współpracownicy",
|
||||
"acquaintances": "Znajomi",
|
||||
"professional": "Wsparcie profesjonalne",
|
||||
"other": "Inne osoby"
|
||||
},
|
||||
"contactForm": {
|
||||
"inPerson": "Osobiście",
|
||||
"phone": "Telefon",
|
||||
"video": "Rozmowa wideo",
|
||||
"written": "Pisemnie"
|
||||
},
|
||||
"contactExtent": {
|
||||
"brief": "Krótki",
|
||||
"medium": "Średni",
|
||||
"extended": "Długi"
|
||||
},
|
||||
"leisureCategories": {
|
||||
"film": "Film lub serial",
|
||||
"reading": "Czytanie",
|
||||
"gaming": "Gry",
|
||||
"music": "Muzyka",
|
||||
"creative": "Coś twórczego",
|
||||
"outing": "Wyjście",
|
||||
"garden": "Ogród",
|
||||
"cooking": "Gotowanie",
|
||||
"timeWithChild": "Czas z dzieckiem",
|
||||
"relaxation": "Odpoczynek",
|
||||
"other": "Coś innego"
|
||||
},
|
||||
"eventType": {
|
||||
"goodNews": "Dobra wiadomość",
|
||||
"badNews": "Zła wiadomość",
|
||||
"success": "Coś się udało",
|
||||
"disappointment": "Rozczarowanie",
|
||||
"conflict": "Konflikt",
|
||||
"appointment": "Termin",
|
||||
"familyEvent": "Wydarzenie rodzinne",
|
||||
"change": "Zmiana",
|
||||
"unexpected": "Coś nieoczekiwanego"
|
||||
},
|
||||
"rating": {
|
||||
"workLoad": {
|
||||
"low": "bez presji",
|
||||
"high": "pod dużą presją"
|
||||
},
|
||||
"workSatisfaction": {
|
||||
"low": "wcale niezadowolony",
|
||||
"high": "bardzo zadowolony"
|
||||
},
|
||||
"contactQuality": {
|
||||
"low": "nie zrobił mi dobrze",
|
||||
"high": "zrobił mi bardzo dobrze"
|
||||
},
|
||||
"contactSupport": {
|
||||
"low": "bez wsparcia",
|
||||
"high": "dobrze wspierany"
|
||||
},
|
||||
"leisureJoy": {
|
||||
"low": "bez radości",
|
||||
"high": "dużo radości"
|
||||
},
|
||||
"leisureRecovery": {
|
||||
"low": "wcale nieregenerujący",
|
||||
"high": "bardzo regenerujący"
|
||||
},
|
||||
"eventValence": {
|
||||
"low": "wyraźnie zły",
|
||||
"high": "wyraźnie dobry"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"medications": {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Pair guard binding `context-vocabulary.ts` to the six locale bundles.
|
||||
*
|
||||
* The generic i18n guards cannot carry this one. `contextValueLabelKey` builds
|
||||
* its keys from a template with a literal `mood.context.` prefix, so the
|
||||
* reverse-coverage matcher records the whole subtree as reachable and stops
|
||||
* being able to tell a live label from a stale one — the exact degradation
|
||||
* that file's own doc comment warns about, arriving here by construction
|
||||
* rather than by accident. So the check that matters is written here, against
|
||||
* the arrays themselves, in both directions:
|
||||
*
|
||||
* * every key the vocabulary names has a non-empty label in all six locales;
|
||||
* * every label under those groups belongs to a key the vocabulary names.
|
||||
*
|
||||
* The second direction is the one that catches a value removed from the
|
||||
* vocabulary whose translations were left behind, and the first is the one
|
||||
* that catches a value added without them.
|
||||
*
|
||||
* Mutation check: drop `"garden"` from `LEISURE_CATEGORY_KEYS` and the orphan
|
||||
* assertion goes red naming it in all six bundles; delete
|
||||
* `mood.context.eventType.conflict` from `messages/pl.json` and the coverage
|
||||
* assertion goes red naming pl.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
CONTACT_CIRCLE_KEYS,
|
||||
CONTACT_EXTENT_KEYS,
|
||||
CONTACT_FORM_KEYS,
|
||||
CONTEXT_SECTION_KEYS,
|
||||
EVENT_TYPE_KEYS,
|
||||
LEISURE_CATEGORY_KEYS,
|
||||
WORK_STATUS_KEYS,
|
||||
CONTEXT_RATING_FIELDS,
|
||||
contextSectionLabelKey,
|
||||
contextValueLabelKey,
|
||||
} from "@/lib/mood/context-vocabulary";
|
||||
|
||||
const MESSAGES = join(__dirname, "../../../../messages");
|
||||
const LOCALES = readdirSync(MESSAGES)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.map((f) => f.replace(/\.json$/, ""));
|
||||
|
||||
function bundle(locale: string): Record<string, unknown> {
|
||||
return JSON.parse(
|
||||
readFileSync(join(MESSAGES, `${locale}.json`), "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function resolve(root: Record<string, unknown>, key: string): unknown {
|
||||
return key
|
||||
.split(".")
|
||||
.reduce<unknown>(
|
||||
(node, part) =>
|
||||
node && typeof node === "object"
|
||||
? (node as Record<string, unknown>)[part]
|
||||
: undefined,
|
||||
root,
|
||||
);
|
||||
}
|
||||
|
||||
/** The value groups whose members come straight from a vocabulary array. */
|
||||
const VALUE_GROUPS: Array<{ group: string; keys: readonly string[] }> = [
|
||||
{ group: "workStatus", keys: WORK_STATUS_KEYS },
|
||||
{ group: "contactCircles", keys: CONTACT_CIRCLE_KEYS },
|
||||
{ group: "contactForm", keys: CONTACT_FORM_KEYS },
|
||||
{ group: "contactExtent", keys: CONTACT_EXTENT_KEYS },
|
||||
{ group: "leisureCategories", keys: LEISURE_CATEGORY_KEYS },
|
||||
{ group: "eventType", keys: EVENT_TYPE_KEYS },
|
||||
];
|
||||
|
||||
describe("mood context vocabulary labels", () => {
|
||||
it("reads six locales and a non-empty vocabulary", () => {
|
||||
// Both ends have to be there before anything below proves anything. A
|
||||
// guard that walks an empty list is a green light nobody earned.
|
||||
expect(LOCALES.length).toBe(6);
|
||||
expect(VALUE_GROUPS.length).toBeGreaterThan(0);
|
||||
const total = VALUE_GROUPS.reduce((n, g) => n + g.keys.length, 0);
|
||||
expect(total).toBeGreaterThan(30);
|
||||
expect(CONTEXT_RATING_FIELDS.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
for (const locale of LOCALES) {
|
||||
it(`${locale} carries a label for every vocabulary value`, () => {
|
||||
const root = bundle(locale);
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const { group, keys } of VALUE_GROUPS) {
|
||||
for (const key of keys) {
|
||||
const label = resolve(root, contextValueLabelKey(group, key));
|
||||
if (typeof label !== "string" || label.trim() === "") {
|
||||
missing.push(contextValueLabelKey(group, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const section of CONTEXT_SECTION_KEYS) {
|
||||
const label = resolve(root, contextSectionLabelKey(section));
|
||||
if (typeof label !== "string" || label.trim() === "") {
|
||||
missing.push(contextSectionLabelKey(section));
|
||||
}
|
||||
}
|
||||
for (const field of CONTEXT_RATING_FIELDS) {
|
||||
for (const anchor of ["low", "high"] as const) {
|
||||
const key = `mood.context.rating.${field}.${anchor}`;
|
||||
const label = resolve(root, key);
|
||||
if (typeof label !== "string" || label.trim() === "") {
|
||||
missing.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
missing,
|
||||
`messages/${locale}.json is missing ${missing.length} context label(s)`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it(`${locale} carries no label the vocabulary no longer names`, () => {
|
||||
const root = bundle(locale);
|
||||
const orphans: string[] = [];
|
||||
|
||||
for (const { group, keys } of VALUE_GROUPS) {
|
||||
const node = resolve(root, `mood.context.${group}`);
|
||||
expect(
|
||||
node && typeof node === "object",
|
||||
`mood.context.${group} is absent from ${locale} — this matcher would prove nothing`,
|
||||
).toBe(true);
|
||||
for (const present of Object.keys(node as Record<string, unknown>)) {
|
||||
if (!keys.includes(present)) {
|
||||
orphans.push(`mood.context.${group}.${present}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
orphans,
|
||||
`messages/${locale}.json carries ${orphans.length} label(s) for values the vocabulary does not name`,
|
||||
).toEqual([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* The closed vocabularies a mood entry's day context is written from.
|
||||
*
|
||||
* Four sections — work, contacts, leisure, a notable event — each with a small
|
||||
* fixed key list and a handful of numbers. Every key here becomes three
|
||||
* things at once: a string stored in somebody's row, six locale labels, and a
|
||||
* feature name any later analysis names in its output. Renaming one after it
|
||||
* has been stored means a data migration, six locale edits and a re-fit, so
|
||||
* the list is closed and lives in exactly one file. The Zod enums derive from
|
||||
* these arrays, which is what makes "a value the vocabulary does not name
|
||||
* cannot be stored" a structural property rather than a convention.
|
||||
*
|
||||
* What is deliberately NOT here:
|
||||
*
|
||||
* * Sleep, activity and vitals. They already have a home, and a mood entry
|
||||
* links to that home instead of asking a second time
|
||||
* (`src/lib/mood/linked-context.ts`). A stored copy would drift the moment
|
||||
* somebody corrected a sleep session.
|
||||
* * Body symptoms. The illness module owns symptom capture and severity, and
|
||||
* the mood surface links into it rather than growing a parallel form.
|
||||
* * A productivity rating and a self-determination rating. Both were on the
|
||||
* table and both were cut: every field a person can leave empty still
|
||||
* costs a control, six translations and a model feature, and neither of
|
||||
* those two earned that.
|
||||
*
|
||||
* Labels are neutral by rule. "Worked a regular day" is a fact; "worked too
|
||||
* much" is a judgement, and a judgement about somebody's day belongs to nobody
|
||||
* but them. Contact circles are groups of people and are never scored: how
|
||||
* many people somebody saw is not a good or a bad number, which is why the
|
||||
* quality of the contact is a separate field from the fact of it.
|
||||
*/
|
||||
|
||||
/** Every numeric self-rating in the context shares the level-A 0-10 scale. */
|
||||
export const CONTEXT_RATING_MIN = 0;
|
||||
export const CONTEXT_RATING_MAX = 10;
|
||||
|
||||
/**
|
||||
* Minutes bound for the duration fields. A day holds 1440 of them, and a
|
||||
* bound is what keeps a mistyped digit from landing as a real answer that
|
||||
* then drags every average it touches.
|
||||
*/
|
||||
export const CONTEXT_MINUTES_MIN = 0;
|
||||
export const CONTEXT_MINUTES_MAX = 1440;
|
||||
|
||||
/** How a notable event landed, from clearly bad to clearly good. */
|
||||
export const CONTEXT_VALENCE_MIN = -5;
|
||||
export const CONTEXT_VALENCE_MAX = 5;
|
||||
|
||||
/**
|
||||
* The shape of the working day. `off` covers every kind of not-working day —
|
||||
* a weekend, leave, sick leave — because the reason belongs to the modules
|
||||
* that own it, not to a mood entry.
|
||||
*/
|
||||
export const WORK_STATUS_KEYS = [
|
||||
"off",
|
||||
"partial",
|
||||
"regular",
|
||||
"overtime",
|
||||
] as const;
|
||||
export type WorkStatus = (typeof WORK_STATUS_KEYS)[number];
|
||||
|
||||
/**
|
||||
* Who the day's contact was with. Multi-select: a day is rarely one circle.
|
||||
* `professional` is a therapist, a doctor, a counsellor — a contact that is
|
||||
* neither private nor a colleague, and one people do have on the days they
|
||||
* most want to look back at.
|
||||
*/
|
||||
export const CONTACT_CIRCLE_KEYS = [
|
||||
"partner",
|
||||
"child",
|
||||
"family",
|
||||
"friends",
|
||||
"colleagues",
|
||||
"acquaintances",
|
||||
"professional",
|
||||
"other",
|
||||
] as const;
|
||||
export type ContactCircle = (typeof CONTACT_CIRCLE_KEYS)[number];
|
||||
|
||||
/** How the contact happened. */
|
||||
export const CONTACT_FORM_KEYS = [
|
||||
"inPerson",
|
||||
"phone",
|
||||
"video",
|
||||
"written",
|
||||
] as const;
|
||||
export type ContactForm = (typeof CONTACT_FORM_KEYS)[number];
|
||||
|
||||
/** Roughly how long it lasted, in the coarse bands people actually recall. */
|
||||
export const CONTACT_EXTENT_KEYS = ["brief", "medium", "extended"] as const;
|
||||
export type ContactExtent = (typeof CONTACT_EXTENT_KEYS)[number];
|
||||
|
||||
/** What the free time was spent on. Multi-select. */
|
||||
export const LEISURE_CATEGORY_KEYS = [
|
||||
"film",
|
||||
"reading",
|
||||
"gaming",
|
||||
"music",
|
||||
"creative",
|
||||
"outing",
|
||||
"garden",
|
||||
"cooking",
|
||||
"timeWithChild",
|
||||
"relaxation",
|
||||
"other",
|
||||
] as const;
|
||||
export type LeisureCategory = (typeof LEISURE_CATEGORY_KEYS)[number];
|
||||
|
||||
/**
|
||||
* The kind of thing that happened. One per entry: a day with two notable
|
||||
* events is a day with two things worth writing in the note, and a list here
|
||||
* would invite ticking every box rather than naming the one that mattered.
|
||||
*/
|
||||
export const EVENT_TYPE_KEYS = [
|
||||
"goodNews",
|
||||
"badNews",
|
||||
"success",
|
||||
"disappointment",
|
||||
"conflict",
|
||||
"appointment",
|
||||
"familyEvent",
|
||||
"change",
|
||||
"unexpected",
|
||||
] as const;
|
||||
export type EventType = (typeof EVENT_TYPE_KEYS)[number];
|
||||
|
||||
/**
|
||||
* Every 0-10 self-rating on the context row, in capture order.
|
||||
*
|
||||
* Each one has a low and a high anchor in the locale bundles
|
||||
* (`mood.context.rating.<field>.{low,high}`), because a bare number on a
|
||||
* ten-point scale means whatever the person reading it decides it means, and
|
||||
* an anchor is what makes the value they set last week comparable to the one
|
||||
* they set today. `workLoad` is inverse-oriented — higher is heavier, not
|
||||
* better — and it is stored exactly as the person set it against the anchors
|
||||
* they read. A surface that needs "up is better" flips the sign itself, the
|
||||
* same rule `MoodEntry.stressA2` follows.
|
||||
*/
|
||||
export const CONTEXT_RATING_FIELDS = [
|
||||
"workLoad",
|
||||
"workSatisfaction",
|
||||
"contactQuality",
|
||||
"contactSupport",
|
||||
"leisureJoy",
|
||||
"leisureRecovery",
|
||||
] as const;
|
||||
export type ContextRatingField = (typeof CONTEXT_RATING_FIELDS)[number];
|
||||
|
||||
/** The rating fields where a higher number is a heavier day, not a better one. */
|
||||
export const CONTEXT_INVERSE_RATING_FIELDS: readonly ContextRatingField[] = [
|
||||
"workLoad",
|
||||
];
|
||||
|
||||
/** The four sections, in capture order. */
|
||||
export const CONTEXT_SECTION_KEYS = [
|
||||
"work",
|
||||
"contacts",
|
||||
"leisure",
|
||||
"events",
|
||||
] as const;
|
||||
export type ContextSection = (typeof CONTEXT_SECTION_KEYS)[number];
|
||||
|
||||
/**
|
||||
* i18n key for one vocabulary value.
|
||||
*
|
||||
* Resolved from data rather than written at a call site, which is why
|
||||
* `mood.context` sits in the reverse-coverage guard's dynamic allowlist beside
|
||||
* `mood.dimension` and `mood.tag`.
|
||||
*/
|
||||
export function contextValueLabelKey(
|
||||
field: string,
|
||||
value: string,
|
||||
): `mood.context.${string}.${string}` {
|
||||
return `mood.context.${field}.${value}`;
|
||||
}
|
||||
|
||||
/** i18n key for a section heading. */
|
||||
export function contextSectionLabelKey(
|
||||
section: ContextSection,
|
||||
): `mood.context.section.${ContextSection}` {
|
||||
return `mood.context.section.${section}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The multi-select lists as they are stored: a JSON array in a string column,
|
||||
* matching `MoodEntry.tags`.
|
||||
*
|
||||
* Encoding lives here rather than at the three call sites that need it,
|
||||
* because a column read by a route, a backup and an analysis is exactly the
|
||||
* kind of thing that ends up with three parsers and two of them wrong.
|
||||
*/
|
||||
export function encodeKeyList(keys: readonly string[] | null): string | null {
|
||||
if (!keys || keys.length === 0) return null;
|
||||
return JSON.stringify([...keys]);
|
||||
}
|
||||
|
||||
/** Decode a stored key list. A malformed value reads as empty, never throws. */
|
||||
export function decodeKeyList(stored: string | null): string[] {
|
||||
if (!stored) return [];
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(stored);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((v): v is string => typeof v === "string");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user