feat: implement internationalization for duty calendar
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.
This commit is contained in:
2026-02-19 16:00:00 +03:00
parent a5f7a5a0ef
commit 4c2d95e776
9 changed files with 262 additions and 51 deletions

View File

@@ -3,7 +3,7 @@
*/
import { dutyListEl, state } from "./dom.js";
import { EVENT_TYPE_LABELS } from "./constants.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./utils.js";
import {
localDateString,
@@ -33,9 +33,10 @@ export function dutyTimelineCardHtml(d, isCurrent) {
} else {
timeStr = startDDMM + " " + startTime + " " + endDDMM + " " + endTime;
}
const lang = state.lang;
const typeLabel = isCurrent
? "Сейчас дежурит"
: (EVENT_TYPE_LABELS[d.event_type] || "Дежурство");
? t(lang, "duty.now_on_duty")
: (t(lang, "event_type." + (d.event_type || "duty")));
const extraClass = isCurrent ? " duty-item--current" : "";
return (
'<div class="duty-item duty-item--duty duty-timeline-card' +
@@ -59,15 +60,16 @@ export function dutyTimelineCardHtml(d, isCurrent) {
* @returns {string}
*/
export function dutyItemHtml(d, typeLabelOverride, showUntilEnd, extraClass) {
const lang = state.lang;
const typeLabel =
typeLabelOverride != null
? typeLabelOverride
: (EVENT_TYPE_LABELS[d.event_type] || d.event_type);
: (t(lang, "event_type." + (d.event_type || "duty")));
let itemClass = "duty-item duty-item--" + (d.event_type || "duty");
if (extraClass) itemClass += " " + extraClass;
let timeOrRange = "";
if (showUntilEnd && d.event_type === "duty") {
timeOrRange = "до " + formatTimeLocal(d.end_at);
timeOrRange = t(lang, "duty.until", { time: formatTimeLocal(d.end_at) });
} else if (d.event_type === "vacation" || d.event_type === "unavailable") {
const startStr = formatDateKey(d.start_at);
const endStr = formatDateKey(d.end_at);
@@ -98,7 +100,7 @@ export function renderDutyList(duties) {
const filtered = duties.filter((d) => d.event_type === "duty");
if (filtered.length === 0) {
dutyListEl.classList.remove("duty-timeline");
dutyListEl.innerHTML = '<p class="muted">В этом месяце дежурств нет.</p>';
dutyListEl.innerHTML = '<p class="muted">' + t(state.lang, "duty.none_this_month") + "</p>";
return;
}
dutyListEl.classList.add("duty-timeline");
@@ -121,7 +123,9 @@ export function renderDutyList(duties) {
"duty-timeline-day" + (isToday ? " duty-timeline-day--today" : "");
const dateLabel = dateKeyToDDMM(date);
const dateCellHtml = isToday
? '<span class="duty-timeline-date"><span class="duty-timeline-date-label">Сегодня</span><span class="duty-timeline-date-day">' +
? '<span class="duty-timeline-date"><span class="duty-timeline-date-label">' +
escapeHtml(t(state.lang, "duty.today")) +
'</span><span class="duty-timeline-date-day">' +
escapeHtml(dateLabel) +
'</span><span class="duty-timeline-date-dot" aria-hidden="true"></span></span>'
: '<span class="duty-timeline-date">' + escapeHtml(dateLabel) + "</span>";