Skip to content

feat(i18n): add Korean as a seventh end-to-end locale - #839

Merged
MBombeck merged 3 commits into
MBombeck:mainfrom
aucun6352:main
Aug 28, 2026
Merged

feat(i18n): add Korean as a seventh end-to-end locale#839
MBombeck merged 3 commits into
MBombeck:mainfrom
aucun6352:main

Conversation

@aucun6352

@aucun6352 aucun6352 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Added Korean to I18n

Type of change

Added I18n for Korean users

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation only
  • Test / CI / tooling
  • Refactor (no functional change)

Test plan

  • screenshots for UI changes.

Screenshots (UI changes only)

image

Checklist

  • Targets the main branch (trunk-based — see CONTRIBUTING.md)
  • pnpm typecheck passes locally
  • pnpm lint passes locally
  • pnpm test passes locally
  • pnpm format:check passes locally
  • pnpm build passes locally
  • User-facing strings go through t("key") with both messages/en.json and messages/de.json updated
  • No secrets, personal data, or maintainer-name references in committed files
  • Updated CHANGELOG.md if user-visible
  • Updated docs/audit/ or docs.healthlog.dev if behavior or self-hosting docs change

Linked issues

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.
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.

@MBombeck MBombeck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a serious piece of work, @aucun6352, and it shows. I ran the full gate locally and read every non-catalog diff. The short version: the engineering is mergeable, the clinical questionnaires are not yet, and one of the blockers is on my side of the fence rather than yours.

What I verified, so you know it was seen

The catalog is structurally impeccable: 8,290 of 8,290 keys present, zero placeholder mismatches against the runtime's actual substitution (I checked the apostrophe cases by hand), zero empty values, zero encoding artifacts. Every locale-keyed bank the type system demands got a real Korean arm, not a stub, and every one is exercised by a locale-looping test. The three changes that touch other locales are all correct and test-proven: the NFC recompose in the fold (identity for Latin, necessary for Hangul), the tilde range separators (both require digits on both sides, so "~5.0" as "approximately" cannot become a window), and the Hangul class in the lab-name normaliser. The five test files with deletions all extend six-locale lists to seven; nothing was weakened to get to green. Typecheck, lint, format, knip, openapi:check, all 22,289 tests, and the build pass on my machine.

The dose and risk regexes in the outbound screen deserve a particular mention. Verb-final clause order, the particle requirement mirroring what the Latin banks demand of "to"/"auf", the note that \b is dead next to Hangul: whoever wrote those read why the existing banks are shaped the way they are. That is rare in an external contribution of any size.

The blocker: the four questionnaires

PHQ-9, GAD-7, WHO-5 and the sleep questionnaire are validated psychometric instruments, and validated Korean versions of the first three exist in the literature. The PR translates the items freshly from English instead, and the differences are not cosmetic. The standardized Korean PHQ-9 (KJFM standardization, official Korean sheet) renders item 1 as "일 또는 여가 활동을 하는 데 흥미나 즐거움을 느끼지 못함"; this PR ships "무슨 일을 하는 데 흥미나 즐거움이 거의 없음". The response anchors diverge harder: the validated Korean instrument operationalizes anchor 2 as "7일 이상 방해 받았다", while this PR re-translates the English "more than half the days" as "절반 이상". Score comparability against every Korean study using the standardized version is gone at that point. GAD-7 has the same shape (validated Korean version, official sheet): close, but paraphrased where the validated text is fixed.

There is also a mechanical half. src/lib/mental-health/instruments.ts still lists six locales in ALL_APP_LOCALES, so hasValidatedItems(*, "ko") is false and the check-in wizard renders the note "this questionnaire is shown in the original English wording" while displaying your Korean items under it. The note becomes false on every Korean check-in.

And the sleep questionnaire has a written invariant directly above its locale list: validated in English only, so every non-EN locale presents the English items with an honest note, never a self-made translation. French and German keep all eight items byte-identical to English for that reason. The Korean translation of those eight has to come out regardless of what happens with the other three.

