/** * Internationalization: language detection and translations for webapp. */ import { getInitData } from "./auth.js"; /** @type {Record>} */ 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:", "day_detail.close": "Close", "contact.label": "Contact", "contact.show": "Contacts", "contact.back": "Back", "contact.phone": "Phone", "contact.telegram": "Telegram", "current_duty.title": "Current Duty", "current_duty.no_duty": "No one is on duty right now", "current_duty.shift": "Shift", "current_duty.remaining": "Remaining: {hours}h {minutes}min", "current_duty.back": "Back to calendar" }, 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": "События:", "day_detail.close": "Закрыть", "contact.label": "Контакт", "contact.show": "Контакты", "contact.back": "Назад", "contact.phone": "Телефон", "contact.telegram": "Telegram", "current_duty.title": "Текущее дежурство", "current_duty.no_duty": "Сейчас никто не дежурит", "current_duty.shift": "Смена", "current_duty.remaining": "Осталось: {hours}ч {minutes}мин", "current_duty.back": "Назад к календарю" } }; 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} [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)); }