diff --git a/webapp/js/api.js b/webapp/js/api.js index b97509a..58e0e90 100644 --- a/webapp/js/api.js +++ b/webapp/js/api.js @@ -4,15 +4,18 @@ import { FETCH_TIMEOUT_MS } from "./constants.js"; import { getInitData } from "./auth.js"; +import { state } from "./dom.js"; +import { t } from "./i18n.js"; /** - * Build fetch options with init data header and timeout abort. + * Build fetch options with init data header, Accept-Language and timeout abort. * @param {string} initData - Telegram init data * @returns {{ headers: object, signal: AbortSignal, timeoutId: number }} */ export function buildFetchOptions(initData) { const headers = {}; if (initData) headers["X-Telegram-Init-Data"] = initData; + headers["Accept-Language"] = state.lang || "ru"; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); return { headers, signal: controller.signal, timeoutId }; @@ -37,7 +40,7 @@ export async function fetchDuties(from, to) { try { const res = await fetch(url, { headers: opts.headers, signal: opts.signal }); if (res.status === 403) { - let detail = "Доступ запрещён"; + let detail = t(state.lang, "access_denied"); try { const body = await res.json(); if (body && body.detail !== undefined) { @@ -53,11 +56,11 @@ export async function fetchDuties(from, to) { err.serverDetail = detail; throw err; } - if (!res.ok) throw new Error("Ошибка загрузки"); + if (!res.ok) throw new Error(t(state.lang, "error_load_failed")); return res.json(); } catch (e) { if (e.name === "AbortError") { - throw new Error("Не удалось загрузить данные. Проверьте интернет."); + throw new Error(t(state.lang, "error_network")); } throw e; } finally { diff --git a/webapp/js/calendar.js b/webapp/js/calendar.js index 2ac5459..702444c 100644 --- a/webapp/js/calendar.js +++ b/webapp/js/calendar.js @@ -2,8 +2,8 @@ * Calendar grid and events-by-date mapping. */ -import { calendarEl, monthTitleEl } from "./dom.js"; -import { MONTHS } from "./constants.js"; +import { calendarEl, monthTitleEl, state } from "./dom.js"; +import { monthName, t } from "./i18n.js"; import { escapeHtml } from "./utils.js"; import { localDateString, @@ -116,6 +116,7 @@ export function renderCalendar( d.getDate() + '
В этом месяце дежурств нет.
'; + dutyListEl.innerHTML = '' + t(state.lang, "duty.none_this_month") + "
"; 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 - ? 'Сегодня' + + ? '' + + escapeHtml(t(state.lang, "duty.today")) + + '' + escapeHtml(dateLabel) + '' : '' + escapeHtml(dateLabel) + ""; diff --git a/webapp/js/hints.js b/webapp/js/hints.js index e54446e..4e45f11 100644 --- a/webapp/js/hints.js +++ b/webapp/js/hints.js @@ -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 ? '-' : ''; @@ -210,7 +222,7 @@ export function getDutyMarkerHintHtml(marker) { } if (rowHtmls.length === 0) return null; return ( - 'Доступ запрещён.
"; + const msg = t(state.lang, "access_denied"); + accessDeniedEl.innerHTML = "" + msg + "
"; if (serverDetail && serverDetail.trim()) { const detail = document.createElement("p"); detail.className = "access-denied-detail";