What would make this land:

  1. Replace the PHQ-9 and GAD-7 items and anchors with the officially validated Korean texts. Both instruments are in the public domain (the Pfizer grant quoted in instruments.ts covers redistribution), so this is a copy-the-right-words problem, not a licensing one. Then add "ko" to their validatedItemLocales.
  2. For WHO-5, the official Korean version exists via the WHO's distribution; I could not fetch the sheet to compare verbatim. If you have access to it, use it and say so; if not, the items revert to English with the honest note, same as the sleep questionnaire.
  3. The eight sleep-questionnaire items revert to English.

If you are a native speaker or have one at hand, a register pass over the wider catalog would also be welcome, though the app's own banner honestly marks Korean as community-maintained, so that is not a gate.

Two things on my side

The aggregate bundle budget fails at 3627 KB gz against a 3500 cap, entirely the seventh catalog. That number is mine to move, with the reasoning written next to it as with every raise before it, so leave bundle-budget.json untouched.

And whether a seventh locale is adopted at all is a standing commitment: every future string, every hand-maintained matcher bank, forever. Your PR makes the strongest possible case that the structure supports it. I want to be straight that the decision is still open rather than implied by a green pipeline.

One small thing

The two commits are authored as a tool rather than as you. Please re-author them under your own name or handle before this merges; the credit in the history should be yours, and this repository keeps tooling names out of its log as a matter of policy.

The six shared-allowlist entries in the english-leak guard would be cleaner as ko-scoped exemptions, since fr and es genuinely translate those keys today, but that is polish, not a gate.

Thank you for the care in this. Most locale PRs are a JSON file and a config line; this one read the codebase first.

The Korean questionnaire wording was translated freshly from English, so
a score built from it was comparable to nothing in the literature. Each
instrument now carries the text its own publisher put out, or no Korean
at all.

