Files
duty-teller/webapp/js/i18n.js
Nikolay Tatarinov 7ffa727832
Some checks failed
CI / lint-and-test (push) Failing after 27s
feat: enhance error handling and configuration validation
- Added a global exception handler to log unhandled exceptions and return a generic 500 JSON response without exposing details to the client.
- Updated the configuration to validate the `DATABASE_URL` format, ensuring it starts with `sqlite://` or `postgresql://`, and log warnings for invalid formats.
- Introduced safe parsing for numeric environment variables (`HTTP_PORT`, `INIT_DATA_MAX_AGE_SECONDS`) with defaults on invalid values, including logging warnings for out-of-range values.
- Enhanced the duty schedule parser to enforce limits on the number of schedule rows and the length of full names and duty strings, raising appropriate errors when exceeded.
- Updated internationalization messages to include generic error responses for import failures and parsing issues, improving user experience.
- Added unit tests to verify the new error handling and configuration validation behaviors.
2026-03-02 23:36:03 +03:00

216 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Internationalization: language from backend config (window.__DT_LANG) and translations.
*/
/** @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.",
"error.retry": "Retry",
"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",
"event_type.other": "Other",
"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: "Не удалось загрузить данные.",
"error.retry": "Повторить",
"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": "Отпуск",
"event_type.other": "Другое",
"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'}
*/
export function normalizeLang(code) {
if (!code || typeof code !== "string") return "en";
const lower = code.toLowerCase();
if (lower.startsWith("ru")) return "ru";
return "en";
}
/**
* Get application language from backend config (window.__DT_LANG).
* Set by /app/config.js from DEFAULT_LANGUAGE. Fallback to 'en' if missing or invalid.
* @returns {'ru'|'en'}
*/
export function getLang() {
const raw =
typeof window !== "undefined" && window.__DT_LANG != null
? String(window.__DT_LANG)
: "";
const lang = normalizeLang(raw);
return lang === "ru" ? "ru" : "en";
}
/**
* 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}
*/
/**
* Resolve event_type.* key with fallback: unknown types use event_type.duty or event_type.other.
* @param {'ru'|'en'} lang
* @param {string} key
* @returns {string}
*/
function tEventType(lang, key) {
const dict = MESSAGES[lang] || MESSAGES.en;
const enDict = MESSAGES.en;
let s = dict[key];
if (s === undefined) s = enDict[key];
if (s === undefined) {
s = dict["event_type.other"] || enDict["event_type.other"] || enDict["event_type.duty"] || key;
}
return s;
}
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 && key.startsWith("event_type.")) {
return tEventType(lang, 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 - 011
* @returns {string}
*/
export function monthName(lang, month) {
const key = MONTH_KEYS[month];
return key ? t(lang, key) : "";
}
/**
* Get weekday short labels (MonSun order) for given lang.
* @param {'ru'|'en'} lang
* @returns {string[]}
*/
export function weekdayLabels(lang) {
return WEEKDAY_KEYS.map((k) => t(lang, k));
}