Commit Graph
7 Commits
Author SHA1 Message Date
l1137l 48bfbad4e6 fix(i18n): address the ru review — units, Cyrillic regexes, locale registry
Dose bank
- DRUG_UNIT learns мг / мкг / мл / ЕД / МЕ, so "2,4 мг" anchors a dose again.
- The bank matches verb-first Russian word order too ("принимайте 2 таблетки",
  "пропустите дозу метформина", "начните с 5 мг"), and the Cyrillic drug names
  people actually see on the box; the deferral words ("спросите врача",
  "как назначено") stand those shapes down.

Cyrillic-safe regexes
- \w and \b are ASCII in JavaScript: every stem in the ru bank and in
  population-norms/ru.ts is [а-яё]*, and the word edges are lookarounds
  (?<![а-яёa-z]), so «высокий риск» and «нормальный диапазон» match again.
- coach-prose-grounding: Russian month names in the genitive, a space as the
  thousands separator in parseLocaleNumber and numFrag, and Cyrillic-safe
  edges around the written-date pattern.

Registered everywhere a locale has to be
- targetLanguageName() returns "Russian", so the coach, the period narrative
  and the document descriptions stop saying "write in English".
- i18n-key-tool.mjs, split-description.mjs, LOINC name normalisation (it
  stripped Cyrillic, so no Russian lab name resolved), HOST_TLDS (ru/by/kz/ua),
  no-key-fallbacks (32 tables), country-label, the AUDIT-C standard drink.
- docs/api/openapi.yaml regenerated.

