feat(i18n): add Russian (ru) locale

Full Russian catalog (11,154 keys, 1:1 with en, identical nesting) and every
locale wiring point the app declares, so ru is a first-class locale instead
of an English fallback:

- messages/ru.json, project.inlang/settings.json, i18n boot script
- i18n config / load-locale, format-locale (ru-RU), date-fns ru
- labs: parse-reference-range RANGE_PROSE for Russian lab sheets
  ("3,5 - 5,0", "от 3,5 до 5,0", "до 5,6", "не более 5,6")
- AI: citation-coverage normative stems, coach system prompt and insights
  reply footer (Russian prompts must override the hard-coded
  "Respond in English"), population-norm patterns
- safety: outbound-screen Russian imperative/dose/unit/causality bank
- mental health: PHQ-9, GAD-7, WHO-5 and SCI wording

verify-message-catalogs.mjs passes for all 10 locales.
This commit is contained in:
l1137l committed 2026-11-01 17:11:31 +00:00
1 parent 577e43004f
commit 6b9d222643
24 files changed
+14551 -6

No files matched your search

+14098
View File
File diff suppressed because it is too large. Load diff
+12 -1
View File
@@ -1,7 +1,18 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"locales": ["en", "de", "fr", "es", "it", "pl", "ko", "nl", "pt-BR"],
"locales": [
"en",
"de",
"fr",
"es",
"it",
"pl",
"ko",
"nl",
"pt-BR",
"ru"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4.5.5/dist/index.js"
],
+1 -1
View File
@@ -28,7 +28,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(__dirname, "..");
const outDir = join(repoRoot, "public", "i18n");
const LOCALES = ["de", "en", "fr", "es", "it", "pl", "ko", "nl", "pt-BR"];
const LOCALES = ["de", "en", "fr", "es", "it", "pl", "ko", "nl", "pt-BR", "ru"];
mkdirSync(outDir, { recursive: true });
+3 -1
View File
@@ -1,4 +1,4 @@
import { de, enUS, es, fr, it, ko, nl, pl, ptBR } from "date-fns/locale";
import { de, enUS, es, fr, it, ko, nl, pl, ptBR, ru } from "date-fns/locale";
import type { Locale as DateFnsLocale } from "date-fns";
import type { Locale } from "@/lib/i18n/config";
@@ -27,6 +27,8 @@ export function resolveDateFnsLocale(locale: Locale): DateFnsLocale {
return nl;
case "pt-BR":
return ptBR;
case "ru":
return ru;
default:
return enUS;
}
+18
View File
@@ -178,6 +178,24 @@ const NORMATIVE_KEYWORDS: Record<Locale, readonly string[]> = {
"superior a",
"inferior a",
],
// Russian replies state a target or a reference interval; the stems carry
// the case endings (целевой/целевого, референсный/референсного), and "выше"/
// "ниже" are the comparatives a recommendation actually uses. ё folds to е.
ru: [
"целев",
"цель",
"должн",
"следует",
"рекоменд",
"нормальн",
"норма",
"референс",
"выше",
"ниже",
"больше",
"меньше",
"в пределах",
],
};
/**
@@ -15,6 +15,7 @@ import { patterns as pl } from "./pl";
import { patterns as nl } from "./nl";
import { patterns as ptBR } from "./pt-br";
import { patterns as ko } from "./ko";
import { patterns as ru } from "./ru";
export const POPULATION_NORM_PATTERNS: Readonly<
Record<Locale, readonly RegExp[]>
@@ -28,4 +29,5 @@ export const POPULATION_NORM_PATTERNS: Readonly<
nl,
"pt-BR": ptBR,
ko,
ru,
};
+11
View File
@@ -0,0 +1,11 @@
/** Population-norm framing, ru. */
export const patterns: readonly RegExp[] = [
/\bнормальн\w*\s+(?:диапазон|предел|значени|границ)\w*/i,
/\b(?:референсн|референс)\w*\s+(?:интервал|диапазон|значени|границ)\w*/i,
/\b(?:большинств|все|многие)\s+(?:люд|взросл)\w*/i,
/\b(?:взросл|люд)\w*[^.?!]{0,15}(?:обычно|как\s+правило|в\s+среднем)\b/i,
/\b(?:врач|специалист|эксперт|рекомендаци)\w*[^.?!]{0,20}(?:говор|рекоменду|счита)\w*/i,
/\b(?:общ|широк)\w*\s+(?:популяци|выборк)\w*/i,
/\b(?:средн|типичн|здоров)\w*\s+(?:показател|уровень|значени|норм)\w*/i,
/\bв\s+среднем\s+по\s+популяции\b/i,
];
+8 -2
View File
@@ -1258,6 +1258,7 @@ const LOCALE_REPLY_FOOTER_FALLBACK: Record<
pl: "\n\nREPLY LANGUAGE: respond in Polish. Mirror the user's register (formal Pan/Pani for medical-adjacent topics); use natural Polish health vocabulary.",
nl: "\n\nREPLY LANGUAGE: respond in Dutch. Use the informal je/jij register throughout (never formal u); use natural Dutch health vocabulary.",
ko: "\n\nREPLY LANGUAGE: respond in Korean. Use the 해요체 register throughout (never 합쇼체 or plain 한다체); use natural Korean health vocabulary.",
ru: "\n\nREPLY LANGUAGE — THIS OVERRIDES THE LANGUAGE RULE ABOVE. Any \"Respond in English\" instruction earlier in this prompt does NOT apply: respond in Russian (русский язык), address the user with «вы», use natural Russian health vocabulary and the standard Russian lab terminology. Never answer in English.",
"pt-BR":
"\n\nREPLY LANGUAGE: respond in Brazilian Portuguese (pt-BR, never European Portuguese). Address the user as você throughout; use natural Brazilian health vocabulary.",
};
@@ -1297,8 +1298,13 @@ export function getCoachSystemPrompt(
base = COACH_PROMPT_DE;
} else if (locale === "en") {
base = COACH_PROMPT_EN;
} else if (locale === "ko" || locale === "nl" || locale === "pt-BR") {
// ko, nl and pt-BR ship no safety-contract matrix, so they are not a
} else if (
locale === "ko" ||
locale === "nl" ||
locale === "pt-BR" ||
locale === "ru"
) {
// ko, nl, pt-BR and ru ship no safety-contract matrix, so they are not a
// NativeCoachLocale.
base = COACH_PROMPT_EN + LOCALE_REPLY_FOOTER_FALLBACK[locale];
} else {
+6 -1
View File
@@ -994,7 +994,12 @@ export function getStrictInsightsSystemPrompt(locale: Locale): string {
if (locale === "en") return SYSTEM_PROMPT_EN;
// ko, nl and pt-BR ship no safety-contract matrix, so they are not a
// NativeCoachLocale.
if (locale === "ko" || locale === "nl" || locale === "pt-BR") {
if (
locale === "ko" ||
locale === "nl" ||
locale === "pt-BR" ||
locale === "ru"
) {
return SYSTEM_PROMPT_EN + INSIGHTS_LOCALE_REPLY_FOOTER_FALLBACK[locale];
}
try {
@@ -16,6 +16,7 @@ export const INSIGHTS_LOCALE_REPLY_FOOTER_FALLBACK: Record<
pl: "\n\nREPLY LANGUAGE: render all user-facing strings in Polish. Use natural Polish health vocabulary with formal Pan/Pani register for medical-adjacent topics. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
nl: "\n\nREPLY LANGUAGE: render all user-facing strings in Dutch, in the informal je/jij register throughout (never formal u). Use natural Dutch health vocabulary. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
ko: "\n\nREPLY LANGUAGE: render all user-facing strings in Korean, in the 해요체 register throughout (never 합쇼체 or plain 한다체). Use natural Korean health vocabulary. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
ru: "\n\nREPLY LANGUAGE — THIS OVERRIDES THE LANGUAGE RULE ABOVE. The \"Respond in English\" instruction earlier in this prompt does NOT apply to this request: write EVERY user-facing string in Russian (русский язык), with natural Russian medical vocabulary and the standard Russian lab terminology, addressing the user with «вы». Never answer in English. The severity / sourceWindow / sourceMetric / topic enum values still stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
"pt-BR":
"\n\nREPLY LANGUAGE: render all user-facing strings in Brazilian Portuguese (pt-BR, never European Portuguese), addressing the user as você throughout. Use natural Brazilian health vocabulary. The severity / sourceWindow / sourceMetric / topic enum values stay in lowercase English exactly as listed in OUTPUT FORMAT — those are contract keys, NOT translations.",
};
+2
View File
@@ -51,6 +51,7 @@ import { bank as pl } from "./outbound-screen/locales/pl";
import { bank as nl } from "./outbound-screen/locales/nl";
import { bank as ko } from "./outbound-screen/locales/ko";
import { bank as ptBR } from "./outbound-screen/locales/pt-br";
import { bank as ru } from "./outbound-screen/locales/ru";
import {
DOSE_UNIT,
INN_STEM,
@@ -85,6 +86,7 @@ const LOCALE_BANKS: Readonly<Record<Locale, LocaleBank>> = {
nl,
ko,
"pt-BR": ptBR,
ru,
};
/** One value per locale, read off its bank. */
@@ -0,0 +1,149 @@
/** Outbound-screen banks for Russian, composed by `../../outbound-screen.ts`. */
import {
DOSE_UNIT,
DRUG_UNIT,
UNIT_END,
ENGINE,
medicationExtras,
unlessDeferred,
type LocaleBank,
} from "../shared";
const RU_MED_NOUN =
"(?:доз[аыуей]|дозу|таблетк\\w*|капсул\\w*|препарат\\w*|лекарств\\w*|инсулин\\w*|инъекц\\w*|укол\\w*|средств\\w*|терапи\\w*|при[её]м\\w*)";
/** Russian dosage-form counters, with the case endings an object takes. */
const RU_COUNT = "(?:таблетк\\w*|капсул\\w*|капл\\w*|доз\\w*|единиц\\w*|ед\\.?|мл)";
const RU_TARGET = `(?:[\\d.,]+(?:\\s*${DRUG_UNIT}${UNIT_END}|\\s*${RU_COUNT})|(?:одну|одна|две|два|три|четыре|половину|полторы|пол)\\s*${RU_COUNT})`;
/**
* A Russian imperative or recommendation that closes a dose clause. The
* prohibitive forms ("не увеличивайте", "не снижайте") never reach the verb
* from a negation-aware lead, and the talking-about forms ("увеличил") carry a
* different ending.
*/
const RU_IMPERATIVE =
"(?:(?:увеличьте|увеличь|повысьте|повысь|поднимите|подними|нарастите|снизьте|снизь|уменьшите|уменьши|понизьте|понизь|опустите|опусти|сократите|сократи|удвойте|удвой|уполовиньте|разделите|пропустите|пропусти|отмените|отмени|прекратите|прекрати|примите|принимайте|начните|начинайте|перейдите))";
// Russian writes "12%" with digits; the spelled-out counters ("двенадцать
// процентов") are rare in model prose and collide with the numeral declension,
// so only the digit forms are matched.
const QUAL_NUM_RU = "(?:\\d{1,3}\\s*(?:%|процент\\w*))";
const RISK_NOUN_RU = "(?:риск\\w*|вероятност\\w*|шанс\\w*)";
const HORIZON_RU =
"(?:(?:10|десяти)[- ]?летн\\w*|пожизненн\\w*)\\s*(?:риск\\w*|сердечно[- ]сосудист\\w*|смертност\\w*)";
const RESULT_VERB_RU =
"(?:относит\\w*|попада\\w*|классифицир\\w*|перевод\\w*|помеща\\w*|оказыва\\w*\\s*в)";
const RISK_BAND_RU =
"(?:высок|повышенн|промежуточн|умеренн|низк)\\w*\\s*(?:риск\\w*|категор\\w*|групп\\w*|диапазон\\w*|полос\\w*)";
const RISK_LEVEL_RU = "(?:высок|повышен|умеренн|низк|пограничн|тревожн)";
const RISK_VERDICT_RU = `(?:[—–-]|это|является|находится|означает)\\s*[^.?!]{0,10}${RISK_LEVEL_RU}`;
const RU_MED_CLASS =
"(?:антикоагулянт\\w*|антиагрегант\\w*|статины|статин\\w*|бета[- ]блокатор\\w*|диуретик\\w*|антидепрессант\\w*|метформин\\w*|инсулин\\w*)";
/** Signals that the number-free change shapes stand down for (`unlessDeferred`). */
const RU_DEFERRAL = "\\?|врач\\w*|доктор\\w*|специалист\\w*|эндокринолог\\w*|фармацевт\\w*|обсуди\\w*|посовету\\w*|спроси\\w*|если\\s+только|не\\s+";
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${RU_MED_NOUN}|(?:${medicationExtras(RU_MED_CLASS, names, false)})\\S{0,3})`,
doseChangeClass: (med) => [
// A stated amount closed by an imperative or a recommendation:
// "2000 мг увеличьте", "принимайте 2 таблетки".
new RegExp(`${RU_TARGET}[^.?!]{0,25}${RU_IMPERATIVE}`, "i"),
// A medication object halved, doubled, skipped or stopped by imperative.
new RegExp(
`${med}[^.?!]{0,20}(?:(?:вдвое|в\\s*два\\s*раза|пополам|вдвое\\s+меньше|наполовину)\\s*(?:увеличьте|увеличь|снизьте|снизь|уменьшите|уменьши|сократите|разделите|удвойте)|(?:пропустите|пропусти|пропускать|отмените|отмени|прекратите|прекрати|бросьте))`,
),
new RegExp(
`(?:доз[уа]|при[её]м|инъекци\\w*|укол\\w*)\\s*(?:пропустите|пропусти|пропускать|отмените|отмени|прекратите|прекрати)`,
),
// One more dose of a medication, closed by an imperative:
// "примите ещё одну таблетку метформина".
new RegExp(
`${med}[^.?!]{0,12}(?:ещ[её]|дополнительн\\w*|лишн\\w*)\\s*(?:одну|одна|две|дозу|таблетку|капсулу)?\\s*(?:примите|принимайте|выпейте|прими)`,
),
// A medication object raised or lowered by imperative with no amount,
// standing down where a question, a negation or a clinician appears.
unlessDeferred(
RU_DEFERRAL,
new RegExp(
`${med}[^.?!\\d]{0,8}?(?:увеличьте|увеличь|повысьте|повысь|поднимите|подними|снизьте|снизь|уменьшите|уменьши|понизьте|понизь|опустите|опусти|сократите|сократи)`,
),
),
],
// A target dose: the unit sits on the number and a change verb or a
// recommending lead-in frames it. The permitted restatement ("вы на 7,5 мг
// на этой неделе") names no change verb and does not trip.
dose: [
new RegExp(
`[\\d.,]+\\s*${DOSE_UNIT}\\s*(?:до|вплоть\\s*до)[^.?!]{0,20}(?:увелич|повыс|подним|нараст|начинай|переход|сниз|уменьш|пониз|опуст|сократ)`,
"i",
),
new RegExp(
`(?:увелич|повыс|подним|нараст|сниз|уменьш|пониз|опуст|сократ)\\w*[^.?!]{0,40}[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}`,
"i",
),
new RegExp(
`(?:рекоменд|совету|предлаг|стоит|нужно|следует|можно)[^.?!]{0,40}[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}`,
"i",
),
new RegExp(
`[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}[^.?!]{0,40}(?:рекоменд|совету|предлаг|стоит|принимайте|примите|начните|перейдите)`,
"i",
),
new RegExp(
`(?:следующ|нов|целев|рабоч)\\w*\\s*(?:доз|этап|титрац)\\w*[^.?!]{0,30}[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}`,
"i",
),
unlessDeferred(
RU_DEFERRAL,
/(?:доз|таблетк|капсул|инсулин|препарат|лекарств|приём|прием)[^.?!]{0,25}(?:вдвое|в два раза|пополам|пропуст|отмен|прекрат|удво|уполовин)[^.?!]{0,40}(?:недел|дн|месяц|попробуйте|можно)/,
),
],
risk: [
new RegExp(`${RISK_NOUN_RU}[^.?!]{0,15}${QUAL_NUM_RU}`, "i"),
new RegExp(`${QUAL_NUM_RU}[^.?!]{0,15}${RISK_NOUN_RU}`, "i"),
new RegExp(`${HORIZON_RU}[^.?!]{0,40}${QUAL_NUM_RU}`, "i"),
new RegExp(`${QUAL_NUM_RU}[^.?!]{0,40}${HORIZON_RU}`, "i"),
new RegExp(`\\b${ENGINE}\\b[^.?!]{0,50}${QUAL_NUM_RU}`, "i"),
new RegExp(`${QUAL_NUM_RU}[^.?!]{0,50}\\b${ENGINE}\\b`, "i"),
new RegExp(
`(?:\\b${ENGINE}\\b|${HORIZON_RU})[^.?!]{0,60}${RESULT_VERB_RU}`,
"i",
),
new RegExp(
`(?:\\b${ENGINE}\\b|${HORIZON_RU})[^.?!]{0,60}${RISK_BAND_RU}`,
"i",
),
new RegExp(
`(?:\\b${ENGINE}\\b|${HORIZON_RU})[^.?!]{0,40}${RISK_VERDICT_RU}`,
"i",
),
],
causal: [
/потому\s+что/i,
/из[- ]за/i,
/причин\w*/i,
/вызван\w*/i,
/приводит\s+к/i,
/привел[оаи]\s+к/i,
/вследствие/i,
/обусловлен\w*/i,
/следстви\w*/i,
/благодаря/i,
/явля\w+\s+(?:причиной|результатом)/i,
],
continuation:
/(?:продолжайте|продолжайте\s+принимать|продолжить\s+при[её]м|как\s+назначено|по\s+назначению|как\s+обычно|как\s+и\s+раньше|в\s+той\s+же\s+дозе|не\s+меняйте)/i,
changeStem:
/(?:увелич|повыс|подним|нараст|сниз|уменьш|пониз|опуст|сократ|удво|уполовин|пропуст|отмен|прекрат|перейд|начнит|измен|титр)/i,
};
+1
View File
@@ -33,6 +33,7 @@ const INTL_LOCALE_MAP: Record<Locale, string> = {
ko: "ko-KR",
nl: "nl-NL",
"pt-BR": "pt-BR",
ru: "ru-RU",
};
export function resolveIntlLocale(locale: Locale): string {
+3
View File
@@ -8,6 +8,7 @@ export const locales = [
"ko",
"nl",
"pt-BR",
"ru",
] as const;
export type Locale = (typeof locales)[number];
@@ -68,6 +69,7 @@ export const localeLanguageNames: Record<Locale, string> = {
ko: "Korean",
nl: "Dutch",
"pt-BR": "Brazilian Portuguese",
ru: "Russian",
};
export const localeLabels: Record<Locale, string> = {
@@ -80,6 +82,7 @@ export const localeLabels: Record<Locale, string> = {
ko: "한국어",
nl: "Nederlands",
"pt-BR": "Português (Brasil)",
ru: "Русский",
};
/**
+1
View File
@@ -56,6 +56,7 @@ const loaders: Record<Locale, () => Promise<{ default: MessageBundle }>> = {
ko: () => import("../../../messages/ko.json"),
nl: () => import("../../../messages/nl.json"),
"pt-BR": () => import("../../../messages/pt-BR.json"),
ru: () => import("../../../messages/ru.json"),
};
function initialCache(): Partial<Record<Locale, MessageBundle>> {
+39
View File
@@ -323,6 +323,45 @@ export const RANGE_PROSE: Record<Locale, RangeProse> = {
lower: ["최소", "최저"],
filler: ["참고치", "참고범위", "정상치", "정상범위", "기준치", "기준범위"],
},
// Russian lab sheets print "3,5 - 5,0", "от 3,5 до 5,0", "до 5,6",
// "не более 5,6", "не менее 3,5" — "от"/"до" are both a bound prefix and a
// filler, which is why they appear in all three lists.
ru: {
twoSided: ["до", "по"],
upper: [
"до",
"не более",
"не выше",
"менее",
"меньше",
"ниже",
"макс.",
"макс",
"максимум",
],
lower: [
"от",
"не менее",
"не ниже",
"более",
"больше",
"выше",
"мин.",
"мин",
"минимум",
],
filler: [
"от",
"до",
"референс",
"референсные значения",
"референсный интервал",
"норма",
"показатели нормы",
"реф.",
"реф",
],
},
};
/** Escape a literal for regex use and let any run of whitespace join its words. */
+35
View File
@@ -0,0 +1,35 @@
/**
* GAD-7, Russian: a working translation written for HealthLog (not a
* published validated one), faithful to the English original and on the same
* 0-3 scale.
*
* Источник: Рабочий перевод HealthLog с английского оригинала GAD-7 (Spitzer, Kroenke, Williams & Löwe, Arch Intern Med 2006;166:1092-1097). Валидированный русскоязычный вариант в открытом доступе отсутствует.
*/
import type { InstrumentWording } from "../types";
const wording: InstrumentWording = {
validated: false,
stems: {
period:
"За последние 2 недели как часто вас беспокоили следующие проблемы?",
},
items: [
"Вы чувствовали нервозность, тревогу или напряжённость",
"Вам не удавалось остановить или сдержать беспокойство",
"Вы слишком много беспокоились о разных вещах",
"Вам было трудно расслабиться",
"Вы были настолько беспокойны, что трудно было усидеть на месте",
"Вы легко раздражались",
"Вы боялись, что может случиться что-то ужасное",
],
anchors: {
all: {
0: "Совсем нет",
1: "Несколько дней",
2: "Больше половины дней",
3: "Почти каждый день",
},
},
};
export default wording;
+8
View File
@@ -35,6 +35,7 @@ import phq9Pl from "./phq9/pl";
import phq9Ko from "./phq9/ko";
import phq9Nl from "./phq9/nl";
import phq9PtBr from "./phq9/pt-br";
import phq9Ru from "./phq9/ru";
import gad7En from "./gad7/en";
import gad7De from "./gad7/de";
import gad7Es from "./gad7/es";
@@ -44,6 +45,7 @@ import gad7Pl from "./gad7/pl";
import gad7Ko from "./gad7/ko";
import gad7Nl from "./gad7/nl";
import gad7PtBr from "./gad7/pt-br";
import gad7Ru from "./gad7/ru";
import who5En from "./who5/en";
import who5De from "./who5/de";
import who5Es from "./who5/es";
@@ -53,6 +55,7 @@ import who5Pl from "./who5/pl";
import who5Ko from "./who5/ko";
import who5Nl from "./who5/nl";
import who5PtBr from "./who5/pt-br";
import who5Ru from "./who5/ru";
import sciEn from "./sci/en";
import sciDe from "./sci/de";
import sciEs from "./sci/es";
@@ -62,6 +65,7 @@ import sciPl from "./sci/pl";
import sciKo from "./sci/ko";
import sciNl from "./sci/nl";
import sciPtBr from "./sci/pt-br";
import sciRu from "./sci/ru";
export const WORDING: Readonly<
Record<InstrumentId, Readonly<Record<Locale, InstrumentWording>>>
@@ -76,6 +80,7 @@ export const WORDING: Readonly<
ko: phq9Ko,
nl: phq9Nl,
"pt-BR": phq9PtBr,
ru: phq9Ru,
},
GAD7: {
en: gad7En,
@@ -87,6 +92,7 @@ export const WORDING: Readonly<
ko: gad7Ko,
nl: gad7Nl,
"pt-BR": gad7PtBr,
ru: gad7Ru,
},
WHO5: {
en: who5En,
@@ -98,6 +104,7 @@ export const WORDING: Readonly<
ko: who5Ko,
nl: who5Nl,
"pt-BR": who5PtBr,
ru: who5Ru,
},
SCI: {
en: sciEn,
@@ -109,5 +116,6 @@ export const WORDING: Readonly<
ko: sciKo,
nl: sciNl,
"pt-BR": sciPtBr,
ru: sciRu,
},
};
+1
View File
@@ -18,6 +18,7 @@ const LOADERS: Record<Locale, () => Promise<{ default: LocaleWording }>> = {
ko: () => import("./locales/ko"),
nl: () => import("./locales/nl"),
"pt-BR": () => import("./locales/pt-br"),
ru: () => import("./locales/ru"),
};
export async function loadLocaleWording(
@@ -0,0 +1,10 @@
import gad7 from "../gad7/ru";
import phq9 from "../phq9/ru";
import sci from "../sci/ru";
import who5 from "../who5/ru";
import type { LocaleWording } from "../types";
/** Every questionnaire in Russian, loaded as one chunk. */
const wording: LocaleWording = { PHQ9: phq9, GAD7: gad7, WHO5: who5, SCI: sci };
export default wording;
+47
View File
@@ -0,0 +1,47 @@
/**
* PHQ-9, Russian: a working translation written for HealthLog (not a
* published validated one), faithful to the English original and on the same
* 0-3 scale, so scores stay comparable across languages.
*
* Источник: Рабочий перевод HealthLog с английского оригинала PHQ-9 (Kroenke, Spitzer & Williams, J Gen Intern Med 2001;16:606-613). Валидированный русскоязычный вариант в открытом доступе отсутствует.
*/
import type { InstrumentWording } from "../types";
const wording: InstrumentWording = {
validated: false,
stems: {
period:
"За последние 2 недели как часто вас беспокоили следующие проблемы?",
},
items: [
"Вам было неинтересно или не приносило удовольствия то, что вы делали",
"Вы чувствовали подавленность, угнетённость или безысходность",
"Вам было трудно заснуть, вы плохо спали или спали слишком много",
"Вы чувствовали усталость или упадок сил",
"У вас был плохой аппетит или вы переедали",
"Вы плохо думали о себе, считали себя неудачником или подводили себя и свою семью",
"Вам было трудно сосредоточиться, например читать или смотреть телевизор",
"Вы двигались или говорили заметно медленнее обычного — или, наоборот, были настолько суетливы и беспокойны, что двигались больше обычного",
"Вам приходили мысли, что вам лучше умереть, или о том, чтобы причинить себе вред",
],
anchors: {
all: {
0: "Совсем нет",
1: "Несколько дней",
2: "Больше половины дней",
3: "Почти каждый день",
},
},
functional: {
title:
"Если вы отметили какие-либо проблемы, насколько они затрудняли вам работу, домашние дела или общение с другими людьми?",
anchors: {
0: "Совсем не затрудняли",
1: "Несколько затрудняли",
2: "Сильно затрудняли",
3: "Очень сильно затрудняли",
},
},
};
export default wording;
+59
View File
@@ -0,0 +1,59 @@
/**
* SCI, Russian: a working translation written for HealthLog (not a published
* validated one), faithful to the eight items and their anchor sets.
*
* Источник: Рабочий перевод HealthLog с английского оригинала SCI (Espie CA, Kyle SD, Hames P, et al. The Sleep Condition Indicator. BMJ Open 2014;4:e004183).
*/
import type { InstrumentWording } from "../types";
const wording: InstrumentWording = {
validated: false,
stems: {
night: "Думая об обычной ночи за последний месяц…",
impact: "Думая о прошедшем месяце, в какой степени плохой сон…",
finally: "И последнее…",
},
items: [
"… сколько времени вам нужно, чтобы заснуть?",
"… если вы просыпаетесь ночью, сколько времени вы бодрствуете в сумме? (сложите все пробуждения)",
"… сколько ночей в неделю у вас есть проблема со сном?",
"… как бы вы оценили качество своего сна?",
"… влиял на ваше настроение, энергию или отношения?",
"… влиял на вашу концентрацию, продуктивность или способность бодрствовать?",
"… беспокоил вас в целом?",
"… как давно у вас есть проблема со сном?",
],
anchors: {
duration: {
0: "≥ 61 мин",
1: "46–60 мин",
2: "31–45 мин",
3: "16–30 мин",
4: "0–15 мин",
},
nights: { 0: "5–7", 1: "4", 2: "3", 3: "2", 4: "0–1" },
quality: {
0: "Очень плохое",
1: "Плохое",
2: "Среднее",
3: "Хорошее",
4: "Очень хорошее",
},
impact: {
0: "Очень сильно",
1: "Сильно",
2: "Умеренно",
3: "Немного",
4: "Совсем нет",
},
problemDuration: {
0: "> 1 года",
1: "7–12 месяцев",
2: "3–6 месяцев",
3: "1–2 месяца",
4: "Проблемы нет / < 1 месяца",
},
},
};
export default wording;
+32
View File
@@ -0,0 +1,32 @@
/**
* WHO-5, Russian: a working translation written for HealthLog (the WHO's own
* Russian version is under CC BY-NC-SA and not reproduced verbatim here),
* faithful to the five items and the 0-5 scale.
*
* Источник: Рабочий перевод HealthLog опросника ВОЗ-5 (World Health Organization. The World Health Organization-Five Well-Being Index (WHO-5). Geneva: WHO; 2024, WHO/UCN/MSD/MHE/2024.1).
*/
import type { InstrumentWording } from "../types";
const wording: InstrumentWording = {
validated: false,
stems: { period: "За последние две недели…" },
items: [
"Я чувствовал себя бодрым и в хорошем настроении",
"Я чувствовал себя спокойно и расслабленно",
"Я чувствовал себя активным и полным сил",
"Я просыпался свежим и отдохнувшим",
"Моя повседневная жизнь была наполнена тем, что мне интересно",
],
anchors: {
all: {
0: "Ни разу",
1: "Иногда",
2: "Меньше половины времени",
3: "Больше половины времени",
4: "Большую часть времени",
5: "Всё время",
},
},
};
export default wording;
+4
View File
@@ -34,6 +34,10 @@ const BARE_WORDS: Readonly<Record<Locale, BareWords>> = {
ko: { help: ["도움말", "도움"], greeting: ["안녕", "안녕하세요"] },
nl: { help: ["hulp"], greeting: ["hoi", "hallo", "goedemorgen"] },
"pt-BR": { help: ["ajuda"], greeting: ["oi", "ola", "bom dia"] },
ru: {
help: ["помощь", "помоги", "помогите"],
greeting: ["привет", "здравствуй", "здравствуйте", "добрый день"],
},
};
/** "Help", in every language the app ships. */