From 3ed266e281a982a70670edf6d37308996e6eda0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 12 Nov 2026 12:49:18 +0100 Subject: [PATCH] fix(measurements): drop the stray space in a unit-less range message A score, rating, count or ratio has no unit label, so the range message for a manual pain score read "Enter a value between 0 and 10 ." with a space before the stop, and the same in every locale. A second sentence without the unit now answers those types. --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/ko.json | 3 +- messages/nl.json | 3 +- messages/pl.json | 3 +- messages/pt-BR.json | 3 +- messages/ru.json | 3 +- .../measurement-list-glucose-unit.test.tsx | 31 ++++++++++++++++++- .../measurements/list/use-measurement-edit.ts | 16 +++++----- 12 files changed, 58 insertions(+), 19 deletions(-) diff --git a/messages/de.json b/messages/de.json index c96aea84d..54f6f2e0d 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Trainingsbereitschaft", "typeAcuteTrainingLoad": "Akute Trainingsbelastung", "sourceGarmin": "Garmin", - "valueOutOfRange": "Gib einen Wert zwischen {min} und {max} {unit} ein." + "valueOutOfRange": "Gib einen Wert zwischen {min} und {max} {unit} ein.", + "valueOutOfRangeNoUnit": "Gib einen Wert zwischen {min} und {max} ein." }, "metricCatalog": { "title": "Was HealthLog erfassen kann", diff --git a/messages/en.json b/messages/en.json index cf62fc3bd..d4ad8ab7b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Training readiness", "typeAcuteTrainingLoad": "Acute training load", "sourceGarmin": "Garmin", - "valueOutOfRange": "Enter a value between {min} and {max} {unit}." + "valueOutOfRange": "Enter a value between {min} and {max} {unit}.", + "valueOutOfRangeNoUnit": "Enter a value between {min} and {max}." }, "metricCatalog": { "title": "What HealthLog can track", diff --git a/messages/es.json b/messages/es.json index fd4716bf1..eaa860236 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Preparación para entrenar", "typeAcuteTrainingLoad": "Carga de entrenamiento aguda", "sourceGarmin": "Garmin", - "valueOutOfRange": "Introduce un valor entre {min} y {max} {unit}." + "valueOutOfRange": "Introduce un valor entre {min} y {max} {unit}.", + "valueOutOfRangeNoUnit": "Introduce un valor entre {min} y {max}." }, "metricCatalog": { "title": "Lo que HealthLog puede registrar", diff --git a/messages/fr.json b/messages/fr.json index 7c3d96f50..52fbf7e87 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Disponibilité à l'entraînement", "typeAcuteTrainingLoad": "Charge d'entraînement aiguë", "sourceGarmin": "Garmin", - "valueOutOfRange": "Saisissez une valeur entre {min} et {max} {unit}." + "valueOutOfRange": "Saisissez une valeur entre {min} et {max} {unit}.", + "valueOutOfRangeNoUnit": "Saisissez une valeur entre {min} et {max}." }, "metricCatalog": { "title": "Ce que HealthLog peut suivre", diff --git a/messages/it.json b/messages/it.json index fa0fca028..26c6051c2 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Prontezza all'allenamento", "typeAcuteTrainingLoad": "Carico di allenamento acuto", "sourceGarmin": "Garmin", - "valueOutOfRange": "Inserisci un valore tra {min} e {max} {unit}." + "valueOutOfRange": "Inserisci un valore tra {min} e {max} {unit}.", + "valueOutOfRangeNoUnit": "Inserisci un valore tra {min} e {max}." }, "metricCatalog": { "title": "Cosa può monitorare HealthLog", diff --git a/messages/ko.json b/messages/ko.json index 8d8f43157..6f9c186c1 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "트레이닝 준비도", "typeAcuteTrainingLoad": "급성 훈련 부하", "sourceGarmin": "Garmin", - "valueOutOfRange": "{min}에서 {max} {unit} 사이의 값을 입력해 주세요." + "valueOutOfRange": "{min}에서 {max} {unit} 사이의 값을 입력해 주세요.", + "valueOutOfRangeNoUnit": "{min}에서 {max} 사이의 값을 입력해 주세요." }, "metricCatalog": { "title": "HealthLog가 기록할 수 있는 것", diff --git a/messages/nl.json b/messages/nl.json index 2997c7662..761424489 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Trainingsgereedheid", "typeAcuteTrainingLoad": "Acute trainingsbelasting", "sourceGarmin": "Garmin", - "valueOutOfRange": "Vul een waarde in tussen {min} en {max} {unit}." + "valueOutOfRange": "Vul een waarde in tussen {min} en {max} {unit}.", + "valueOutOfRangeNoUnit": "Vul een waarde in tussen {min} en {max}." }, "metricCatalog": { "title": "Wat HealthLog kan bijhouden", diff --git a/messages/pl.json b/messages/pl.json index d76523f14..416541d3f 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Gotowość do treningu", "typeAcuteTrainingLoad": "Ostre obciążenie treningowe", "sourceGarmin": "Garmin", - "valueOutOfRange": "Wpisz wartość od {min} do {max} {unit}." + "valueOutOfRange": "Wpisz wartość od {min} do {max} {unit}.", + "valueOutOfRangeNoUnit": "Wpisz wartość od {min} do {max}." }, "metricCatalog": { "title": "Co HealthLog potrafi śledzić", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index aefc823cc..415813bfb 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Prontidão para treinar", "typeAcuteTrainingLoad": "Carga de treino aguda", "sourceGarmin": "Garmin", - "valueOutOfRange": "Informe um valor entre {min} e {max} {unit}." + "valueOutOfRange": "Informe um valor entre {min} e {max} {unit}.", + "valueOutOfRangeNoUnit": "Informe um valor entre {min} e {max}." }, "metricCatalog": { "title": "O que o HealthLog pode acompanhar", diff --git a/messages/ru.json b/messages/ru.json index a63019d8f..f38b380a0 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -1033,7 +1033,8 @@ "typeTrainingReadiness": "Готовность к тренировке", "typeAcuteTrainingLoad": "Острая тренировочная нагрузка", "sourceGarmin": "Garmin", - "valueOutOfRange": "Введите значение от {min} до {max} {unit}." + "valueOutOfRange": "Введите значение от {min} до {max} {unit}.", + "valueOutOfRangeNoUnit": "Введите значение от {min} до {max}." }, "metricCatalog": { "title": "Что умеет отслеживать HealthLog", diff --git a/src/components/measurements/__tests__/measurement-list-glucose-unit.test.tsx b/src/components/measurements/__tests__/measurement-list-glucose-unit.test.tsx index ed1d056da..a42afdca6 100644 --- a/src/components/measurements/__tests__/measurement-list-glucose-unit.test.tsx +++ b/src/components/measurements/__tests__/measurement-list-glucose-unit.test.tsx @@ -64,7 +64,11 @@ import { MeasurementList } from "../measurement-list"; import type { UnitDisplay } from "@/hooks/use-unit-display"; import { convertGlucose, toCanonicalMgdl } from "@/lib/glucose"; import { getUnitForType } from "@/lib/measurements/unit-map"; -import { editSeedValue, storedValueFor } from "../list/use-measurement-edit"; +import { + editSeedValue, + outOfRangeMessage, + storedValueFor, +} from "../list/use-measurement-edit"; import { rowDisplayFor } from "../list/use-measurement-row-display"; /** A metric reader on the given glucose unit, as the list's row display. */ @@ -140,3 +144,28 @@ describe("MeasurementList — blood-glucose display unit", () => { expect(storedValueFor(display, glucose(95.48), 95.48)).toBe(95.48); }); }); + +describe("the out-of-range sentence", () => { + const t = ((key: string, params?: Record) => + `${key} ${JSON.stringify(params)}`) as unknown as Parameters< + typeof outOfRangeMessage + >[2]; + const reading = (type: string, unit: string) => + ({ id: "m1", type, value: 1, unit, source: "MANUAL" }) as never; + const shows = (unit: string) => ({ + rowDisplay: (m: { value: number }) => ({ value: m.value, unit }), + }); + + it("names the unit where the type has one", () => { + expect( + outOfRangeMessage(shows("kg"), reading("WEIGHT", "kg"), t, String), + ).toMatch(/^measurements\.valueOutOfRange \{.*"unit":"kg"/); + }); + + it("leaves the unit out for a score, which has no label", () => { + // "Enter a value between 0 and 10 ." read with a space before the stop. + expect( + outOfRangeMessage(shows(""), reading("PAIN_NRS", "score"), t, String), + ).toBe('measurements.valueOutOfRangeNoUnit {"min":"0","max":"10"}'); + }); +}); diff --git a/src/components/measurements/list/use-measurement-edit.ts b/src/components/measurements/list/use-measurement-edit.ts index 8a524db5c..cca4c24b7 100644 --- a/src/components/measurements/list/use-measurement-edit.ts +++ b/src/components/measurements/list/use-measurement-edit.ts @@ -103,20 +103,20 @@ export function editValueRange( type Translate = ReturnType["t"]; /** The sentence for a value outside its band, in the unit the input shows. */ -function outOfRangeMessage( +export function outOfRangeMessage( display: Pick, measurement: Measurement, t: Translate, formatNumber: (value: number) => string, ): string { const range = editValueRange(display, measurement); - return range - ? t("measurements.valueOutOfRange", { - min: formatNumber(range.min), - max: formatNumber(range.max), - unit: range.unit, - }) - : t("measurements.invalidValue"); + if (!range) return t("measurements.invalidValue"); + const bounds = { min: formatNumber(range.min), max: formatNumber(range.max) }; + // A score, rating, count or ratio has no unit label, which left a space + // before the full stop. + return range.unit + ? t("measurements.valueOutOfRange", { ...bounds, unit: range.unit }) + : t("measurements.valueOutOfRangeNoUnit", bounds); } /**