Poprawka botto bar i wersje językowe
This commit is contained in:
@@ -4,10 +4,18 @@ 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";
|
||||
/** @type {Set<(lang: string) => void>} */
|
||||
let langBusy = false;
|
||||
/** @type {Set<(lang: string) => void | Promise<void>>} */
|
||||
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) => {
|
||||
@@ -15,6 +23,30 @@ function interpolate(template, vars = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -31,6 +63,10 @@ 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;
|
||||
@@ -72,7 +108,12 @@ export function onLangChange(callback) {
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
|
||||
export function setLang(lang, { persist = true, notify = true } = {}) {
|
||||
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;
|
||||
@@ -87,14 +128,38 @@ export function setLang(lang, { persist = true, notify = true } = {}) {
|
||||
|
||||
applyTranslations();
|
||||
|
||||
if (notify && changed) {
|
||||
if (!(notify && changed)) {
|
||||
return currentLang;
|
||||
}
|
||||
|
||||
if (langBusy) {
|
||||
return currentLang;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
setLangBusy(true);
|
||||
|
||||
try {
|
||||
const tasks = [];
|
||||
listeners.forEach((cb) => {
|
||||
try {
|
||||
cb(currentLang);
|
||||
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;
|
||||
@@ -116,7 +181,13 @@ function flagImgHtml(code) {
|
||||
return `<img class="lang-flag-img" src="assets/img/flags/${safe}.svg" alt="" width="28" height="20" decoding="async">`;
|
||||
}
|
||||
|
||||
function spinnerHtml() {
|
||||
return `<span class="lang-picker-spinner" aria-hidden="true"></span>`;
|
||||
}
|
||||
|
||||
export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
ensureDocumentClickHandler();
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "lang-picker";
|
||||
wrap.dataset.langPicker = "1";
|
||||
@@ -133,13 +204,41 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
menu.id = `${idPrefix}Menu`;
|
||||
menu.setAttribute("role", "listbox");
|
||||
|
||||
function refresh() {
|
||||
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");
|
||||
@@ -147,12 +246,14 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
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)}<span>${lang.label}</span>`;
|
||||
option.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
close();
|
||||
if (langBusy) return;
|
||||
pickerApi.close();
|
||||
if (lang.code !== getLang()) {
|
||||
setLang(lang.code);
|
||||
void setLang(lang.code);
|
||||
}
|
||||
});
|
||||
menu.appendChild(option);
|
||||
@@ -160,25 +261,21 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
function close() {
|
||||
menu.classList.add("hidden");
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
wrap.classList.remove("is-open");
|
||||
openPickerClosers.add(pickerApi);
|
||||
}
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (langBusy) return;
|
||||
if (menu.classList.contains("hidden")) open();
|
||||
else close();
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!wrap.contains(e.target)) close();
|
||||
else pickerApi.close();
|
||||
});
|
||||
|
||||
wrap.appendChild(btn);
|
||||
@@ -186,6 +283,10 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
refresh();
|
||||
|
||||
onLangChange(() => refresh());
|
||||
onLangBusyChange(() => {
|
||||
if (langBusy) pickerApi.close();
|
||||
paintButton();
|
||||
});
|
||||
|
||||
return { el: wrap, refresh, close };
|
||||
return { el: wrap, refresh, close: pickerApi.close };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user