mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
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.
84 lines
2.4 KiB
JavaScript
84 lines
2.4 KiB
JavaScript
/**
|
|
* One-shot helper for a text sweep across the six locale bundles.
|
|
*
|
|
* node scripts/i18n-key-tool.mjs drop <dotted.key> [...]
|
|
* node scripts/i18n-key-tool.mjs set <locale> <dotted.key> <value>
|
|
* node scripts/i18n-key-tool.mjs show <dotted.key>
|
|
*
|
|
* Keys are dotted paths into the bundle; a segment containing a literal dot
|
|
* is not supported (no such key exists). `drop` removes the key from every
|
|
* locale and prunes an object left empty behind it.
|
|
*/
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
|
|
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko", "nl", "pt-BR", "ru"];
|
|
const file = (loc) => `messages/${loc}.json`;
|
|
|
|
function read(loc) {
|
|
return JSON.parse(readFileSync(file(loc), "utf8"));
|
|
}
|
|
function write(loc, data) {
|
|
writeFileSync(file(loc), `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
function drop(bundle, path) {
|
|
const parts = path.split(".");
|
|
const last = parts.pop();
|
|
const chain = [];
|
|
let node = bundle;
|
|
for (const part of parts) {
|
|
if (node == null || typeof node !== "object") return false;
|
|
chain.push([node, part]);
|
|
node = node[part];
|
|
}
|
|
if (node == null || !(last in node)) return false;
|
|
delete node[last];
|
|
for (let i = chain.length - 1; i >= 0; i -= 1) {
|
|
const [parent, key] = chain[i];
|
|
if (parent[key] && Object.keys(parent[key]).length === 0)
|
|
delete parent[key];
|
|
else break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function set(bundle, path, value) {
|
|
const parts = path.split(".");
|
|
const last = parts.pop();
|
|
let node = bundle;
|
|
for (const part of parts) {
|
|
if (typeof node[part] !== "object" || node[part] === null) node[part] = {};
|
|
node = node[part];
|
|
}
|
|
node[last] = value;
|
|
}
|
|
|
|
function get(bundle, path) {
|
|
return path.split(".").reduce((n, p) => (n == null ? n : n[p]), bundle);
|
|
}
|
|
|
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
|
|
if (cmd === "drop") {
|
|
for (const loc of LOCALES) {
|
|
const bundle = read(loc);
|
|
let n = 0;
|
|
for (const key of rest) if (drop(bundle, key)) n += 1;
|
|
write(loc, bundle);
|
|
console.log(`${loc}: dropped ${n}/${rest.length}`);
|
|
}
|
|
} else if (cmd === "set") {
|
|
const [loc, key, ...valueParts] = rest;
|
|
const bundle = read(loc);
|
|
set(bundle, key, valueParts.join(" "));
|
|
write(loc, bundle);
|
|
console.log(`${loc}: ${key} set`);
|
|
} else if (cmd === "show") {
|
|
for (const loc of LOCALES) {
|
|
console.log(`${loc}\t${JSON.stringify(get(read(loc), rest[0]))}`);
|
|
}
|
|
} else {
|
|
console.error("usage: drop|set|show");
|
|
process.exit(1);
|
|
}
|