All checks were successful
CI / lint-and-test (push) Successful in 14s
- 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.
181 lines
5.5 KiB
JavaScript
181 lines
5.5 KiB
JavaScript
/**
|
||
* 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 - 0–11
|
||
* @returns {string}
|
||
*/
|
||
export function monthName(lang, month) {
|
||
const key = MONTH_KEYS[month];
|
||
return key ? t(lang, key) : "";
|
||
}
|
||
|
||
/**
|
||
* Get weekday short labels (Mon–Sun order) for given lang.
|
||
* @param {'ru'|'en'} lang
|
||
* @returns {string[]}
|
||
*/
|
||
export function weekdayLabels(lang) {
|
||
return WEEKDAY_KEYS.map((k) => t(lang, k));
|
||
}
|