feat(insights): write the no-key fallbacks in every shipped locale

The deterministic fallbacks nine metric surfaces render when no provider is
reachable carried German and English bodies only. `getLocalizedText` returned
`locale === "de" ? de : en`, so French, Spanish, Italian and Polish readers got
English — on the no-key path, an outage, a spent budget or a timeout, which is
precisely when this text is the whole assessment.

Every string now has a body per locale: the verdict leads and value sentences
of the signal-grounded composer, the per-metric labels and pointers, and the
eight no-signal floors. Selection reads the reader's locale; English stays as
the structural floor for a locale that is ever genuinely missing a body.

The bodies are written per language, not translated word-for-word. fr/es/it
replace the comparative adjective ("higher than your usual average") with a
preposition — au-dessus de / por encima de / sopra — because a comparative
would have to agree in gender with the metric noun, and the labels do not share
one. Polish restructures further: the sentence takes `wynosi`, which is
invariant for the subject's gender, and the comparison uses powyżej / poniżej
with the genitive; the labels span all three genders, so nothing that inflects
can sit in a shared template.

The tone harness grades the four locales by the same rules rather than passing
them by default: false cheer, clinical-verdict and imperative-opener vocabulary
extended to all six, the no-assessment sentinel keyed per locale instead of a
de/en binary, and a byte-identity guard that fails if any locale silently falls
back to the English text again. The value-led-opener rule was rewritten as
well — it assumed the copula sits against the figure, which under-matched its
own second locale (German "liegt aktuell bei 24") and would have missed all
four new ones.
This commit is contained in:
Marc-André Bombeck committed 2026-07-19 13:42:09 +02:00
1 parent 80e88cfb23
commit 8b13b46ac7
3 files changed
+419 -128

No files matched your search

