192 lines
5.2 KiB
JavaScript
192 lines
5.2 KiB
JavaScript
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 };
|
|
|
|
let currentLang = "pl";
|
|
/** @type {Set<(lang: string) => void>} */
|
|
const listeners = new Set();
|
|
|
|
function interpolate(template, vars = {}) {
|
|
return String(template).replace(/\{(\w+)\}/g, (_, key) => {
|
|
return vars[key] != null ? String(vars[key]) : `{${key}}`;
|
|
});
|
|
}
|
|
|
|
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 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 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) {
|
|
listeners.forEach((cb) => {
|
|
try {
|
|
cb(currentLang);
|
|
} catch (err) {
|
|
console.warn("[i18n] listener failed", err);
|
|
}
|
|
});
|
|
}
|
|
|
|
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 `<img class="lang-flag-img" src="assets/img/flags/${safe}.svg" alt="" width="28" height="20" decoding="async">`;
|
|
}
|
|
|
|
export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
|
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");
|
|
|
|
function refresh() {
|
|
const langs = getAvailableLanguages();
|
|
const current = langs.find((l) => l.code === getLang()) || langs[0];
|
|
const code = current?.code || "pl";
|
|
btn.innerHTML = flagImgHtml(code);
|
|
btn.setAttribute("aria-label", `${t("lang.aria")}: ${current?.label || code}`);
|
|
|
|
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.innerHTML = `${flagImgHtml(lang.code)}<span>${lang.label}</span>`;
|
|
option.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
close();
|
|
if (lang.code !== getLang()) {
|
|
setLang(lang.code);
|
|
}
|
|
});
|
|
menu.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function open() {
|
|
menu.classList.remove("hidden");
|
|
btn.setAttribute("aria-expanded", "true");
|
|
wrap.classList.add("is-open");
|
|
}
|
|
|
|
function close() {
|
|
menu.classList.add("hidden");
|
|
btn.setAttribute("aria-expanded", "false");
|
|
wrap.classList.remove("is-open");
|
|
}
|
|
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
if (menu.classList.contains("hidden")) open();
|
|
else close();
|
|
});
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (!wrap.contains(e.target)) close();
|
|
});
|
|
|
|
wrap.appendChild(btn);
|
|
wrap.appendChild(menu);
|
|
refresh();
|
|
|
|
onLangChange(() => refresh());
|
|
|
|
return { el: wrap, refresh, close };
|
|
}
|