Questionnaires
- The PHQ-9 / GAD-7 source comments no longer claim no validated Russian
  version exists (Zolotareva 2023; Pfizer's official screeners).
- WHO-5 items are gender-neutral: «чувствовал(а)».
- "body fat" and "fat mass" both read «Жировая масса»; body fat is now
  «Процент жировой массы», which is what the two metrics measure.

Tests
- ru in the 13 locale tables, plus mood, onboarding, instruments, clinician
  view, status summary, describe screen, citation coverage and LOINC.
- i18n-no-dot-separator: the ru bundle is exempt from the dash rule (a spaced
  dash is Russian punctuation, not a fragment join); the middle dot still
  fails there.
- Two 800-line baselines raised by the 12 and 5 lines this change adds.

pnpm typecheck, pnpm openapi:check and the full suite (36 316 tests) pass.
2026-11-02 20:22:03 +00:00
Marc-André Bombeck 764794496c feat(i18n): register Brazilian Portuguese as a locale
Add pt-BR to the locale list and everything that hangs off it: the
client loader, Intl and date-fns mapping, the reply-language footers of
the Coach and insights prompts (English body, pt-BR footer, the same
path Korean and Dutch take), the citation-coverage normative words, the
no-key fallback copy, the AUDIT-C standard-drink line, country labels,
period search, CSV yes/no cells and the Telegram bare-word aliases.

The locale enums in the extract, managed-profile and record-settings
schemas now derive from the locale list instead of repeating it, and
the five notification and login-alert routes share one isLocale guard
instead of hand-rolled chains, so a later locale is one edit there.
A language tag is matched exactly and case-insensitively before its
primary subtag, so pt-BR and pt-PT browsers land on the Portuguese
bundle rather than English, and AUDIT-C no longer cuts a region tag to
two letters.

PHQ-9, GAD-7 and WHO-5 are not marked validated for pt-BR: the wording
has not been read against the published Brazilian forms, so readers get
the English items plus the existing note.
2026-10-22 14:50:00 +02:00
Rob de Graaf c6f61fad33 feat(i18n): add Dutch as an eighth end-to-end locale
Adds messages/nl.json (AI-initial, informal je/jij register, full key
parity with en.json) and registers nl everywhere the locale list lives:
config, loaders, Intl/date-fns mapping, zod enums, OpenAPI spec, scripts,
the managed-profile picker and the notification locale guards.

Dutch follows the Korean pattern for the AI surfaces (English body plus a
Dutch reply-language footer, no safety-contract matrix) and gets its own
banks in the outbound safety screen (dose, risk, causal, continuation),
the prose-grounding and citation-coverage tables, the reference-range
prose and the no-key fallback copy, each with Dutch test fixtures.

PHQ-9 / GAD-7 / WHO-5 are not marked validated for nl yet: the Dutch
wording has not been read against the published forms, so Dutch readers
get the English items plus the existing note until it has.
2026-10-19 11:06:55 +02:00
aucun6352 f1b0420479 feat(i18n): add Korean as a seventh end-to-end locale
Register `ko` in the locale list, language names, and labels, and ship
messages/ko.json so the whole UI — dashboard, settings, share views,
clinician PDF, notifications, and AI prompts — renders in Korean.

Extend the locale-parity, English-leak, plural-completeness, and copy
guard tests to cover the new locale, and update README/CONTRIBUTING to
list Korean among the supported languages.
2026-08-26 16:42:45 +09:00
Marc-André Bombeck 272c426bea fix(ui): close the holes the audit found in this work's own guards
Every item here is a check that could pass while the thing it checks is
broken, or a margin the sweep's codemod could not reach.

Guards:

- The access-payload matcher accepted `.level` on ANY object. A chart's zoom
  level or a log record's level would have satisfied it and reported a
  consumer that does not exist. It now names the bindings the entry actually
  arrives under; a third binding is a deliberate edit rather than a silent
  widening. Re-falsified: rename both readers, it goes red.
- The `pl-7` sweep skipped any line CONTAINING `//`, so a class string sharing
  a line with a URL was waved through. Only the part after a real line-comment
  marker is exempt now. Proven by putting `pl-7` on a line with an
  `https://` comment: caught.
- `LEGIT_IDENTICAL` in the English-leak guard had no staleness check, unlike
  both of its siblings, and carried two dead keys — one from a removal long
  before this work, one from the duplicated version fields that came off
  `/admin`. Both pruned, the check added, falsified with a made-up key.
- `split-description.mjs` split inside `z. B.`, `d. h.`, `u. a.` and `p. ex.`
  and produced a head ending mid-abbreviation. Both halves have to be caught,
  which is why the rule is a shape (single letter + stop) rather than a list,
  and the merge re-tests its own tail. Exercised against eight cases including
  the ones that used to break.

Margins the codemod could not see, because the child lives in a different
component from the card that spaces it: six form roots under managed record
settings (one of which also carried the banned `space-y-5` step), the
low-stock status line, the avatar card's explainer, and the backups restore
report. Each was adding a margin on top of the card's own gap.

Two facts the text diet cut too close, both restored inside the guideline:
that an excluded injection site wins over a medication that prefers it, and
that the privacy card's second button leads to a full health record.
2026-08-07 23:20:39 +02:00
Marc-André Bombeck 5a103ea096 test(e2e): measure that Settings and Admin read as one surface
Thirty-four routes, both consoles, English and German, desktop and phone.
The claims this program makes are geometric, so they are checked with
geometry rather than by looking at screenshots.

What it found, which is the reason it exists:

- `SettingsCard as="section"` sat one spacing step tighter than its
  neighbour on every desktop width. The `<Card>`-backed variant inherits
  `gap-4 md:gap-6`; the landmark variant wrote a flat `gap-4`. Seventy-five
  of eighty-six cards measured 24 px and eleven measured 16 px, directly
  above and below each other. The expected value in the spec is a function
  of the viewport for exactly this reason — a constant would have agreed
  with whichever half it was written against.
- One German page subtitle wrapped to a second line where English fit on
  one. Shortened in all six languages.

After both: heading top 24.0 px on every route, spread 0.0 px, in both
languages and at both widths — that is the no-jump claim, measured. Card
header-to-body distance equals the card's own gap on all 86 cards, and no
card body sits off the card's inner edge.

The header's HEIGHT is pinned only where one line is achievable. At 393 px a
70-character sentence needs about 380 px of column, so most explainers wrap
there; the spec asserts the wrap away at desktop width and only forbids a
third line on a phone. The alternative is truncating the sentence, which is
the defect this program removed.
2026-08-07 22:16:31 +02:00
Marc-André Bombeck ba0cd63cf5 refactor(i18n): a description is one sentence, in all six languages
Measured before the sweep: 317 description and hint strings under the
`settings` and `admin` namespaces, median 83 characters, 113 of them two
sentences or more. The rule the design standards now carry is one sentence,
guideline 120 characters, and this is the sweep that makes it true.

Almost nothing was retranslated. A second sentence is already written in
every language — it is just in the wrong slot. `scripts/split-description.mjs`
cuts a description at its first sentence boundary in all six bundles at once
and either drops the tail or files it under its own key; the twenty-one keys
that moved now render in the card body as foreground `text-sm`, which is
where multi-sentence prose belongs and where a reader can actually read it
(the slot is muted `text-xs`). Eight tails were dropped outright, because
"Off by default" under a switch, "Topics and frequency are adjustable" above
the topic switches, and "Only admins can create invitations" on an
admin-gated route are not information.

Twenty-eight keys are gone: the privacy page's intro, which was a table of
contents for the four cards under it; two About descriptions that listed
verbatim the labels and the buttons beneath them; a card description that
repeated its own title; the API-token sentence that said what the paragraph
below it said with the actual instruction; and the eight integration
secondary sentences. Six descriptions were rewritten shorter in all six
languages where trimming was honest rather than lossy.

On the strings that changed: 4391 characters of English down to 2290, and 32
multi-sentence descriptions down to zero.

The rule is now a gate. `card-description-length.test.ts` reads the keys that
actually reach a card header's description slot — statically, from the JSX —
and checks them in EN and DE, because a rule that only holds in English
produces a slot that wraps to two lines in half the locales. Its exception
list is empty and carries a reason requirement plus a test that removes a
stale entry. Verified by putting a second sentence back: it fails.
2026-08-07 20:53:05 +02:00