PHQ-9 and GAD-7 come verbatim from the instrument owner's distribution —
Pfizer's `PHQ9_Korean for Korea.pdf` (2010-06-15) and `GAD7_Korean for
Korea.pdf` (2010-05-18), the latter being the file Ahn/Kim/Choi 2019
names as what it validated. The sheets grant reproduction, translation,
display and distribution without permission. Verbatim means verbatim:
the spacing in "7 일 이상 방해 받았다" and the sheet's "안절부절 못하거나"
against the GAD-7 sheet's "안절부절못해서" are reproduced as printed.

WHO-5 has no official Korean arm. Neither the WHO's 2024 distribution
(26 languages) nor the Psychiatric Research Unit folder it inherited
from (31 languages) has ever carried one, and ePROVIDE lists the same
31. Korean therefore carries the published forward/back-translation
from Kim HJ et al., J Korean Geriatr Psychiatry 2010;14(2):90-96, which
is CC BY-NC and so redistributable; the attribution line names it. Its
validation sample was community-dwelling elderly rather than general
adults, which the definition says out loud.

The Sleep Condition Indicator reverts to English. It is validated in
English only, its cut-off was derived from those words, and no openly
redistributable Korean exists — so `ko` now matches de/es/fr/it/pl:
items, section stems and response anchors byte-identical to en.json,
under the localized "validated in English" note.

`ALL_APP_LOCALES` grows to seven, which is what stops the check-in
wizard claiming English wording over Korean items on every Korean
check-in.

One gap is deliberate and not yet resolved. The official Korean PHQ-9
and GAD-7 sheets disagree on anchor 2 — "7 일 이상 방해 받았다" against
"2 주 중 절반 이상 방해 받았다" — but both instruments read one shared
`mentalHealth.options` set, which holds for the other six locales
because their two sheets agree word for word. Korean takes the PHQ-9
wording for both here. GAD-7's anchors are therefore validated in
substance but not verbatim; splitting them into a `gad7Options`
namespace is the fix, and is left for the maintainer to weigh against
the 24 duplicated keys it would add across the other locales.

The functional-impairment item and its four anchors come from the same
PHQ-9 sheet. They are not scored and not covered by the
`validatedItemLocales` contract.
@MBombeck

Copy link
Copy Markdown
Owner

This is exactly the sourcing standard the review asked for, thank you. PHQ-9 and GAD-7 verbatim from the instrument owner's own Korean sheets, WHO-5 from a published, redistributable translation with the attribution and its validation-sample caveat stated in the definition, and SCI back to English in line with every other locale rather than an unvalidated stand-in.

I ran the full i18n guard battery against 31eb50e locally and it is green (call-site coverage, drift, English-leak, reference-matcher, reverse coverage, locale integrity).

What remains open before merge are the two maintainer decisions from the review; those sit on my side, not yours. I will resolve them and get back to you here.

@aucun6352

Copy link
Copy Markdown
Contributor Author

Thanks you got there before I posted this, so treat it as background rather than a question. Two points on how the sourcing landed, for the record.

GAD-7 anchors

In Korean, the official PHQ-9 and GAD-7 sheets word anchor 2 differently: 7 일 이상 방해 받았다 on the PHQ-9 sheet, 2 주 중 절반 이상 방해 받았다 on the GAD-7 one. The other six locales share one set because their two sheets agree word for word; Korean is where that breaks. Using the GAD-7 wording means splitting mentalHealth.options into a separate namespace and adding keys across all seven locales. This PR was meant to add Korean, not to restructure the instrument registry, so both instruments keep the PHQ-9 wording for now. I'll make the change if you want the GAD-7 wording.

WHO-5

There is no official Korean version. It is not among the 26 languages on the WHO's 2024 distribution, nor among the 31 published by the Psychiatric Centre North Zealand, which holds the original copyright. Since the WHO notice states every translation came from that centre, I concluded none is likely to appear.

I compared two alternatives. The Korean Working Conditions Survey (KWCS) carries WHO-5, but its questionnaire states on page 1 that copyright belongs to Eurofound and that use requires contacting the Occupational Safety and Health Research Institute, so I ruled it out. I used the published paper instead - it went through forward and back translation, and it is CC BY-NC 3.0.

Kim HJ, Moon YS, Son BK, Lee SK, Rho HJ, Kim DH. The Utility of Korean Version of the WHO Five Well-Being Index in Evaluating Depressive Symptoms and Quality of Life in the Aged Dwelling in Community. J Korean Geriatr Psychiatry 2010;14(2):90-96.
https://www.kci.go.kr/kciportal/ci/sereArticleSearch/ciSereArtiView.kci?sereArticleSearchBean.artiId=ART001514454
Licence: https://www.kci.go.kr/kciportal/landing/article.kci?arti_id=ART001514454

MBombeck added a commit that referenced this pull request Aug 28, 2026
feat: Korean locale (#839, @aucun6352) — the seventh interface language
@MBombeck
MBombeck merged commit 6ff81b2 into MBombeck:main Aug 28, 2026
@MBombeck

Copy link
Copy Markdown
Owner

Merged and live in v1.37.32. Thank you, @aucun6352 — this is a genuinely well-made contribution.

It is not just a message catalog. You handled the parts a translation usually gets wrong: decomposed Hangul jamo now recompose in the shared fold so search and comparison match, Korean lab names normalise onto their LOINC keys, the tilde reads as a range separator the way Korean written numbers use it, and the questionnaires ship in their officially published Korean wording rather than an improvised one. That is the difference between a locale that exists and one that works.

Korean is now HealthLog's seventh interface language, sitting alongside German, English, Spanish, French, Italian and Polish. 감사합니다.

TimonBed pushed a commit to TimonBed/HealthLog that referenced this pull request Aug 30, 2026
Integrates @aucun6352's Korean locale (PR MBombeck#839): the full catalog, the
NFC recompose in the shared fold, the Hangul lab-name normaliser, the
tilde range separator, and the officially published Korean PHQ-9, GAD-7
and WHO-5 wording. Resolves the CHANGELOG and OpenAPI conflicts against
the current trunk and regenerates the spec. Raises the aggregate bundle
budget to 3700 KB gz for the seventh message catalog, with the reasoning
recorded next to the value.

The contributor's three commits are preserved with their authorship.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants