This commit is contained in:
@@ -2,6 +2,238 @@ const tg = window.Telegram?.WebApp;
|
||||
tg?.ready();
|
||||
tg?.expand();
|
||||
|
||||
const CarPassI18n = (() => {
|
||||
const textNodes = new WeakMap();
|
||||
const attrOriginals = new WeakMap();
|
||||
let observer = null;
|
||||
let timer = null;
|
||||
const dictionaries = {
|
||||
en: {
|
||||
"Гараж": "Garage",
|
||||
"Автомобили": "Vehicles",
|
||||
"Автомобиль": "Vehicle",
|
||||
"Авто": "Vehicles",
|
||||
"Заправка": "Fuel",
|
||||
"Сервис": "Service",
|
||||
"Расход": "Expense",
|
||||
"Расходы": "Expenses",
|
||||
"ТО и ремонт": "Maintenance and repair",
|
||||
"Дата": "Date",
|
||||
"Одометр, км": "Odometer, km",
|
||||
"Одометр": "Odometer",
|
||||
"Литры": "Liters",
|
||||
"Цена за литр": "Price per liter",
|
||||
"АЗС": "Fuel station",
|
||||
"Полный бак": "Full tank",
|
||||
"Стоимость": "Cost",
|
||||
"Валюта": "Currency",
|
||||
"Категория": "Category",
|
||||
"Название": "Title",
|
||||
"Комментарий": "Comment",
|
||||
"Сохранить": "Save",
|
||||
"Сохранить запись": "Save entry",
|
||||
"Сохранить расход": "Save expense",
|
||||
"Сохранить заправку": "Save fuel entry",
|
||||
"Сохранить настройки": "Save settings",
|
||||
"Создать запись": "Create booking",
|
||||
"Запись в СТО": "Book service",
|
||||
"СТО": "Service centers",
|
||||
"Сервисы": "Services",
|
||||
"Каталог": "Catalog",
|
||||
"Заявка": "Request",
|
||||
"Выберите сервис": "Choose service",
|
||||
"Город": "City",
|
||||
"Специализация": "Specialization",
|
||||
"Найти": "Search",
|
||||
"Что нужно сделать": "What needs to be done",
|
||||
"Услуга": "Service",
|
||||
"Длительность": "Duration",
|
||||
"Свободное окно": "Available slot",
|
||||
"Окно записи": "Booking slot",
|
||||
"Отправить заявку": "Send request",
|
||||
"Проверить карточку авто": "Check vehicle card",
|
||||
"Меню": "Menu",
|
||||
"Проверить вход": "Check login",
|
||||
"Открыть в Telegram": "Open in Telegram",
|
||||
"Обновить": "Refresh",
|
||||
"Настройки": "Settings",
|
||||
"Язык": "Language",
|
||||
"Панель СТО": "Service workplace",
|
||||
"Записи клиентов": "Client bookings",
|
||||
"Заказ-наряды": "Work orders",
|
||||
"Сотрудники": "Staff",
|
||||
"Пригласить": "Invite",
|
||||
"Заказ-наряд": "Work order",
|
||||
"Работы": "Labor",
|
||||
"Запчасти и жидкости": "Parts and fluids",
|
||||
"Проверьте смету": "Review estimate",
|
||||
"Согласовать": "Approve",
|
||||
"Отклонить": "Reject",
|
||||
"Админ-панель": "Admin panel",
|
||||
"Операционный обзор": "Operational overview",
|
||||
"Последние события": "Latest events",
|
||||
"Быстрые переходы": "Quick links",
|
||||
"Заявки СТО": "Service applications",
|
||||
"Записи": "Bookings",
|
||||
"Фильтр": "Filter",
|
||||
"Фильтры": "Filters",
|
||||
"Запросить": "Query",
|
||||
"Показать": "Show",
|
||||
"Импорт и экспорт": "Import and export",
|
||||
"Скачать JSON": "Download JSON",
|
||||
"Проверить файл": "Preview file",
|
||||
"Импортировать": "Import",
|
||||
"Паспорт автомобиля": "Vehicle passport",
|
||||
"Выберите автомобиль": "Choose vehicle",
|
||||
"Параметры авто": "Vehicle settings",
|
||||
"Сохранить паспорт": "Save passport",
|
||||
"Удалить автомобиль": "Delete vehicle",
|
||||
"Готов к работе": "Ready",
|
||||
"Сохраняю...": "Saving...",
|
||||
"Сохранено": "Saved",
|
||||
"Ошибка": "Error",
|
||||
"Нет данных": "No data",
|
||||
"Нет доступа": "No access",
|
||||
},
|
||||
ko: {
|
||||
"Гараж": "차고",
|
||||
"Автомобили": "차량",
|
||||
"Автомобиль": "차량",
|
||||
"Авто": "차량",
|
||||
"Заправка": "주유",
|
||||
"Сервис": "정비",
|
||||
"Расход": "지출",
|
||||
"Расходы": "지출",
|
||||
"ТО и ремонт": "정비 및 수리",
|
||||
"Дата": "날짜",
|
||||
"Одометр, км": "주행거리, km",
|
||||
"Одометр": "주행거리",
|
||||
"Литры": "리터",
|
||||
"Цена за литр": "리터당 가격",
|
||||
"АЗС": "주유소",
|
||||
"Полный бак": "가득 주유",
|
||||
"Стоимость": "비용",
|
||||
"Валюта": "통화",
|
||||
"Категория": "카테고리",
|
||||
"Название": "제목",
|
||||
"Комментарий": "메모",
|
||||
"Сохранить": "저장",
|
||||
"Сохранить запись": "기록 저장",
|
||||
"Сохранить расход": "지출 저장",
|
||||
"Сохранить заправку": "주유 저장",
|
||||
"Сохранить настройки": "설정 저장",
|
||||
"Создать запись": "예약 생성",
|
||||
"Запись в СТО": "정비소 예약",
|
||||
"СТО": "정비소",
|
||||
"Сервисы": "서비스",
|
||||
"Каталог": "목록",
|
||||
"Заявка": "요청",
|
||||
"Выберите сервис": "정비소 선택",
|
||||
"Город": "도시",
|
||||
"Специализация": "전문 분야",
|
||||
"Найти": "검색",
|
||||
"Что нужно сделать": "필요한 작업",
|
||||
"Услуга": "서비스",
|
||||
"Длительность": "소요 시간",
|
||||
"Свободное окно": "예약 가능 시간",
|
||||
"Окно записи": "예약 시간",
|
||||
"Отправить заявку": "요청 보내기",
|
||||
"Проверить карточку авто": "차량 카드 확인",
|
||||
"Меню": "메뉴",
|
||||
"Проверить вход": "로그인 확인",
|
||||
"Открыть в Telegram": "텔레그램에서 열기",
|
||||
"Обновить": "새로고침",
|
||||
"Настройки": "설정",
|
||||
"Язык": "언어",
|
||||
"Панель СТО": "정비소 작업실",
|
||||
"Записи клиентов": "고객 예약",
|
||||
"Заказ-наряды": "작업지시서",
|
||||
"Сотрудники": "직원",
|
||||
"Пригласить": "초대",
|
||||
"Заказ-наряд": "작업지시서",
|
||||
"Работы": "공임",
|
||||
"Запчасти и жидкости": "부품 및 오일",
|
||||
"Проверьте смету": "견적 확인",
|
||||
"Согласовать": "승인",
|
||||
"Отклонить": "거절",
|
||||
"Админ-панель": "관리자 패널",
|
||||
"Операционный обзор": "운영 요약",
|
||||
"Последние события": "최근 이벤트",
|
||||
"Быстрые переходы": "빠른 이동",
|
||||
"Заявки СТО": "정비소 신청",
|
||||
"Записи": "예약",
|
||||
"Фильтр": "필터",
|
||||
"Фильтры": "필터",
|
||||
"Запросить": "조회",
|
||||
"Показать": "보기",
|
||||
"Импорт и экспорт": "가져오기/내보내기",
|
||||
"Скачать JSON": "JSON 다운로드",
|
||||
"Проверить файл": "파일 확인",
|
||||
"Импортировать": "가져오기",
|
||||
"Паспорт автомобиля": "차량 패스포트",
|
||||
"Выберите автомобиль": "차량 선택",
|
||||
"Параметры авто": "차량 설정",
|
||||
"Сохранить паспорт": "패스포트 저장",
|
||||
"Удалить автомобиль": "차량 삭제",
|
||||
"Готов к работе": "준비 완료",
|
||||
"Сохраняю...": "저장 중...",
|
||||
"Сохранено": "저장됨",
|
||||
"Ошибка": "오류",
|
||||
"Нет данных": "데이터 없음",
|
||||
"Нет доступа": "접근 불가",
|
||||
},
|
||||
};
|
||||
|
||||
function t(text, locale = currentLocale()) {
|
||||
return dictionaries[locale]?.[text] || text;
|
||||
}
|
||||
|
||||
function currentLocale() {
|
||||
return window.CarPassPage?.state?.user?.locale || localStorage.getItem("carpassLocale") || "ru";
|
||||
}
|
||||
|
||||
function apply(root = document.body, locale = currentLocale()) {
|
||||
document.documentElement.lang = locale;
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode(node) {
|
||||
const parent = node.parentElement;
|
||||
if (!parent || ["SCRIPT", "STYLE", "TEXTAREA", "INPUT", "SELECT"].includes(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||
return node.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
||||
},
|
||||
});
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode;
|
||||
if (!textNodes.has(node)) textNodes.set(node, node.nodeValue.trim());
|
||||
const original = textNodes.get(node);
|
||||
node.nodeValue = node.nodeValue.replace(node.nodeValue.trim(), t(original, locale));
|
||||
}
|
||||
root.querySelectorAll?.("[placeholder], [aria-label], [title]").forEach((element) => {
|
||||
["placeholder", "aria-label", "title"].forEach((attr) => {
|
||||
const value = element.getAttribute(attr);
|
||||
if (!value) return;
|
||||
let originals = attrOriginals.get(element);
|
||||
if (!originals) {
|
||||
originals = {};
|
||||
attrOriginals.set(element, originals);
|
||||
}
|
||||
originals[attr] ||= value;
|
||||
element.setAttribute(attr, t(originals[attr], locale));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function observe(root = document.body) {
|
||||
if (observer || !root) return;
|
||||
observer = new MutationObserver(() => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => apply(root), 40);
|
||||
});
|
||||
observer.observe(root, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
return { apply, t, currentLocale, observe };
|
||||
})();
|
||||
|
||||
const CarPassPage = (() => {
|
||||
const state = { user: null, authConfig: null };
|
||||
|
||||
@@ -51,13 +283,14 @@ const CarPassPage = (() => {
|
||||
}
|
||||
document.body.classList.remove("auth-required");
|
||||
document.querySelector("#authOverlay")?.classList.add("hidden");
|
||||
localStorage.setItem("carpassLocale", state.user.locale || "ru");
|
||||
return state.user;
|
||||
}
|
||||
|
||||
function toast(message, tone = "success") {
|
||||
const node = document.querySelector("#toast");
|
||||
if (!node) return;
|
||||
node.textContent = message;
|
||||
node.textContent = t(message);
|
||||
node.className = `toast ${tone}`;
|
||||
window.clearTimeout(toast.timer);
|
||||
toast.timer = window.setTimeout(() => node.classList.add("hidden"), 2600);
|
||||
@@ -69,7 +302,7 @@ const CarPassPage = (() => {
|
||||
button.dataset.label = button.textContent;
|
||||
button.disabled = true;
|
||||
button.classList.add("is-busy");
|
||||
button.innerHTML = `<span class="spinner"></span><span>${label}</span>`;
|
||||
button.innerHTML = `<span class="spinner"></span><span>${t(label)}</span>`;
|
||||
} else {
|
||||
button.disabled = false;
|
||||
button.classList.remove("is-busy");
|
||||
@@ -115,18 +348,73 @@ const CarPassPage = (() => {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value).slice(0, 16).replace("T", " ");
|
||||
return date.toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||||
const locale = { ru: "ru-RU", en: "en-US", ko: "ko-KR" }[state.user?.locale] || "ru-RU";
|
||||
return date.toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function t(text) {
|
||||
return CarPassI18n.t(text, state.user?.locale || "ru");
|
||||
}
|
||||
|
||||
function applyTranslations(root = document.body) {
|
||||
CarPassI18n.apply(root, state.user?.locale || "ru");
|
||||
}
|
||||
|
||||
async function updateLocale(locale) {
|
||||
if (!state.user || state.user.locale === locale) return;
|
||||
state.user = await api(`/users/${state.user.id}/preferences`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ locale }),
|
||||
});
|
||||
localStorage.setItem("carpassLocale", locale);
|
||||
applyTranslations();
|
||||
}
|
||||
|
||||
function installLocaleSwitch() {
|
||||
const topbar = document.querySelector(".topbar");
|
||||
if (!topbar || document.querySelector("#globalLocaleSelect")) return;
|
||||
let host = topbar.querySelector(".top-actions");
|
||||
if (!host) {
|
||||
host = document.createElement("div");
|
||||
host.className = "top-actions";
|
||||
topbar.appendChild(host);
|
||||
}
|
||||
const select = document.createElement("select");
|
||||
select.id = "globalLocaleSelect";
|
||||
select.className = "locale-switch";
|
||||
select.setAttribute("aria-label", "Язык");
|
||||
select.innerHTML = `
|
||||
<option value="ru">RU</option>
|
||||
<option value="en">EN</option>
|
||||
<option value="ko">KO</option>
|
||||
`;
|
||||
select.value = state.user?.locale || "ru";
|
||||
select.addEventListener("change", async () => {
|
||||
try {
|
||||
await updateLocale(select.value);
|
||||
toast("Сохранено");
|
||||
} catch (error) {
|
||||
toast(error.message || "Ошибка", "error");
|
||||
}
|
||||
});
|
||||
const primarySelect = host.querySelector("select:not(.locale-switch)");
|
||||
if (primarySelect) primarySelect.insertAdjacentElement("afterend", select);
|
||||
else host.prepend(select);
|
||||
}
|
||||
|
||||
async function boot(init) {
|
||||
try {
|
||||
await loadAuthConfig();
|
||||
await ensureUser();
|
||||
installLocaleSwitch();
|
||||
applyTranslations();
|
||||
CarPassI18n.observe();
|
||||
await init();
|
||||
applyTranslations();
|
||||
} catch (error) {
|
||||
if (error.message === "Требуется вход через Telegram") return;
|
||||
console.error(error);
|
||||
@@ -148,5 +436,9 @@ const CarPassPage = (() => {
|
||||
csvList,
|
||||
formatDateTime,
|
||||
today,
|
||||
t,
|
||||
applyTranslations,
|
||||
installLocaleSwitch,
|
||||
updateLocale,
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user