@@ -236,6 +236,21 @@ describe("tone harness — deterministic fallbacks", () => {
},
];
/**
* "No assessment is available" in each locale's own words. Keyed per locale
* rather than `de ? … : /No assessment/`, which is the same de/en binary that
* let four locales render English bodies unnoticed — under it, a French floor
* passed this assertion precisely BECAUSE it was still English.
*/
const NO_READ_SENTINEL: Record<Locale, RegExp> = {
de: /keine Einschätzung vor/,
en: /No assessment/,
fr: /Aucune évaluation/,
es: /no hay ninguna valoración/,
it: /non è disponibile alcuna valutazione/,
pl: /nie ma (?:tu )?żadnej oceny/,
};
describe.each(FLOORS)("$name floor", ({ build }) => {
it.each(locales)("%s opens on the read, not on an order", (locale) => {
const text = build(locale);
@@ -244,11 +259,33 @@ describe("tone harness — deterministic fallbacks", () => {
// The floor states plainly that no assessment is available — it must not
// dress the absence up as one.
expect(text).toMatch(
locale === "de" ? /keine Einschätzung vor/ : /No assessment/,
);
expect(text).toMatch(NO_READ_SENTINEL[locale]);
});
});
/**
* The regression this whole pass exists to close: `getLocalizedText` used to
* return `locale === "de" ? de : en`, so fr/es/it/pl rendered the ENGLISH
* body on nine metric surfaces. Every tone rule above passed on that text —
* it was in voice, it just was not the reader's language.
*
* Byte-identity against English is therefore the assertion that actually
* pins it. It holds for both the no-signal floors and the signal-grounded
* composer, because the composer has its own per-locale sentence templates
* (the value sentence and the verdict lead), not only per-locale nouns.
*/
describe.each(RIDERS)("%s renders its own body", (locale) => {
it.each(FLOORS)("$name floor is not the English text", ({ build }) => {
expect(build(locale)).not.toEqual(build("en"));
});
it.each(GROUNDED)(
"$name grounded line is not the English text",
({ build }) => {
expect(build(locale)).not.toEqual(build("en"));
},
);
});
});
describe("tone harness — the rules themselves", () => {
+40 -7
View File
@@ -241,17 +241,45 @@ export function checkLocaleIntegrity(input: {
// ── deterministic fallback tone ─────────────────────────────────────────────
/** Praise that the numbers did not earn. */
/**
* Praise that the numbers did not earn.
*
* Six locales, because the deterministic fallbacks now carry a written body in
* each: a rule that only knows English and German praise would grade four
* locales as clean no matter what they said. Vocabulary only — the rule shape
* is language-independent.
*/
const FALSE_CHEER =
/\b(great job|well done|amazing|fantastic|excellent|awesome|congratulations|keep it up|don't worry)\b|\b(super gemacht|toll gemacht|großartig|fantastisch|hervorragend|weiter so|keine sorge)\b/i;
/\b(great job|well done|amazing|fantastic|excellent|awesome|congratulations|keep it up|don't worry)\b|\b(super gemacht|toll gemacht|großartig|fantastisch|hervorragend|weiter so|keine sorge)\b|\b(bravo|félicitations|continue comme ça|génial|formidable|ne t['’]inquiète pas)\b|\b(enhorabuena|felicidades|sigue así|genial|estupendo|no te preocupes)\b|\b(complimenti|continua così|ottimo lavoro|fantastico|eccellente|non preoccuparti)\b|\b(gratulacje|brawo|tak trzymaj|świetnie|wspaniale|nie martw się)\b/i;
/** Verdict vocabulary a non-diagnostic surface may never use. */
/**
* Verdict vocabulary a non-diagnostic surface may never use.
*
* Note what is deliberately NOT here: the plain noun for "verdict" in each
* language (verdict / veredicto / verdetto / wyrok / Urteil). Several floors
* use it in the negated form that carries the whole non-diagnostic stance — "a
* reference range is a coarse anchor, not a verdict" — so banning the noun
* would flag the sentence that states the rule. The ban targets diagnosis,
* pathology, attribution of a condition to the reader, and treatment
* direction, which is what a non-diagnostic surface actually may not do.
*/
const CLINICAL_VERDICT =
/\b(diagnos\w*|abnormal|patholog\w*|you (?:have|suffer from)|risk score|clinically significant)\b|\b(Diagnose|krankhaft|pathologisch|du (?:hast|leidest)|behandlungsbedürftig)\b/i;
/\b(diagnos\w*|abnormal|patholog\w*|you (?:have|suffer from)|risk score|clinically significant)\b|\b(Diagnose|krankhaft|pathologisch|du (?:hast|leidest)|behandlungsbedürftig)\b|\b(diagnostiqu\w*|pathologique|tu souffres de|nécessite un traitement)\b|\b(diagnóstic\w*|patológic\w*|sufres de|requiere (?:un )?tratamiento)\b|\b(diagnosi|diagnostic\w*|patologic\w*|soffri di|richiede (?:un )?trattamento)\b|\b(diagnoz\w*|patologiczn\w*|cierpisz na|wymaga leczenia)\b/i;
/** The value-led opener §10 bans: "Your <metric> is <number>…". */
/**
* The value-led opener §10 bans: "Your <metric> is <number>…".
*
* Rewritten from the de/en-only shape, which assumed the copula sits directly
* against the figure. That is true for "Your BMI is 24" and false for almost
* everything else the surfaces actually render — German "liegt aktuell bei
* 24", French "est à 24", Spanish "está en 24", Polish "wynosi obecnie 24".
* The old pattern therefore under-matched German, its own second locale, and
* would have missed all four new ones. The shape is now: possessive determiner,
* up to three words of metric name, the copula, then up to two positional
* fillers before the figure.
*/
const VALUE_LED_OPENER =
/^(Your|Dein|Deine|Ihr|Ihre)\s+\S+(\s+\S+)?\s+(is|ist|liegt)\s+\d/i;
/^(?:Your|Dein(?:e)?|Ihr(?:e)?|Ta|Ton|Tes|Tu|Tus|La tua|Le tue|Il tuo|I tuoi|Twój|Twoja|Twoje)\b(?:\s+\S+){0,3}\s+(?:is|ist|liegt|est|está|è|wynosi)\b(?:\s+(?:at|bei|à|a|en|aktuell|obecnie|right now)){0,2}\s+\d/i;
/**
* The OTHER way a deterministic line stops leading with meaning: it opens on a
@@ -263,9 +291,14 @@ const VALUE_LED_OPENER =
* failure: the surface tells the reader what to do before it says anything
* about what it can and cannot see. A pointer is fine LATER in the line; it
* may not be the opening move.
*
* Extended to the four locales whose bodies used to be English. Romance and
* Polish imperatives are a closed enough set here because the pointers these
* floors could regress into are drawn from the same small verb family the
* de/en list already names — measure, weigh, track, compare, read.
*/
const IMPERATIVE_OPENER =
/^(Measure|Track|Evaluate|Monitor|Check|Watch|Log|React|Use|Keep|Pay|Consider|Make sure)\b|^(Miss|Bewerte|Beobachte|Achte|Nutze|Halte|Reagiere|Prüfe|Vergleiche|Erfasse)\b/;
/^(Measure|Track|Evaluate|Monitor|Check|Watch|Log|React|Use|Keep|Pay|Consider|Make sure)\b|^(Miss|Bewerte|Beobachte|Achte|Nutze|Halte|Reagiere|Prüfe|Vergleiche|Erfasse)\b|^(Mesure|Suis|Évalue|Surveille|Vérifie|Note|Utilise|Garde|Compare|Pèse|Prends|Lis)\b|^(Mide|Evalúa|Vigila|Comprueba|Registra|Usa|Mantén|Compara|Pésate|Toma|Lee)\b|^(Misura|Valuta|Monitora|Controlla|Registra|Usa|Mantieni|Confronta|Pesati|Prendi|Leggi)\b|^(Mierz|Oceń|Obserwuj|Sprawdź|Zapisuj|Używaj|Utrzymuj|Porównaj|Waż|Czytaj)\b/;
function firstSentence(text: string): string {
const m = /^[^.!?]*[.!?]/.exec(text.trim());
+339 -118
View File
@@ -2,12 +2,19 @@ import type { Locale } from "@/lib/i18n/config";
import type { MetricSignal } from "@/lib/insights/metric-signal";
/**
* v1.4.25 W9e — these no-key fallbacks ship DE + EN bodies only. The
* type accepts every shipped locale (fr/es/it/pl too) and routes
* non-DE locales through the EN body, mirroring the same fallback
* chain the JSON message bundles use. When DE/EN-only fallbacks get
* replaced with proper FR/ES/IT/PL bodies, expand `getLocalizedText`
* to read the matching argument.
* v1.4.25 W9e / v1.31.0 — these no-key fallbacks now carry a written body for
* every shipped locale (de/en/fr/es/it/pl). They used to ship DE + EN only and
* route the other four through the English body, which meant a French, Spanish,
* Italian or Polish self-hoster read English on nine metric surfaces whenever
* no provider was reachable. `pick` selects the reader's own body; English
* stays as the structural floor for a locale that is ever genuinely missing
* one, not as the shipped answer for four of six.
*
* The bodies are written per language rather than translated token-for-token.
* Where a sentence cannot carry the English shape — a comparative adjective
* that would have to agree with the metric noun's gender, a locative "is at"
* with no natural equivalent — that language's sentence is restructured. The
* per-language notes sit at the templates themselves.
*
* v1.21.0 (coach C1 HIGH-1/2) — these surfaces used to emit static
* clinical tips with ZERO reference to the user's own numbers, and the
@@ -22,12 +29,24 @@ import type { MetricSignal } from "@/lib/insights/metric-signal";
*/
export type InsightLocale = Locale;
/**
* A per-locale table with English required.
*
* English is required so the fallback arm can never itself be undefined; every
* other locale is optional so a genuinely missing body degrades to English
* rather than to `undefined`. Every table in this file fills all six.
*/
type Localized<T> = { en: T } & Partial<Record<InsightLocale, T>>;
function pick<T>(locale: InsightLocale, table: Localized<T>): T {
return table[locale] ?? table.en;
}
function getLocalizedText(
locale: InsightLocale,
de: string,
en: string,
text: Localized<string>,
): string {
return locale === "de" ? de : en;
return pick(locale, text);
}
// ── signal-grounded composer ────────────────────────────────────────────────
@@ -35,7 +54,7 @@ function getLocalizedText(
/** A natural-language metric label + the pointer phrasing for one metric. */
interface FallbackCopy {
/** Possessive natural-language label ("your blood pressure"). */
label: { de: string; en: string };
label: Localized<string>;
/** Unit suffix appended to a value (" bpm"), empty when none. */
unit?: string;
/** Digits to round the rendered value to (0 for integers). */
@@ -44,7 +63,7 @@ interface FallbackCopy {
* One grounded pointer used when the value sits outside the user's own
* usual swing — framed as an opportunity, never an order.
*/
pointer: { de: string; en: string };
pointer: Localized<string>;
}
function fmtValue(value: number, copy: FallbackCopy): string {
@@ -73,15 +92,40 @@ function capitalise(s: string): string {
* signal is too thin to state a confident read (no baseline / no delta), in
* which case the line simply opens on the value as before.
*/
const VERDICT_STEADY: Localized<string> = {
de: "Stabil und wie gewohnt",
en: "Steady and much as usual",
fr: "Stable, comme d’habitude",
es: "Estable, como de costumbre",
it: "Stabile, come al solito",
pl: "Stabilnie, tak jak zwykle",
};
const VERDICT_FAVOURABLE: Localized<string> = {
de: "Das geht in eine gute Richtung",
en: "This is moving in a good direction",
fr: "Cela évolue dans le bon sens",
es: "Esto va en buena dirección",
it: "Sta andando nella direzione giusta",
pl: "To zmierza w dobrym kierunku",
};
const VERDICT_OFF_USUAL: Localized<string> = {
de: "Zuletzt etwas außerhalb deines üblichen Rahmens",
en: "A little off your usual lately",
fr: "Un peu en dehors de tes repères habituels ces derniers temps",
es: "Últimamente algo fuera de tu rango habitual",
it: "Ultimamente un po’ fuori dal tuo intervallo abituale",
pl: "Ostatnio nieco poza twoim zwykłym zakresem",
};
function verdictLead(
signal: MetricSignal,
locale: InsightLocale,
): string | null {
// Inside the user's own usual swing → a steady, reassuring verdict.
if (signal.outsideNormalSwing === false) {
return locale === "de"
? "Stabil und wie gewohnt"
: "Steady and much as usual";
return pick(locale, VERDICT_STEADY);
}
// Outside the usual swing → is the move in a favourable or unfavourable
// direction? For a target-band metric the direction alone can't say, so it
@@ -98,18 +142,93 @@ function verdictLead(
? signal.delta < 0
: null;
if (favourable === true) {
return locale === "de"
? "Das geht in eine gute Richtung"
: "This is moving in a good direction";
return pick(locale, VERDICT_FAVOURABLE);
}
return locale === "de"
? "Zuletzt etwas außerhalb deines üblichen Rahmens"
: "A little off your usual lately";
return pick(locale, VERDICT_OFF_USUAL);
}
// No baseline / no usable delta → no confident verdict; open on the value.
return null;
}
/**
* The value sentence, placed against the user's own baseline.
*
* Three shapes, one per grounding level. Each is a per-locale template rather
* than one template with swapped words, because the languages do not share a
* sentence shape here:
*
* · de/en keep the locative "liegt bei" / "is at" plus a comparative adjective.
* · fr/es/it drop the comparative ("plus haut", "más alto", "più alto") for a
* PREPOSITION — "au-dessus de", "por encima de", "sopra". The comparative
* would have to agree in gender with the metric noun, which differs across
* the labels (ta tension / ton poids, tu presión / tu peso, la tua pressione
* / il tuo peso), so a single template carrying an adjective cannot be
* correct for all of them. The preposition governs the noun without agreeing
* with it, which makes one template safe for every metric.
* · pl restructures further: there is no natural locative "is at" for a
* measurement, so the sentence takes the verb `wynosi` ("amounts to"), which
* is invariant for the subject's gender — Polish adjectives and past-tense
* verbs are not, and the labels span all three genders (twój nastrój m.,
* twoja waga f., twoje tętno n.). The comparison likewise uses the
* prepositions `powyżej` / `poniżej` with the genitive rather than a
* comparative adjective.
*/
const VALUE_VS_BASELINE: Localized<
(label: string, current: string, baseline: string, higher: boolean) => string
> = {
de: (l, c, b, higher) =>
`${l} liegt aktuell bei ${c} — ${higher ? "höher" : "niedriger"} als dein üblicher Schnitt von ${b}.`,
en: (l, c, b, higher) =>
`${l} is at ${c} right now — ${higher ? "higher" : "lower"} than your usual average of ${b}.`,
fr: (l, c, b, higher) =>
`${l} est à ${c} en ce moment — ${higher ? "au-dessus" : "en dessous"} de ta moyenne habituelle de ${b}.`,
es: (l, c, b, higher) =>
`${l} está en ${c} ahora mismo — ${higher ? "por encima" : "por debajo"} de tu promedio habitual de ${b}.`,
it: (l, c, b, higher) =>
`${l} è a ${c} in questo momento — ${higher ? "sopra" : "sotto"} la tua media abituale di ${b}.`,
pl: (l, c, b, higher) =>
`${l} wynosi obecnie ${c} — ${higher ? "powyżej" : "poniżej"} twojej zwykłej średniej ${b}.`,
};
const VALUE_AT_BASELINE: Localized<(label: string, current: string) => string> =
{
de: (l, c) =>
`${l} liegt aktuell bei ${c}, im Bereich deines üblichen Schnitts.`,
en: (l, c) => `${l} is at ${c}, right around your usual average.`,
fr: (l, c) => `${l} est à ${c}, tout près de ta moyenne habituelle.`,
es: (l, c) => `${l} está en ${c}, justo en torno a tu promedio habitual.`,
it: (l, c) => `${l} è a ${c}, più o meno sulla tua media abituale.`,
pl: (l, c) =>
`${l} wynosi obecnie ${c}, czyli mniej więcej tyle co twoja zwykła średnia.`,
};
const VALUE_ALONE: Localized<(label: string, current: string) => string> = {
de: (l, c) => `${l} liegt aktuell bei ${c}.`,
en: (l, c) => `${l} is at ${c} right now.`,
fr: (l, c) => `${l} est à ${c} en ce moment.`,
es: (l, c) => `${l} está en ${c} ahora mismo.`,
it: (l, c) => `${l} è a ${c} in questo momento.`,
pl: (l, c) => `${l} wynosi obecnie ${c}.`,
};
const CLOSER_IN_RANGE: Localized<string> = {
de: "Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.",
en: "That sits in your usual range — nothing you need to act on right now.",
fr: "C’est dans tes repères habituels — rien qui appelle une réaction maintenant.",
es: "Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.",
it: "Rientra nel tuo intervallo abituale — niente su cui intervenire adesso.",
pl: "Mieści się to w twoim zwykłym zakresie — nic, na co trzeba teraz reagować.",
};
const CLOSER_UNKNOWN_SWING: Localized<string> = {
de: "Ein paar Messungen mehr machen die Tendenz belastbar — beobachte sie über die nächsten Tage.",
en: "A few more readings will make the trend dependable — keep an eye on it over the coming days.",
fr: "Quelques mesures de plus rendront la tendance fiable — garde un œil dessus les prochains jours.",
es: "Unas pocas mediciones más harán fiable la tendencia — obsérvala durante los próximos días.",
it: "Qualche misurazione in più renderà la tendenza affidabile — tienila d’occhio nei prossimi giorni.",
pl: "Kilka kolejnych pomiarów sprawi, że tendencja stanie się wiarygodna — obserwuj ją przez najbliższe dni.",
};
/**
* Compose a warm, grounded deterministic line from a metric signal. Leads with
* a plain-words verdict (meaning first), then names the current value placed
@@ -125,7 +244,7 @@ function composeGroundedFallback(
): string | null {
if (!signal || !Number.isFinite(signal.current)) return null;
const label = locale === "de" ? copy.label.de : copy.label.en;
const label = capitalise(pick(locale, copy.label));
const current = fmtValue(signal.current, copy);
const sentences: string[] = [];
@@ -143,47 +262,29 @@ function composeGroundedFallback(
Number.isFinite(signal.baseline)
) {
const baseline = fmtValue(signal.baseline, copy);
const higher = signal.delta > 0;
sentences.push(
locale === "de"
? `${capitalise(label)} liegt aktuell bei ${current} — ${
higher ? "höher" : "niedriger"
} als dein üblicher Schnitt von ${baseline}.`
: `${capitalise(label)} is at ${current} right now — ${
higher ? "higher" : "lower"
} than your usual average of ${baseline}.`,
pick(locale, VALUE_VS_BASELINE)(
label,
current,
baseline,
signal.delta > 0,
),
);
} else if (signal.baseline !== null && Number.isFinite(signal.baseline)) {
sentences.push(
locale === "de"
? `${capitalise(label)} liegt aktuell bei ${current}, im Bereich deines üblichen Schnitts.`
: `${capitalise(label)} is at ${current}, right around your usual average.`,
);
sentences.push(pick(locale, VALUE_AT_BASELINE)(label, current));
} else {
// No baseline yet — name the value honestly without inventing a comparison.
sentences.push(
locale === "de"
? `${capitalise(label)} liegt aktuell bei ${current}.`
: `${capitalise(label)} is at ${current} right now.`,
);
sentences.push(pick(locale, VALUE_ALONE)(label, current));
}
// 2) One grounded pointer when outside the usual swing; otherwise affirm.
if (signal.outsideNormalSwing === true) {
sentences.push(locale === "de" ? copy.pointer.de : copy.pointer.en);
sentences.push(pick(locale, copy.pointer));
} else if (signal.outsideNormalSwing === false) {
sentences.push(
locale === "de"
? "Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst."
: "That sits in your usual range — nothing you need to act on right now.",
);
sentences.push(pick(locale, CLOSER_IN_RANGE));
} else {
// Unknown swing (no baseline): keep one steady, non-alarming pointer.
sentences.push(
locale === "de"
? "Ein paar Messungen mehr machen die Tendenz belastbar — beobachte sie über die nächsten Tage."
: "A few more readings will make the trend dependable — keep an eye on it over the coming days.",
);
sentences.push(pick(locale, CLOSER_UNKNOWN_SWING));
}
return sentences.join(" ");
@@ -192,65 +293,141 @@ function composeGroundedFallback(
// ── per-metric copy ─────────────────────────────────────────────────────────
const BLOOD_PRESSURE_COPY: FallbackCopy = {
label: { de: "dein Blutdruck", en: "your blood pressure" },
label: {
de: "dein Blutdruck",
en: "your blood pressure",
fr: "ta tension artérielle",
es: "tu presión arterial",
it: "la tua pressione sanguigna",
pl: "twoje ciśnienie krwi",
},
unit: "",
digits: 0,
pointer: {
de: "Ein paar ruhige Messungen unter gleichen Bedingungen zeigen, ob das die Tendenz ist oder ein Ausreißer.",
en: "A few calm readings under the same conditions will show whether this is the trend or a single outlier.",
fr: "Quelques mesures au calme dans les mêmes conditions montreront si c’est la tendance ou une valeur isolée.",
es: "Unas cuantas mediciones en calma y en las mismas condiciones mostrarán si es la tendencia o un valor aislado.",
it: "Qualche misurazione a riposo nelle stesse condizioni mostrerà se è la tendenza o un valore isolato.",
pl: "Kilka spokojnych pomiarów w tych samych warunkach pokaże, czy to tendencja, czy pojedynczy wyskok.",
},
};
const WEIGHT_COPY: FallbackCopy = {
label: { de: "dein Gewicht", en: "your weight" },
label: {
de: "dein Gewicht",
en: "your weight",
fr: "ton poids",
es: "tu peso",
it: "il tuo peso",
pl: "twoja waga",
},
unit: "",
digits: 1,
pointer: {
de: "Wäge dich ein paar Tage zur gleichen Zeit, dann ordnet sich die normale Schwankung von selbst ein.",
en: "Weigh in at the same time for a few days and the normal day-to-day swing sorts itself out.",
fr: "Pèse-toi à la même heure pendant quelques jours et la fluctuation quotidienne normale se lisse d’elle-même.",
es: "Pésate a la misma hora durante unos días y la fluctuación diaria normal se compensa sola.",
it: "Pesati alla stessa ora per qualche giorno e la normale oscillazione quotidiana si compensa da sé.",
pl: "Waż się przez kilka dni o tej samej porze, a normalne dobowe wahania same się wyrównają.",
},
};
const PULSE_COPY: FallbackCopy = {
label: { de: "dein Ruhepuls", en: "your resting pulse" },
label: {
de: "dein Ruhepuls",
en: "your resting pulse",
fr: "ton pouls au repos",
es: "tu pulso en reposo",
it: "il tuo battito a riposo",
pl: "twoje tętno spoczynkowe",
},
unit: " bpm",
digits: 0,
pointer: {
de: "Miss ihn entspannt und zur gleichen Tageszeit, dann siehst du, ob die Richtung anhält.",
en: "Take it relaxed and at the same time of day to see whether the direction holds.",
fr: "Prends-le détendu et à la même heure de la journée pour voir si la direction se maintient.",
es: "Tómatelo relajado y a la misma hora del día para ver si la dirección se mantiene.",
it: "Misuralo da rilassato e alla stessa ora del giorno per vedere se la direzione tiene.",
pl: "Mierz je na spokojnie i o tej samej porze dnia, żeby zobaczyć, czy kierunek się utrzyma.",
},
};
const BMI_COPY: FallbackCopy = {
label: { de: "dein BMI", en: "your BMI" },
label: {
de: "dein BMI",
en: "your BMI",
fr: "ton IMC",
es: "tu IMC",
it: "il tuo IMC",
pl: "twoje BMI",
},
unit: "",
digits: 1,
pointer: {
de: "Schau ihn dir zusammen mit Gewichtstrend und Körperfett über ein paar Wochen an.",
en: "Read it alongside your weight trend and body-fat over a few weeks rather than day by day.",
fr: "Lis-le avec la tendance de ton poids et ta masse grasse sur quelques semaines plutôt qu’au jour le jour.",
es: "Léelo junto con la tendencia de tu peso y tu grasa corporal a lo largo de unas semanas, no día a día.",
it: "Leggilo insieme all’andamento del tuo peso e alla massa grassa nell’arco di qualche settimana, non giorno per giorno.",
pl: "Czytaj je razem z tendencją wagi i poziomem tkanki tłuszczowej w skali kilku tygodni, a nie dzień po dniu.",
},
};
const MOOD_COPY: FallbackCopy = {
label: { de: "deine Stimmung", en: "your mood" },
label: {
de: "deine Stimmung",
en: "your mood",
fr: "ton humeur",
es: "tu estado de ánimo",
it: "il tuo umore",
pl: "twój nastrój",
},
unit: "",
digits: 1,
pointer: {
de: "Halte sie ein paar Tage fest — wiederkehrende Muster sagen mehr als ein einzelner Tag.",
en: "Log it for a few days — a recurring pattern says more than any single day.",
fr: "Note-la pendant quelques jours — un motif qui revient en dit plus qu’une journée isolée.",
es: "Regístralo unos días — un patrón que se repite dice más que un día suelto.",
it: "Registralo per qualche giorno — uno schema che si ripete dice più di un singolo giorno.",
pl: "Zapisuj go przez kilka dni — powtarzający się wzorzec mówi więcej niż pojedynczy dzień.",
},
};
const ADHERENCE_COPY: FallbackCopy = {
label: { de: "deine Einnahmetreue", en: "your medication adherence" },
label: {
de: "deine Einnahmetreue",
en: "your medication adherence",
fr: "ton observance du traitement",
es: "tu adherencia a la medicación",
it: "la tua aderenza alla terapia",
pl: "twoja regularność przyjmowania leków",
},
unit: "%",
digits: 0,
pointer: {
de: "Eine feste Zeit oder ein kurzer Reminder fängt die wiederkehrenden Auslassungen am ehesten ab.",
en: "A fixed time or a quick reminder is the most reliable way to catch the repeat misses.",
fr: "Une heure fixe ou un rappel court est ce qui rattrape le mieux les oublis répétés.",
es: "Una hora fija o un recordatorio breve es lo que mejor recupera los olvidos repetidos.",
it: "Un orario fisso o un promemoria breve è ciò che intercetta meglio le dimenticanze ripetute.",
pl: "Stała pora albo krótkie przypomnienie najlepiej wyłapuje powtarzające się pominięcia.",
},
};
/** The generic pointer for the dynamic per-metric card, which has no copy table. */
const GENERIC_METRIC_POINTER: Localized<string> = {
de: "Ein paar konsistente Messungen zeigen, ob das die Richtung ist oder ein einzelner Tag.",
en: "A few consistent readings will show whether this is the direction or a single day.",
fr: "Quelques mesures régulières montreront si c’est la direction prise ou une seule journée.",
es: "Unas cuantas mediciones constantes mostrarán si es la dirección o un solo día.",
it: "Qualche misurazione costante mostrerà se è la direzione o un singolo giorno.",
pl: "Kilka konsekwentnych pomiarów pokaże, czy to kierunek, czy pojedynczy dzień.",
};
// ── public fallbacks ────────────────────────────────────────────────────────
/**
@@ -268,16 +445,12 @@ export function getNoKeyMetricStatusText(
? composeGroundedFallback(
signal,
{
label: {
de: signal.metric,
en: signal.metric,
},
// The signal's own label is already natural language and is not
// localised upstream, so it stands for every locale.
label: { en: signal.metric },
...(signal.unit ? { unit: ` ${signal.unit}` } : {}),
digits: 1,
pointer: {
de: "Ein paar konsistente Messungen zeigen, ob das die Richtung ist oder ein einzelner Tag.",
en: "A few consistent readings will show whether this is the direction or a single day.",
},
pointer: GENERIC_METRIC_POINTER,
},
locale,
)
@@ -299,24 +472,34 @@ export function getNoKeyMetricStatusText(
* What follows the lead is what makes the metric readable, phrased as how the
* measure behaves rather than as an order to the reader.
*/
const NO_READ_LEAD: Localized<string> = {
de: "Für diese Karte liegt gerade keine Einschätzung vor.",
en: "No assessment on this one right now.",
fr: "Aucune évaluation disponible pour le moment.",
es: "Ahora mismo no hay ninguna valoración disponible.",
it: "Al momento non è disponibile alcuna valutazione.",
pl: "Na razie nie ma tu żadnej oceny.",
};
function noReadLead(locale: InsightLocale): string {
return locale === "de"
? "Für diese Karte liegt gerade keine Einschätzung vor."
: "No assessment on this one right now.";
return pick(locale, NO_READ_LEAD);
}
function floor(locale: InsightLocale, de: string, en: string): string {
return `${noReadLead(locale)} ${getLocalizedText(locale, de, en)}`;
function floor(locale: InsightLocale, body: Localized<string>): string {
return `${noReadLead(locale)} ${getLocalizedText(locale, body)}`;
}
export function getNoKeyGeneralStatusText(locale: InsightLocale): string {
// The overview spans many metrics with no single headline value to ground
// against, so it keeps the honest, generic multi-metric pointer.
return floor(
locale,
"Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.",
"These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.",
);
return floor(locale, {
de: "Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.",
en: "These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.",
fr: "Ces chiffres deviennent lisibles sur plusieurs semaines plutôt que sur des journées isolées, et relevés à des moments constants pour rester comparables. Le signal qui compte, c’est plusieurs indicateurs qui dérivent en même temps dans le même sens défavorable, pas une valeur un jour donné.",
es: "Estos números se vuelven legibles a lo largo de varias semanas y no en días sueltos, y tomados a horas constantes para que sigan siendo comparables. La señal que cuenta son varios indicadores derivando a la vez en la misma dirección desfavorable, no un valor en un día.",
it: "Questi numeri diventano leggibili nell’arco di più settimane anziché di singoli giorni, e rilevati a orari costanti per restare confrontabili. Il segnale che conta è più indicatori che scivolano insieme nella stessa direzione sfavorevole, non un valore in un giorno.",
pl: "Te liczby stają się czytelne w skali kilku tygodni, a nie pojedynczych dni, i tylko wtedy, gdy są zbierane o stałych porach, żeby pozostały porównywalne. Sygnałem, który się liczy, jest kilka wskaźników dryfujących jednocześnie w tę samą niekorzystną stronę, a nie jedna wartość jednego dnia.",
});
}
/**
@@ -324,26 +507,46 @@ export function getNoKeyGeneralStatusText(locale: InsightLocale): string {
* multi-metric overview tip — text about watching several metrics at once, on
* a card showing exactly one lab value. This says something true about a lab
* marker instead.
*
* The named lead is built per locale rather than by splicing a shared clause:
* the quoting convention differs (German low-high quotes, French and Italian
* guillemets, Polish low-high) and so does where the marker name attaches to
* the sentence.
*/
const BIOMARKER_NAMED_LEAD: Localized<(named: string) => string> = {
de: (n) => `Für diesen Laborwert${n} liegt gerade keine Einschätzung vor.`,
en: (n) => `No assessment${n} right now.`,
fr: (n) => `Aucune évaluation${n} pour le moment.`,
es: (n) => `Ahora mismo no hay ninguna valoración${n}.`,
it: (n) => `Al momento non è disponibile alcuna valutazione${n}.`,
pl: (n) => `Na razie nie ma żadnej oceny${n}.`,
};
const BIOMARKER_NAMED_CLAUSE: Localized<(marker: string) => string> = {
de: (m) => ` zu „${m}“`,
en: (m) => ` for "${m}"`,
fr: (m) => ` pour « ${m} »`,
es: (m) => ` de «${m}»`,
it: (m) => ` per «${m}»`,
pl: (m) => ` dla „${m}”`,
};
export function getNoKeyBiomarkerStatusText(
locale: InsightLocale,
markerName?: string | null,
): string {
const trimmed = markerName?.trim() ?? "";
const named =
markerName && markerName.trim().length > 0
? locale === "de"
? ` zu „${markerName.trim()}"`
: ` for "${markerName.trim()}"`
: "";
const lead =
locale === "de"
? `Für diesen Laborwert${named} liegt gerade keine Einschätzung vor.`
: `No assessment${named} right now.`;
return `${lead} ${getLocalizedText(
locale,
"Ein Laborwert liest sich zuerst gegen die eigenen vorherigen Abnahmen — ein Referenzbereich ist ein grober Anker, kein Urteil. Ein Wert, der sich über mehrere Abnahmen kaum bewegt, erzählt etwas anderes als einer, der gerade gesprungen ist. Die nächste ärztliche Kontrolle ist der richtige Ort, ihn einzuordnen.",
"A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.",
)}`;
trimmed.length > 0 ? pick(locale, BIOMARKER_NAMED_CLAUSE)(trimmed) : "";
const lead = pick(locale, BIOMARKER_NAMED_LEAD)(named);
return `${lead} ${getLocalizedText(locale, {
de: "Ein Laborwert liest sich zuerst gegen die eigenen vorherigen Abnahmen — ein Referenzbereich ist ein grober Anker, kein Urteil. Ein Wert, der sich über mehrere Abnahmen kaum bewegt, erzählt etwas anderes als einer, der gerade gesprungen ist. Die nächste ärztliche Kontrolle ist der richtige Ort, ihn einzuordnen.",
en: "A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.",
fr: "Une valeur de laboratoire se lit d’abord contre tes propres prélèvements précédents — un intervalle de référence est un repère grossier, pas un verdict. Une valeur qui a à peine bougé sur plusieurs prélèvements raconte autre chose qu’une valeur qui vient de faire un saut. Ton prochain contrôle médical est le bon endroit pour la remettre en contexte.",
es: "Un valor de laboratorio se lee primero frente a tus propias extracciones anteriores — un intervalo de referencia es un ancla aproximada, no un veredicto. Uno que apenas se ha movido a lo largo de varias extracciones cuenta algo distinto de uno que acaba de dar un salto. Tu próxima revisión médica es el lugar adecuado para ponerlo en contexto.",
it: "Un valore di laboratorio si legge prima di tutto rispetto ai tuoi prelievi precedenti — un intervallo di riferimento è un ancoraggio grossolano, non un verdetto. Uno che si è mosso appena nell’arco di più prelievi racconta una storia diversa da uno che ha appena fatto un salto. Il tuo prossimo controllo medico è il posto giusto per contestualizzarlo.",
pl: "Wynik laboratoryjny czyta się najpierw na tle twoich wcześniejszych oznaczeń — zakres referencyjny to zgrubna kotwica, a nie wyrok. Wynik, który przez kilka oznaczeń niemal się nie ruszył, mówi co innego niż taki, który właśnie podskoczył. Najbliższa kontrola lekarska to właściwe miejsce, żeby go osadzić w kontekście.",
})}`;
}
export function getNoKeyBloodPressureStatusText(
@@ -352,11 +555,14 @@ export function getNoKeyBloodPressureStatusText(
): string {
return (
composeGroundedFallback(signal, BLOOD_PRESSURE_COPY, locale) ??
floor(
locale,
"Blutdruck wird lesbar über eine Reihe ruhiger Messungen unter vergleichbaren Bedingungen — die Richtung über mehrere Tage sagt weit mehr als ein einzelner Ausreißer, und systolisch und diastolisch gehören dabei zusammen gelesen.",
"Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.",
)
floor(locale, {
de: "Blutdruck wird lesbar über eine Reihe ruhiger Messungen unter vergleichbaren Bedingungen — die Richtung über mehrere Tage sagt weit mehr als ein einzelner Ausreißer, und systolisch und diastolisch gehören dabei zusammen gelesen.",
en: "Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.",
fr: "La tension se lit sur une série de mesures au calme dans des conditions comparables — la direction sur plusieurs jours dit bien plus qu’une valeur isolée, et la systolique et la diastolique se lisent ensemble plutôt que l’une après l’autre.",
es: "La tensión se lee a lo largo de una serie de mediciones en calma y en condiciones comparables — la dirección a lo largo de varios días dice mucho más que cualquier valor aislado, y la sistólica y la diastólica se leen juntas y no una por una.",
it: "La pressione si legge su una serie di misurazioni a riposo in condizioni confrontabili — la direzione nell’arco di più giorni dice molto più di un singolo valore isolato, e sistolica e diastolica si leggono insieme anziché una alla volta.",
pl: "Ciśnienie staje się czytelne dopiero w serii spokojnych pomiarów w porównywalnych warunkach — kierunek z kilku dni mówi znacznie więcej niż pojedynczy wyskok, a wartość skurczowa i rozkurczowa czyta się razem, a nie osobno.",
})
);
}
@@ -366,11 +572,14 @@ export function getNoKeyWeightStatusText(
): string {
return (
composeGroundedFallback(signal, WEIGHT_COPY, locale) ??
floor(
locale,
"Gewicht liest sich als Verlauf, nicht als Tageszahl — unter konstanten Bedingungen gewogen, pendelt sich die normale Tagesschwankung von selbst ein. Die Richtung über Wochen, zusammen mit Blutdruck und BMI gelesen, ist das, was trägt.",
"Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.",
)
floor(locale, {
de: "Gewicht liest sich als Verlauf, nicht als Tageszahl — unter konstanten Bedingungen gewogen, pendelt sich die normale Tagesschwankung von selbst ein. Die Richtung über Wochen, zusammen mit Blutdruck und BMI gelesen, ist das, was trägt.",
en: "Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.",
fr: "Le poids se lit comme une tendance et non comme un chiffre du jour — pesé dans des conditions constantes, la fluctuation quotidienne normale se lisse d’elle-même. C’est la direction sur des semaines, lue avec la tension et l’IMC, qui porte.",
es: "El peso se lee como una tendencia y no como una cifra diaria — pesado en condiciones constantes, la fluctuación normal del día a día se compensa sola. Lo que sostiene es la dirección a lo largo de semanas, leída junto a la tensión y el IMC.",
it: "Il peso si legge come andamento e non come numero del giorno — pesato in condizioni costanti, la normale oscillazione quotidiana si compensa da sé. Quello che regge è la direzione nell’arco di settimane, letta insieme alla pressione e all’IMC.",
pl: "Wagę czyta się jako przebieg, a nie jako liczbę z jednego dnia — przy ważeniu w stałych warunkach normalne dobowe wahania same się wyrównają. Nośny jest kierunek z kilku tygodni, czytany razem z ciśnieniem i BMI.",
})
);
}
@@ -380,11 +589,14 @@ export function getNoKeyPulseStatusText(
): string {
return (
composeGroundedFallback(signal, PULSE_COPY, locale) ??
floor(
locale,
"Der Ruhepuls liest sich am besten entspannt und zur gleichen Tageszeit gemessen. Kurze Ausschläge sind gewöhnlich; was auffällt, ist ein Muster, das über mehrere Tage hält, oder wiederholtes Abdriften vom eigenen üblichen Bereich.",
"Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.",
)
floor(locale, {
de: "Der Ruhepuls liest sich am besten entspannt und zur gleichen Tageszeit gemessen. Kurze Ausschläge sind gewöhnlich; was auffällt, ist ein Muster, das über mehrere Tage hält, oder wiederholtes Abdriften vom eigenen üblichen Bereich.",
en: "Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.",
fr: "Le pouls au repos se lit au mieux pris détendu et à la même heure de la journée. Les à-coups brefs sont ordinaires ; ce qui ressort, c’est un motif qui tient sur plusieurs jours, ou une dérive répétée hors de tes repères habituels.",
es: "El pulso en reposo se lee mejor tomado relajado y a la misma hora del día. Los picos breves son corrientes; lo que destaca es un patrón que se mantiene varios días, o una deriva repetida fuera de tu rango habitual.",
it: "Il battito a riposo si legge meglio se misurato da rilassato e alla stessa ora del giorno. I picchi brevi sono ordinari; quello che spicca è uno schema che tiene per più giorni, o uno scostamento ripetuto dal tuo intervallo abituale.",
pl: "Tętno spoczynkowe najlepiej czytać zmierzone na spokojnie i o tej samej porze dnia. Krótkie skoki są czymś zwyczajnym; uwagę zwraca wzorzec utrzymujący się przez kilka dni albo powtarzające się oddalanie od twojego zwykłego zakresu.",
})
);
}
@@ -394,11 +606,14 @@ export function getNoKeyBmiStatusText(
): string {
return (
composeGroundedFallback(signal, BMI_COPY, locale) ??
floor(
locale,
"Der BMI ist eine grobe Orientierung, kein Urteil, und bedeutet am meisten zusammen mit Gewichtstrend und Körperzusammensetzung gelesen. Eine anhaltende Bewegung über Wochen trägt, wo ein Einzelwert es nicht tut.",
"BMI is a rough orientation rather than a verdict, and it means most read alongside your weight trend and body composition. Sustained movement over weeks carries where a single value does not.",
)
floor(locale, {
de: "Der BMI ist eine grobe Orientierung, kein Urteil, und bedeutet am meisten zusammen mit Gewichtstrend und Körperzusammensetzung gelesen. Eine anhaltende Bewegung über Wochen trägt, wo ein Einzelwert es nicht tut.",
en: "BMI is a rough orientation rather than a verdict, and it means most read alongside your weight trend and body composition. Sustained movement over weeks carries where a single value does not.",
fr: "L’IMC est une orientation grossière plutôt qu’un verdict, et il prend son sens lu avec la tendance de ton poids et ta composition corporelle. Un mouvement soutenu sur des semaines porte là où une valeur isolée ne porte pas.",
es: "El IMC es una orientación aproximada más que un veredicto, y cobra sentido leído junto a la tendencia de tu peso y tu composición corporal. Un movimiento sostenido a lo largo de semanas sostiene donde un valor aislado no lo hace.",
it: "L’IMC è un orientamento grossolano più che un verdetto, e assume senso letto insieme all’andamento del tuo peso e alla composizione corporea. Un movimento sostenuto nell’arco di settimane regge dove un valore singolo non regge.",
pl: "BMI to zgrubna orientacja, a nie wyrok, i nabiera sensu dopiero czytane razem z tendencją wagi i składem ciała. Utrzymujący się ruch w skali tygodni jest nośny tam, gdzie pojedyncza wartość nie jest.",
})
);
}
@@ -408,11 +623,14 @@ export function getNoKeyMedicationComplianceStatusText(
): string {
return (
composeGroundedFallback(signal, ADHERENCE_COPY, locale) ??
floor(
locale,
"Einnahmetreue liest sich als Konstanz über Wochen, nicht als eine Reihe perfekter Tage — je Medikament und im Gesamtbild. Wiederkehrende Auslassungen zur selben Tageszeit sind das Muster, auf das es ankommt, und eine feste Routine fängt sie am ehesten ab.",
"Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.",
)
floor(locale, {
de: "Einnahmetreue liest sich als Konstanz über Wochen, nicht als eine Reihe perfekter Tage — je Medikament und im Gesamtbild. Wiederkehrende Auslassungen zur selben Tageszeit sind das Muster, auf das es ankommt, und eine feste Routine fängt sie am ehesten ab.",
en: "Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.",
fr: "L’observance se lit comme une régularité sur des semaines plutôt que comme une série de journées parfaites — par médicament et dans l’ensemble. Les oublis répétés à la même heure sont le motif qui compte, et c’est une routine fixe qui les rattrape d’ordinaire.",
es: "La adherencia se lee como constancia a lo largo de semanas y no como una racha de días perfectos — por medicamento y en el conjunto. Los olvidos repetidos a la misma hora son el patrón que cuenta, y lo que suele recuperarlos es una rutina fija.",
it: "L’aderenza si legge come costanza nell’arco di settimane più che come una serie di giornate perfette — per singolo farmaco e nel quadro complessivo. Le dimenticanze ripetute alla stessa ora sono lo schema che conta, ed è una routine fissa a intercettarle di solito.",
pl: "Regularność przyjmowania leków czyta się jako stałość w skali tygodni, a nie jako serię idealnych dni — osobno dla każdego leku i w całym obrazie. Powtarzające się pominięcia o tej samej porze to wzorzec, który się liczy, a wyłapuje je zwykle stały rytm dnia.",
})
);
}
@@ -422,10 +640,13 @@ export function getNoKeyMoodStatusText(
): string {
return (
composeGroundedFallback(signal, MOOD_COPY, locale) ??
floor(
locale,
"Stimmung liest sich über Wochen, nicht über einzelne Tage — wiederkehrende Muster und wie sie zu den anderen Werten passen, sind das, was trägt. Eine anhaltende Phase niedriger Stimmung ist die, die Aufmerksamkeit verdient.",
"Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.",
)
floor(locale, {
de: "Stimmung liest sich über Wochen, nicht über einzelne Tage — wiederkehrende Muster und wie sie zu den anderen Werten passen, sind das, was trägt. Eine anhaltende Phase niedriger Stimmung ist die, die Aufmerksamkeit verdient.",
en: "Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.",
fr: "L’humeur se lit sur des semaines plutôt que sur des journées isolées — ce qui porte, ce sont les motifs qui reviennent et la façon dont ils s’alignent avec tes autres valeurs. C’est une phase basse qui dure qui mérite l’attention.",
es: "El estado de ánimo se lee a lo largo de semanas y no en días sueltos — lo que sostiene son los patrones que se repiten y cómo encajan con tus otros valores. La que merece atención es una fase baja que se prolonga.",
it: "L’umore si legge nell’arco di settimane più che nei singoli giorni — quello che regge sono gli schemi ricorrenti e come si allineano con gli altri tuoi valori. È una fase bassa che si protrae quella che merita attenzione.",
pl: "Nastrój czyta się w skali tygodni, a nie pojedynczych dni — nośne są powtarzające się wzorce i to, jak układają się względem twoich pozostałych wartości. Uwagi wymaga przede wszystkim dłużej utrzymujący się spadek.",
})
);
}