feat: implement internationalization for duty calendar
All checks were successful
CI / lint-and-test (push) Successful in 14s
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:
@@ -2,8 +2,8 @@
|
||||
* Tooltips for calendar info buttons and duty markers.
|
||||
*/
|
||||
|
||||
import { calendarEl } from "./dom.js";
|
||||
import { EVENT_TYPE_LABELS } from "./constants.js";
|
||||
import { calendarEl, state } from "./dom.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { escapeHtml } from "./utils.js";
|
||||
import { localDateString, formatHHMM } from "./dateUtils.js";
|
||||
|
||||
@@ -90,10 +90,12 @@ function getItemFullName(item) {
|
||||
* @param {number} idx - index in dutyItems
|
||||
* @param {number} total - dutyItems.length
|
||||
* @param {string} hintDay - YYYY-MM-DD
|
||||
* @param {string} sep - separator after "с"/"до" (e.g. " - " for text, "\u00a0" for HTML)
|
||||
* @param {string} sep - separator after "from"/"to" (e.g. " - " for text, "\u00a0" for HTML)
|
||||
* @param {string} fromLabel - translated "from" prefix
|
||||
* @param {string} toLabel - translated "to" prefix
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildDutyItemTimePrefix(item, idx, total, hintDay, sep) {
|
||||
function buildDutyItemTimePrefix(item, idx, total, hintDay, sep, fromLabel, toLabel) {
|
||||
const startAt = item.start_at != null ? item.start_at : item.startAt;
|
||||
const endAt = item.end_at != null ? item.end_at : item.endAt;
|
||||
const endHHMM = endAt ? formatHHMM(endAt) : "";
|
||||
@@ -105,17 +107,17 @@ function buildDutyItemTimePrefix(item, idx, total, hintDay, sep) {
|
||||
let timePrefix = "";
|
||||
if (idx === 0) {
|
||||
if (total === 1 && startSameDay && startHHMM) {
|
||||
timePrefix = "с" + sep + startHHMM;
|
||||
timePrefix = fromLabel + sep + startHHMM;
|
||||
if (endSameDay && endHHMM && endHHMM !== startHHMM) {
|
||||
timePrefix += " до" + sep + endHHMM;
|
||||
timePrefix += " " + toLabel + sep + endHHMM;
|
||||
}
|
||||
} else if (endHHMM) {
|
||||
timePrefix = "до" + sep + endHHMM;
|
||||
timePrefix = toLabel + sep + endHHMM;
|
||||
}
|
||||
} else if (idx > 0) {
|
||||
if (startHHMM) timePrefix = "с" + sep + startHHMM;
|
||||
if (startHHMM) timePrefix = fromLabel + sep + startHHMM;
|
||||
if (endHHMM && endSameDay && endHHMM !== startHHMM) {
|
||||
timePrefix += (timePrefix ? " " : "") + "до" + sep + endHHMM;
|
||||
timePrefix += (timePrefix ? " " : "") + toLabel + sep + endHHMM;
|
||||
}
|
||||
}
|
||||
return timePrefix;
|
||||
@@ -126,16 +128,20 @@ function buildDutyItemTimePrefix(item, idx, total, hintDay, sep) {
|
||||
* @param {object[]} dutyItems
|
||||
* @param {string} hintDay
|
||||
* @param {string} timeSep - e.g. " - " for text, "\u00a0" for HTML
|
||||
* @param {string} fromLabel
|
||||
* @param {string} toLabel
|
||||
* @returns {{ timePrefix: string, fullName: string }[]}
|
||||
*/
|
||||
function getDutyMarkerRows(dutyItems, hintDay, timeSep) {
|
||||
function getDutyMarkerRows(dutyItems, hintDay, timeSep, fromLabel, toLabel) {
|
||||
return dutyItems.map((item, idx) => {
|
||||
const timePrefix = buildDutyItemTimePrefix(
|
||||
item,
|
||||
idx,
|
||||
dutyItems.length,
|
||||
hintDay,
|
||||
timeSep
|
||||
timeSep,
|
||||
fromLabel,
|
||||
toLabel
|
||||
);
|
||||
const fullName = getItemFullName(item);
|
||||
return { timePrefix, fullName };
|
||||
@@ -148,14 +154,17 @@ function getDutyMarkerRows(dutyItems, hintDay, timeSep) {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDutyMarkerHintContent(marker) {
|
||||
const lang = state.lang;
|
||||
const type = marker.getAttribute("data-event-type") || "duty";
|
||||
const label = EVENT_TYPE_LABELS[type] || type;
|
||||
const label = t(lang, "event_type." + type);
|
||||
const names = marker.getAttribute("data-names") || "";
|
||||
const fromLabel = t(lang, "hint.from");
|
||||
const toLabel = t(lang, "hint.to");
|
||||
let body;
|
||||
if (type === "duty") {
|
||||
const { dutyItems, hasTimes, hintDay } = parseDutyMarkerData(marker);
|
||||
if (dutyItems.length >= 1 && hasTimes) {
|
||||
const rows = getDutyMarkerRows(dutyItems, hintDay, " - ");
|
||||
const rows = getDutyMarkerRows(dutyItems, hintDay, " - ", fromLabel, toLabel);
|
||||
body = rows
|
||||
.map((r) =>
|
||||
r.timePrefix ? r.timePrefix + " - " + r.fullName : r.fullName
|
||||
@@ -176,14 +185,17 @@ export function getDutyMarkerHintContent(marker) {
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getDutyMarkerHintHtml(marker) {
|
||||
const lang = state.lang;
|
||||
const type = marker.getAttribute("data-event-type") || "duty";
|
||||
if (type !== "duty") return null;
|
||||
const names = marker.getAttribute("data-names") || "";
|
||||
const { dutyItems, hasTimes, hintDay } = parseDutyMarkerData(marker);
|
||||
const nbsp = "\u00a0";
|
||||
const fromLabel = t(lang, "hint.from");
|
||||
const toLabel = t(lang, "hint.to");
|
||||
let rowHtmls;
|
||||
if (dutyItems.length >= 1 && hasTimes) {
|
||||
const rows = getDutyMarkerRows(dutyItems, hintDay, nbsp);
|
||||
const rows = getDutyMarkerRows(dutyItems, hintDay, nbsp, fromLabel, toLabel);
|
||||
rowHtmls = rows.map((r) => {
|
||||
const timeHtml = r.timePrefix ? escapeHtml(r.timePrefix) : "";
|
||||
const sepHtml = r.timePrefix ? '<span class="calendar-event-hint-sep">-</span>' : '<span class="calendar-event-hint-sep"></span>';
|
||||
@@ -210,7 +222,7 @@ export function getDutyMarkerHintHtml(marker) {
|
||||
}
|
||||
if (rowHtmls.length === 0) return null;
|
||||
return (
|
||||
'<div class="calendar-event-hint-title">Дежурство:</div><div class="calendar-event-hint-rows">' +
|
||||
'<div class="calendar-event-hint-title">' + escapeHtml(t(lang, "hint.duty_title")) + '</div><div class="calendar-event-hint-rows">' +
|
||||
rowHtmls
|
||||
.map((r) => '<div class="calendar-event-hint-row">' + r + "</div>")
|
||||
.join("") +
|
||||
@@ -244,11 +256,12 @@ export function bindInfoButtonTooltips() {
|
||||
document.body.appendChild(hintEl);
|
||||
}
|
||||
if (!calendarEl) return;
|
||||
const lang = state.lang;
|
||||
calendarEl.querySelectorAll(".info-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const summary = btn.getAttribute("data-summary") || "";
|
||||
const content = "События:\n" + summary;
|
||||
const content = t(lang, "hint.events") + "\n" + summary;
|
||||
if (hintEl.hidden || hintEl.textContent !== content) {
|
||||
hintEl.textContent = content;
|
||||
const rect = btn.getBoundingClientRect();
|
||||
|
||||
Reference in New Issue
Block a user