Files
duty-teller/webapp/js/i18n.js
Nikolay Tatarinov 4c2d95e776
All checks were successful
CI / lint-and-test (push) Successful in 14s
feat: implement internationalization for duty calendar
- Introduced a new `i18n.js` module for handling translations and language detection, supporting both Russian and English.
- Updated various components to utilize the new translation functions, enhancing user experience by providing localized messages for errors, hints, and UI elements.
- Refactored existing code in `api.js`, `calendar.js`, `dutyList.js`, `hints.js`, and `ui.js` to replace hardcoded strings with translated messages, improving maintainability and accessibility.
- Enhanced the initialization process in `main.js` to set the document language and update UI elements based on the user's language preference.
2026-02-19 16:00:00 +03:00

181 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Internationalization: language detection and translations for webapp.
*/
import { getInitData } from "./auth.js";
/** @type {Record<string, Record<string, string>>} */
export const MESSAGES = {
en: {
"app.title": "Duty Calendar",
loading: "Loading…",
access_denied: "Access denied.",
error_load_failed: "Load failed",
error_network: "Could not load data. Check your connection.",
error_generic: "Could not load data.",
"nav.prev_month": "Previous month",
"nav.next_month": "Next month",
"weekdays.mon": "Mon",
"weekdays.tue": "Tue",
"weekdays.wed": "Wed",
"weekdays.thu": "Thu",
"weekdays.fri": "Fri",
"weekdays.sat": "Sat",
"weekdays.sun": "Sun",
"month.jan": "January",
"month.feb": "February",
"month.mar": "March",
"month.apr": "April",
"month.may": "May",
"month.jun": "June",
"month.jul": "July",
"month.aug": "August",
"month.sep": "September",
"month.oct": "October",
"month.nov": "November",
"month.dec": "December",
"aria.duty": "On duty",
"aria.unavailable": "Unavailable",
"aria.vacation": "Vacation",
"aria.day_info": "Day info",
"event_type.duty": "Duty",
"event_type.unavailable": "Unavailable",
"event_type.vacation": "Vacation",
"duty.now_on_duty": "On duty now",
"duty.none_this_month": "No duties this month.",
"duty.today": "Today",
"duty.until": "until {time}",
"hint.from": "from",
"hint.to": "until",
"hint.duty_title": "Duty:",
"hint.events": "Events:"
},
ru: {
"app.title": "Календарь дежурств",
loading: "Загрузка…",
access_denied: "Доступ запрещён.",
error_load_failed: "Ошибка загрузки",
error_network: "Не удалось загрузить данные. Проверьте интернет.",
error_generic: "Не удалось загрузить данные.",
"nav.prev_month": "Предыдущий месяц",
"nav.next_month": "Следующий месяц",
"weekdays.mon": "Пн",
"weekdays.tue": "Вт",
"weekdays.wed": "Ср",
"weekdays.thu": "Чт",
"weekdays.fri": "Пт",
"weekdays.sat": "Сб",
"weekdays.sun": "Вс",
"month.jan": "Январь",
"month.feb": "Февраль",
"month.mar": "Март",
"month.apr": "Апрель",
"month.may": "Май",
"month.jun": "Июнь",
"month.jul": "Июль",
"month.aug": "Август",
"month.sep": "Сентябрь",
"month.oct": "Октябрь",
"month.nov": "Ноябрь",
"month.dec": "Декабрь",
"aria.duty": "Дежурные",
"aria.unavailable": "Недоступен",
"aria.vacation": "Отпуск",
"aria.day_info": "Информация о дне",
"event_type.duty": "Дежурство",
"event_type.unavailable": "Недоступен",
"event_type.vacation": "Отпуск",
"duty.now_on_duty": "Сейчас дежурит",
"duty.none_this_month": "В этом месяце дежурств нет.",
"duty.today": "Сегодня",
"duty.until": "до {time}",
"hint.from": "с",
"hint.to": "до",
"hint.duty_title": "Дежурство:",
"hint.events": "События:"
}
};
const MONTH_KEYS = [
"month.jan", "month.feb", "month.mar", "month.apr", "month.may", "month.jun",
"month.jul", "month.aug", "month.sep", "month.oct", "month.nov", "month.dec"
];
const WEEKDAY_KEYS = [
"weekdays.mon", "weekdays.tue", "weekdays.wed", "weekdays.thu",
"weekdays.fri", "weekdays.sat", "weekdays.sun"
];
/**
* Normalize language code to 'ru' or 'en'.
* @param {string} code - e.g. 'ru', 'en', 'uk'
* @returns {'ru'|'en'}
*/
function normalizeLang(code) {
if (!code || typeof code !== "string") return "ru";
const lower = code.toLowerCase();
if (lower.startsWith("ru")) return "ru";
return "en";
}
/**
* Detect language: Telegram initData user.language_code → navigator → fallback 'ru'.
* @returns {'ru'|'en'}
*/
export function getLang() {
const initData = getInitData();
if (initData) {
try {
const params = new URLSearchParams(initData);
const userStr = params.get("user");
if (userStr) {
const user = JSON.parse(decodeURIComponent(userStr));
const code = user && user.language_code;
if (code) return normalizeLang(code);
}
} catch (e) {
/* ignore */
}
}
const nav = navigator.language || (navigator.languages && navigator.languages[0]) || "";
return normalizeLang(nav);
}
/**
* Get translated string; fallback to en if key missing in lang. Supports {placeholder}.
* @param {'ru'|'en'} lang
* @param {string} key - e.g. 'app.title', 'duty.until'
* @param {Record<string, string>} [params] - e.g. { time: '14:00' }
* @returns {string}
*/
export function t(lang, key, params = {}) {
const dict = MESSAGES[lang] || MESSAGES.en;
let s = dict[key];
if (s === undefined) s = MESSAGES.en[key];
if (s === undefined) return key;
Object.keys(params).forEach((k) => {
s = s.replace(new RegExp("\\{" + k + "\\}", "g"), params[k]);
});
return s;
}
/**
* Get month name by 0-based index.
* @param {'ru'|'en'} lang
* @param {number} month - 011
* @returns {string}
*/
export function monthName(lang, month) {
const key = MONTH_KEYS[month];
return key ? t(lang, key) : "";
}
/**
* Get weekday short labels (MonSun order) for given lang.
* @param {'ru'|'en'} lang
* @returns {string[]}
*/
export function weekdayLabels(lang) {
return WEEKDAY_KEYS.map((k) => t(lang, k));
}