fix(i18n): make the Korean locale actually work end to end

Registering `ko` in the `Locale` union left seven locale-keyed tables
without an entry, so the build could not type-check and the safety
screens, the citation-coverage grader and the reference-range parser
read nothing at all for a Korean reader.

Three failures were not missing keys but wrong assumptions:

- The shared fold decomposes with NFD and strips U+0300-U+036F. Latin
  accents become plain letters, but Hangul decomposes into jamo, which
  are not in that range and survived — so folded Korean text was a jamo
  sequence no precomposed table entry could ever match. It recomposes to
  NFC now; every Latin, Polish and German fold is byte-identical.
- `normaliseLabKey` kept `[a-z0-9]`, which reduced every Korean analyte
  name to the empty string, so the derived index held none of them and a
  Korean cholesterol result reached a FHIR export uncoded.
- The reference-range parser had the hyphen and the dashes but not the
  tilde a Korean lab actually prints (`3.5~5.0`).

The Korean safety banks carry no `\b` next to a Hangul token — `\b` is
an ASCII `\w` boundary and Hangul is not `\w`, so `/\b증량/` matches
nothing — and the clause order is the mirror of the Latin banks, since
Korean closes with the verb. The dose patterns require a target-marking
particle for the same reason the Latin banks require "to"/"by": without
it the permitted restatement "7.5mg을 복용 중이고, 체중은 내려가고
있어요" tripped the lowering pattern.

The Coach and Insights prompts route `ko` down the reviewed English body
plus a Korean reply directive; there is no `safety-contracts.ko.yaml`, so
the native builder has no Korean ground rules to compose from.
This commit is contained in:
aucun6352 committed 2026-08-26 20:00:44 +09:00
1 parent f1b0420479
commit f7cc47850a
29 files changed
+207 -18

No files matched your search

@@ -223,9 +223,9 @@ const FULL_RECORD = {
const DOTTED_KEY = /\b[a-z][A-Za-z0-9]*(\.[A-Za-z0-9_]+){2,}\b/g;
describe("<ClinicianView> resolves every label in every locale", () => {
it("covers all six shipped locales", () => {
it("covers all seven shipped locales", () => {
// A floor: an empty locale list would make the loop below assert nothing.
expect(locales.length).toBe(6);
expect(locales.length).toBe(7);
});
for (const locale of locales) {
@@ -41,13 +41,14 @@ const REQUIRED_KEYS = [
];
describe("measurement-reminder + bulk-share dynamic i18n keys", () => {
it("discovers all six shipped locales", () => {
it("discovers all seven shipped locales", () => {
expect(LOCALES.map((l) => l.locale).sort()).toEqual([
"de",
"en",
"es",
"fr",
"it",
"ko",
"pl",
]);
});
@@ -276,7 +276,7 @@ describe("the managed-profile card, in the shared-access section", () => {
/<select[^>]*data-slot="managed-profile-locale"[\s\S]*?<\/select>/
.exec(html)?.[0]
.match(/<option/g) ?? [];
expect(options).toHaveLength(6);
expect(options).toHaveLength(7);
});
it("lists the profiles this account looks after, and only those", () => {