import pl from "../locales/pl.js"; import en from "../locales/en.js"; import de from "../locales/de.js"; const STORAGE_KEY = "karczma_lang"; const catalogs = { pl, en, de }; const LANG_BUSY_MIN_MS = 280; let currentLang = "pl"; let langBusy = false; /** @type {Set<(lang: string) => void | Promise>} */ const listeners = new Set(); /** @type {Set<(busy: boolean) => void>} */ const busyListeners = new Set(); /** @type {Set<{ el: HTMLElement, close: () => void }>} */ const openPickerClosers = new Set(); let documentClickBound = false; function interpolate(template, vars = {}) { return String(template).replace(/\{(\w+)\}/g, (_, key) => { return vars[key] != null ? String(vars[key]) : `{${key}}`; }); } function ensureDocumentClickHandler() { if (documentClickBound) return; documentClickBound = true; document.addEventListener("click", (e) => { openPickerClosers.forEach((picker) => { if (!picker.el.contains(e.target)) { picker.close(); } }); }); } function setLangBusy(busy) { langBusy = !!busy; document.documentElement.classList.toggle("is-lang-loading", langBusy); busyListeners.forEach((cb) => { try { cb(langBusy); } catch (err) { console.warn("[i18n] busy listener failed", err); } }); } export function getAvailableLanguages() { const fromConfig = window.APP_CONFIG?.languages; if (Array.isArray(fromConfig) && fromConfig.length) { return fromConfig; } return [ { code: "pl", label: "Polski", flag: "🇵🇱" }, { code: "en", label: "English", flag: "🇬🇧" }, { code: "de", label: "Deutsch", flag: "🇩🇪" }, ]; } export function getLang() { return currentLang; } export function isLangBusy() { return langBusy; } export function t(key, vars = {}) { const catalog = catalogs[currentLang] || catalogs.pl; const fallback = catalogs.pl; const value = catalog[key] ?? fallback[key] ?? key; return interpolate(value, vars); } export function applyTranslations(root = document) { root.querySelectorAll("[data-i18n]").forEach((el) => { const key = el.getAttribute("data-i18n"); if (!key) return; const html = el.hasAttribute("data-i18n-html"); const text = t(key); if (html) { el.innerHTML = text; } else { el.textContent = text; } }); root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { const key = el.getAttribute("data-i18n-placeholder"); if (!key) return; el.setAttribute("placeholder", t(key)); }); root.querySelectorAll("[data-i18n-aria]").forEach((el) => { const key = el.getAttribute("data-i18n-aria"); if (!key) return; el.setAttribute("aria-label", t(key)); }); document.documentElement.lang = t("html.lang"); document.title = t("doc.title"); } export function onLangChange(callback) { listeners.add(callback); return () => listeners.delete(callback); } export function onLangBusyChange(callback) { busyListeners.add(callback); return () => busyListeners.delete(callback); } export async function setLang(lang, { persist = true, notify = true } = {}) { const next = catalogs[lang] ? lang : "pl"; const changed = next !== currentLang; currentLang = next; if (persist) { try { localStorage.setItem(STORAGE_KEY, currentLang); } catch { // ignore } } applyTranslations(); if (!(notify && changed)) { return currentLang; } if (langBusy) { return currentLang; } const startedAt = Date.now(); setLangBusy(true); try { const tasks = []; listeners.forEach((cb) => { try { const result = cb(currentLang); if (result != null && typeof result.then === "function") { tasks.push(result); } } catch (err) { console.warn("[i18n] listener failed", err); } }); if (tasks.length) { await Promise.allSettled(tasks); } } finally { const wait = Math.max(0, LANG_BUSY_MIN_MS - (Date.now() - startedAt)); if (wait > 0) { await new Promise((resolve) => setTimeout(resolve, wait)); } setLangBusy(false); } return currentLang; } export function initI18n() { let stored = ""; try { stored = localStorage.getItem(STORAGE_KEY) || ""; } catch { stored = ""; } const initial = catalogs[stored] ? stored : "pl"; return setLang(initial, { persist: true, notify: false }); } function flagImgHtml(code) { const safe = String(code || "pl").toLowerCase().replace(/[^a-z]/g, "") || "pl"; return ``; } function spinnerHtml() { return ``; } export function createLanguagePicker({ idPrefix = "lang" } = {}) { ensureDocumentClickHandler(); const wrap = document.createElement("div"); wrap.className = "lang-picker"; wrap.dataset.langPicker = "1"; const btn = document.createElement("button"); btn.type = "button"; btn.className = "lang-picker-btn"; btn.id = `${idPrefix}Btn`; btn.setAttribute("aria-haspopup", "listbox"); btn.setAttribute("aria-expanded", "false"); const menu = document.createElement("div"); menu.className = "lang-picker-menu hidden"; menu.id = `${idPrefix}Menu`; menu.setAttribute("role", "listbox"); const pickerApi = { el: wrap, close: () => { menu.classList.add("hidden"); btn.setAttribute("aria-expanded", "false"); wrap.classList.remove("is-open"); openPickerClosers.delete(pickerApi); }, }; function paintButton() { const langs = getAvailableLanguages(); const current = langs.find((l) => l.code === getLang()) || langs[0]; const code = current?.code || "pl"; if (langBusy) { btn.innerHTML = spinnerHtml(); btn.disabled = true; btn.setAttribute("aria-busy", "true"); btn.setAttribute("aria-label", t("lang.loading")); wrap.classList.add("is-loading"); return; } btn.disabled = false; btn.setAttribute("aria-busy", "false"); btn.innerHTML = flagImgHtml(code); btn.setAttribute("aria-label", `${t("lang.aria")}: ${current?.label || code}`); wrap.classList.remove("is-loading"); } function refresh() { paintButton(); const langs = getAvailableLanguages(); menu.innerHTML = ""; langs.forEach((lang) => { const option = document.createElement("button"); option.type = "button"; option.className = "lang-picker-option" + (lang.code === getLang() ? " is-active" : ""); option.setAttribute("role", "option"); option.setAttribute("aria-selected", lang.code === getLang() ? "true" : "false"); option.disabled = langBusy; option.innerHTML = `${flagImgHtml(lang.code)}${lang.label}`; option.addEventListener("click", (e) => { e.stopPropagation(); if (langBusy) return; pickerApi.close(); if (lang.code !== getLang()) { void setLang(lang.code); } }); menu.appendChild(option); }); } function open() { if (langBusy) return; openPickerClosers.forEach((p) => { if (p !== pickerApi) p.close(); }); menu.classList.remove("hidden"); btn.setAttribute("aria-expanded", "true"); wrap.classList.add("is-open"); openPickerClosers.add(pickerApi); } btn.addEventListener("click", (e) => { e.stopPropagation(); if (langBusy) return; if (menu.classList.contains("hidden")) open(); else pickerApi.close(); }); wrap.appendChild(btn); wrap.appendChild(menu); refresh(); onLangChange(() => refresh()); onLangBusyChange(() => { if (langBusy) pickerApi.close(); paintButton(); }); return { el: wrap, refresh, close: pickerApi.close }; }