Initial commit — existing SVN working copy (SVN r436)
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
(function () {
|
||||
// === 1) Date / day-of-year (for deterministic highlight pick) ===
|
||||
const today = new Date();
|
||||
const formattedDate = today.toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
function getDayOfYear(date) {
|
||||
const start = new Date(date.getFullYear(), 0, 0);
|
||||
return Math.floor((date - start) / 86400000);
|
||||
}
|
||||
const dayOfYear = getDayOfYear(today);
|
||||
|
||||
// === 2) DOM refs for the highlight block (hero) ===
|
||||
const elDate = document.getElementById("event-date");
|
||||
const elTitle = document.getElementById("event-title");
|
||||
const elBody = document.getElementById("event-body");
|
||||
const elImg = document.getElementById("event-image");
|
||||
const elImgWrap = document.getElementById("event-image-wrap");
|
||||
const elSpeaker = document.getElementById("event-speaker");
|
||||
const elLanguage = document.getElementById("event-language");
|
||||
const elInstitute = document.getElementById("event-institute");
|
||||
|
||||
if (elDate) elDate.textContent = formattedDate;
|
||||
|
||||
// === 3) DOM refs for halls and talk lists ===
|
||||
const hallHeadingEls = {
|
||||
"1": document.getElementById("hall-1-heading"),
|
||||
"2": document.getElementById("hall-2-heading"),
|
||||
"3": document.getElementById("hall-3-heading"),
|
||||
};
|
||||
const hallTalksEls = {
|
||||
"1": document.getElementById("hall-1-talks"),
|
||||
"2": document.getElementById("hall-2-talks"),
|
||||
"3": document.getElementById("hall-3-talks"),
|
||||
};
|
||||
|
||||
// === 4) Load schedule JSON ===
|
||||
fetch("text/schedule.json", { cache: "no-cache" })
|
||||
.then((resp) => {
|
||||
if (!resp.ok) throw new Error("could not load schedule.json");
|
||||
return resp.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const halls = data.halls || [];
|
||||
const talks = data.talks || [];
|
||||
|
||||
// 4a) Set hall headings
|
||||
halls.forEach((hall) => {
|
||||
if (hallHeadingEls[hall.id]) {
|
||||
hallHeadingEls[hall.id].textContent =
|
||||
decodeUnicode(hall.label || ("Hörsaal " + hall.id));
|
||||
}
|
||||
});
|
||||
|
||||
// 4b) Group talks by hall id
|
||||
const groupedByHall = {};
|
||||
talks.forEach((talk) => {
|
||||
if (!groupedByHall[talk.hall]) groupedByHall[talk.hall] = [];
|
||||
groupedByHall[talk.hall].push(talk);
|
||||
});
|
||||
|
||||
// 4c) Desktop: gemeinsame Zeitliste + Platzhalter, Mobile: wie bisher
|
||||
const timesOrder = uniqueTimesInOrder(talks); // in Reihenfolge aus JSON
|
||||
|
||||
Object.keys(hallTalksEls).forEach((hallId) => {
|
||||
const container = hallTalksEls[hallId];
|
||||
if (!container) return;
|
||||
|
||||
const hallTalks = groupedByHall[hallId] || [];
|
||||
|
||||
// Index nach Zeit für schnellen Zugriff
|
||||
const byTime = new Map(hallTalks.map(t => [String(t.time || "").trim(), t]));
|
||||
|
||||
// Desktop nebeneinander -> gleiche Zeilen je Zeit; sonst originale Liste
|
||||
const desktop = window.matchMedia("(min-width: 992px)").matches;
|
||||
let rows = [];
|
||||
|
||||
if (desktop) {
|
||||
rows = timesOrder.map((time, i) => {
|
||||
const talk = byTime.get(time) || null;
|
||||
return renderTalkRow(talk, hallId, i, time);
|
||||
});
|
||||
} else {
|
||||
rows = hallTalks.map((t, i) => renderTalkRow(t, hallId, i, t.time || ""));
|
||||
}
|
||||
|
||||
container.innerHTML = rows.join("");
|
||||
wireImageLoadReflow(container);
|
||||
});
|
||||
|
||||
// 4d) Enable toggle animation
|
||||
wireTalkToggles();
|
||||
|
||||
// 4e) Zeilenhöhen anpassen (nur Desktop)
|
||||
equalizeRowHeightsOnce();
|
||||
|
||||
|
||||
// 4e) Fill the highlight block deterministically
|
||||
// Only talks that have BOTH a non-empty description and a real image
|
||||
const highlightCandidates = (talks || []).filter((t) => {
|
||||
const hasDesc = (t.desc || "").trim().length > 0;
|
||||
const hasImg = isImageUrl((t.image || "").trim());
|
||||
return hasDesc && hasImg;
|
||||
});
|
||||
|
||||
if (highlightCandidates.length > 0) {
|
||||
const idx = dayOfYear % highlightCandidates.length;
|
||||
const chosen = highlightCandidates[idx];
|
||||
|
||||
const highlightTitle = decodeUnicode(chosen.title || "Untitled");
|
||||
const highlightBody = chosen.desc || highlightTitle;
|
||||
const highlightImage = chosen.image || "";
|
||||
const highlightAlt =
|
||||
decodeUnicode(chosen.imageAlt || highlightTitle || "Image");
|
||||
|
||||
if (elTitle) elTitle.textContent = highlightTitle;
|
||||
if (elBody) elBody.innerHTML = formatHtmlWithBreaks(highlightBody);
|
||||
|
||||
if (elImg) {
|
||||
elImg.alt = highlightAlt;
|
||||
|
||||
if (isImageUrl(highlightImage)) {
|
||||
elImg.src = highlightImage;
|
||||
elImg.style.display = "";
|
||||
|
||||
} else {
|
||||
elImg.removeAttribute("src");
|
||||
elImg.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (elSpeaker) {
|
||||
const profTitle = chosen.profTitle || "";
|
||||
const lecturer = chosen.lecturer || "";
|
||||
const instLabel = chosen.instituteLabel || "";
|
||||
const instUrl = chosen.instituteUrl || "";
|
||||
|
||||
const name =
|
||||
(profTitle ? profTitle.trim() + " " : "") + (lecturer || "");
|
||||
const inst = instUrl
|
||||
? `<a class="talk-institute" href="${escapeAttr(
|
||||
instUrl
|
||||
)}" target="_blank" rel="noopener">${escapeText(
|
||||
instLabel || instUrl
|
||||
)}</a>`
|
||||
: instLabel
|
||||
? `<span class="talk-institute">${escapeText(instLabel)}</span>`
|
||||
: "";
|
||||
|
||||
const sep = name && inst ? ", " : "";
|
||||
elSpeaker.innerHTML = `<span data-i18n="speaker">Referent:</span> ${escapeText(
|
||||
name.trim()
|
||||
)}${sep}${inst}`;
|
||||
}
|
||||
|
||||
if (elLanguage && (chosen.language || "")) {
|
||||
elLanguage.innerHTML = `<span data-i18n="language">Sprache:</span> ${escapeText(
|
||||
chosen.language
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (elInstitute) {
|
||||
const instLabel = chosen.instituteLabel || "";
|
||||
const instUrl = chosen.instituteUrl || "";
|
||||
if (elInstitute.tagName === "A" && instUrl) {
|
||||
elInstitute.textContent = decodeUnicode(instLabel || instUrl);
|
||||
elInstitute.setAttribute("href", instUrl);
|
||||
} else {
|
||||
elInstitute.textContent = decodeUnicode(instLabel || "");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No suitable highlight -> clear/hide gracefully
|
||||
if (elTitle) elTitle.textContent = " ";
|
||||
if (elBody) elBody.textContent = " ";
|
||||
if (elImg) {
|
||||
elImg.removeAttribute("src");
|
||||
elImg.alt = "";
|
||||
elImg.style.display = "none";
|
||||
}
|
||||
if (elSpeaker) elSpeaker.textContent = "";
|
||||
if (elLanguage) elLanguage.textContent = "";
|
||||
if (elInstitute) elInstitute.textContent = "";
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("schedule load error:", err);
|
||||
if (elTitle) elTitle.textContent = "Highlight could not be loaded";
|
||||
if (elBody) elBody.textContent = "Please try again later.";
|
||||
if (elImg) {
|
||||
elImg.alt = "No image available";
|
||||
elImg.style.display = "none";
|
||||
}
|
||||
|
||||
Object.values(hallTalksEls).forEach((container) => {
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="talk-block">
|
||||
<p class="time-slot">--:--</p>
|
||||
<button class="talk-toggle" type="button" disabled>
|
||||
<span class="talk-title-text" style="opacity:.6">Error loading program.</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
// Escape for text node contexts
|
||||
function escapeText(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
// Escape for attribute values
|
||||
function escapeAttr(str) {
|
||||
return escapeText(str).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// Decode literals like "U+00AE" or "u+1F4A9" to real Unicode
|
||||
function decodeUnicode(str) {
|
||||
if (!str) return "";
|
||||
return String(str).replace(/U\+([0-9A-F]{4,6})/gi, (_, hex) =>
|
||||
String.fromCodePoint(parseInt(hex, 16))
|
||||
);
|
||||
}
|
||||
|
||||
// Produce HTML: decode "U+XXXX", escape HTML, then turn newlines into <br>
|
||||
function formatHtmlWithBreaks(str) {
|
||||
const decoded = decodeUnicode(str).replace(/\r\n?/g, "\n");
|
||||
const escaped = escapeText(decoded);
|
||||
return escaped.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
// Accept only real image files; PDFs etc. are rejected for highlight/images
|
||||
function isImageUrl(url) {
|
||||
if (!url) return false;
|
||||
return /\.(png|jpe?g|webp|gif|avif|svg)$/i.test(url);
|
||||
}
|
||||
|
||||
// Build an i18n-ready "Speaker" line.
|
||||
function buildLecturerLine({
|
||||
profTitle = "",
|
||||
lecturer = "",
|
||||
instLabel = "",
|
||||
instUrl = "",
|
||||
}) {
|
||||
if (!profTitle && !lecturer && !instLabel) return "";
|
||||
const name =
|
||||
(profTitle ? escapeText(decodeUnicode(profTitle.trim())) + " " : "") +
|
||||
escapeText(decodeUnicode(lecturer || "").trim());
|
||||
const inst = instUrl
|
||||
? `<a class="talk-institute" href="${escapeAttr(
|
||||
instUrl
|
||||
)}" target="_blank" rel="noopener">${escapeText(
|
||||
decodeUnicode(instLabel || instUrl)
|
||||
)}</a>`
|
||||
: instLabel
|
||||
? `<span class="talk-institute">${escapeText(decodeUnicode(instLabel))}</span>`
|
||||
: "";
|
||||
const sep = name && inst ? ", " : "";
|
||||
return `<p class="talk-lecturer"><span data-i18n="speaker">Referent:</span> ${name}${sep}${inst}</p>`;
|
||||
}
|
||||
|
||||
// Build an i18n-ready "Language" line.
|
||||
function buildLanguageLine(langValue = "") {
|
||||
if (!langValue) return "";
|
||||
return `<p class="talk-language"><span data-i18n="language">Sprache:</span> ${escapeText(
|
||||
decodeUnicode(langValue)
|
||||
)}</p>`;
|
||||
}
|
||||
|
||||
// Toggle animation and a11y attributes for each talk block
|
||||
function wireTalkToggles() {
|
||||
const toggles = document.querySelectorAll(".talk-toggle");
|
||||
toggles.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const panelId = btn.getAttribute("aria-controls");
|
||||
const panel = document.getElementById(panelId);
|
||||
if (!panel) return;
|
||||
|
||||
const expanded = btn.getAttribute("aria-expanded") === "true";
|
||||
const nowExpanded = !expanded;
|
||||
|
||||
// a11y attributes
|
||||
btn.setAttribute("aria-expanded", String(nowExpanded));
|
||||
panel.setAttribute("aria-hidden", String(!nowExpanded));
|
||||
|
||||
// animate max-height + opacity
|
||||
if (nowExpanded) {
|
||||
// open just this panel
|
||||
panel.classList.add("is-open");
|
||||
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||
panel.style.opacity = "1";
|
||||
} else {
|
||||
// close just this panel
|
||||
panel.style.maxHeight = panel.scrollHeight + "px"; // start from current height
|
||||
panel.getBoundingClientRect(); // force reflow
|
||||
panel.classList.remove("is-open");
|
||||
panel.style.maxHeight = "0";
|
||||
panel.style.opacity = "0";
|
||||
}
|
||||
// IMPORTANT:
|
||||
// Do NOT re-equalize heights here. We want other columns to stay unchanged.
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Liefert die Zeitpunkte in der Reihenfolge ihres ersten Auftretens im JSON
|
||||
function uniqueTimesInOrder(talks) {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
talks.forEach(t => {
|
||||
const tt = String(t.time || "").trim();
|
||||
if (tt && !seen.has(tt)) { seen.add(tt); out.push(tt); }
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// Eine Zeile (Talk oder leerer Platzhalter für eine Zeit)
|
||||
function renderTalkRow(talk, hallId, i, timeLabel) {
|
||||
const safeTime = timeLabel || (talk ? talk.time || "" : "");
|
||||
if (!talk) {
|
||||
// leerer Platzhalter – sorgt für fluchtende Zeiten und gleiche Spaltenhöhe
|
||||
return `
|
||||
<div class="talk-block is-placeholder">
|
||||
<p class="time-slot">${escapeText(safeTime)}</p>
|
||||
<button class="talk-toggle" type="button" disabled aria-disabled="true">
|
||||
<span class="talk-title-text" style="opacity:.55">—</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const safeTitle = decodeUnicode(talk.title || "");
|
||||
const rawDesc = talk.desc || "";
|
||||
const safeDesc = rawDesc.trim() !== "" ? rawDesc : "";
|
||||
const imgUrl = (talk.image || "").trim();
|
||||
const safeImg = isImageUrl(imgUrl) ? imgUrl : "";
|
||||
const safeAlt = talk.imageAlt || safeTitle || "Talk image";
|
||||
|
||||
const profTitle = talk.profTitle || "";
|
||||
const lecturer = talk.lecturer || "";
|
||||
const instLabel = talk.instituteLabel || "";
|
||||
const instUrl = talk.instituteUrl || "";
|
||||
const language = talk.language || "";
|
||||
|
||||
const panelId = `talk-details-${hallId}-${i}`;
|
||||
const lecturerLine = buildLecturerLine({ profTitle, lecturer, instLabel, instUrl });
|
||||
const languageLine = buildLanguageLine(language);
|
||||
|
||||
const detailsInner = `
|
||||
<div class="talk-details-inner">
|
||||
${safeDesc ? `<p class="talk-desc">${formatHtmlWithBreaks(safeDesc)}</p>` : ""}
|
||||
${lecturerLine}
|
||||
${languageLine}
|
||||
${safeImg ? `<img class="talk-image" src="${escapeAttr(safeImg)}" alt="${escapeAttr(decodeUnicode(safeAlt))}">` : ""}
|
||||
</div>
|
||||
`;
|
||||
const hasDetails = safeDesc || lecturerLine || languageLine || safeImg;
|
||||
|
||||
const toggleButton = hasDetails
|
||||
? `<button class="talk-toggle" type="button" aria-expanded="false" aria-controls="${panelId}">
|
||||
<span class="talk-title-text">${escapeText(safeTitle)}</span>
|
||||
</button>`
|
||||
: `<div class="talk-toggle" aria-disabled="true">
|
||||
<span class="talk-title-text">${escapeText(safeTitle)}</span>
|
||||
</div>`;
|
||||
|
||||
return `<div class="talk-block">
|
||||
<p class="time-slot">${escapeText(safeTime)}</p>
|
||||
${toggleButton}
|
||||
${hasDetails ? `
|
||||
<div class="talk-details" id="${panelId}" aria-hidden="true" style="max-height:0; opacity:0;">
|
||||
${detailsInner}
|
||||
</div>` : ""}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equalize row heights ONCE, right after initial render while all panels are collapsed.
|
||||
* We set min-height so that opening one panel later will not change the other columns.
|
||||
*/
|
||||
function equalizeRowHeightsOnce() {
|
||||
const desktop = window.matchMedia("(min-width: 992px)").matches;
|
||||
const columns = ["1", "2", "3"]
|
||||
.map((id) => document.getElementById(`hall-${id}-talks`))
|
||||
.filter(Boolean);
|
||||
|
||||
// Reset any previous min-heights (needed if you re-render programmatically)
|
||||
columns.forEach((col) => {
|
||||
col.querySelectorAll(".talk-block").forEach((b) => (b.style.minHeight = ""));
|
||||
});
|
||||
|
||||
if (!desktop || columns.length === 0) return;
|
||||
|
||||
const colRows = columns.map((col) =>
|
||||
Array.from(col.querySelectorAll(".talk-block"))
|
||||
);
|
||||
const maxRows = Math.max(...colRows.map((r) => r.length));
|
||||
|
||||
// For each logical time-row, measure the tallest collapsed block and apply as min-height
|
||||
for (let i = 0; i < maxRows; i++) {
|
||||
const rowElems = colRows.map((r) => r[i]).filter(Boolean);
|
||||
const maxH = Math.max(...rowElems.map((el) => el.offsetHeight));
|
||||
rowElems.forEach((el) => (el.style.minHeight = maxH + "px"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// kleines Debounce
|
||||
function debounce(fn, ms) {
|
||||
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), ms); };
|
||||
}
|
||||
|
||||
// Nach dem Auf-/Zuklappen neu ausrichten
|
||||
const _origWire = wireTalkToggles;
|
||||
wireTalkToggles = function () {
|
||||
const toggles = document.querySelectorAll(".talk-toggle");
|
||||
toggles.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const panelId = btn.getAttribute("aria-controls");
|
||||
const panel = document.getElementById(panelId);
|
||||
if (!panel) return;
|
||||
|
||||
const expanded = btn.getAttribute("aria-expanded") === "true";
|
||||
const nowExpanded = !expanded;
|
||||
|
||||
btn.setAttribute("aria-expanded", String(nowExpanded));
|
||||
panel.setAttribute("aria-hidden", String(!nowExpanded));
|
||||
|
||||
if (nowExpanded) {
|
||||
panel.classList.add("is-open");
|
||||
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||
panel.style.opacity = "1";
|
||||
} else {
|
||||
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||
panel.getBoundingClientRect();
|
||||
panel.classList.remove("is-open");
|
||||
panel.style.maxHeight = "0";
|
||||
panel.style.opacity = "0";
|
||||
}
|
||||
// nach Ende der Transition neu messen
|
||||
setTimeout(equalizeRowHeights, 250);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Run once after you injected the HTML
|
||||
function wireImageLoadReflow(root) {
|
||||
const scope = root || document;
|
||||
scope.querySelectorAll(".talk-image").forEach((img) => {
|
||||
if (img.dataset._reflowBound) return; // avoid double-binding
|
||||
img.dataset._reflowBound = "1";
|
||||
|
||||
img.addEventListener("load", () => {
|
||||
const panel = img.closest(".talk-details");
|
||||
if (panel && panel.classList.contains("is-open")) {
|
||||
// Recompute max-height to fit the now-loaded image
|
||||
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||
}
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
(() => {
|
||||
const STORAGE_KEY = "lang";
|
||||
const DEFAULT_LANG = (document.documentElement.getAttribute("lang") || "de").slice(0,2);
|
||||
let currentDict = {};
|
||||
|
||||
const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
|
||||
const $ = (sel, root=document) => root.querySelector(sel);
|
||||
|
||||
async function loadDict(lang) {
|
||||
const url = `/i18n/${lang}.json?v=3`;
|
||||
const res = await fetch(url, { cache: "force-cache" });
|
||||
if (!res.ok) throw new Error(`i18n: failed ${url}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function applyI18n(dict, lang) {
|
||||
currentDict = dict;
|
||||
|
||||
$$("[data-i18n]").forEach(el => {
|
||||
const key = el.getAttribute("data-i18n");
|
||||
if (dict[key] != null) el.textContent = dict[key];
|
||||
});
|
||||
|
||||
$$("[data-i18n-attrs]").forEach(el => {
|
||||
const pairs = el.getAttribute("data-i18n-attrs").split(";").map(s => s.trim()).filter(Boolean);
|
||||
for (const p of pairs) {
|
||||
const [attr, key] = p.split(":").map(s => s.trim());
|
||||
if (attr && key && dict[key] != null) el.setAttribute(attr, dict[key]);
|
||||
}
|
||||
});
|
||||
|
||||
$$("[data-lang-de]").forEach(el => {
|
||||
const val = lang === "en" ? el.getAttribute("data-lang-en") : el.getAttribute("data-lang-de");
|
||||
if (val) el.textContent = val;
|
||||
});
|
||||
|
||||
const titleEl = document.querySelector("title[data-i18n]");
|
||||
if (titleEl) document.title = dict[titleEl.getAttribute("data-i18n")] || document.title;
|
||||
|
||||
const metaDesc = $('meta[name="description"]');
|
||||
if (metaDesc && dict["seo.description"]) metaDesc.setAttribute("content", dict["seo.description"]);
|
||||
|
||||
document.documentElement.setAttribute("lang", lang);
|
||||
|
||||
$$(".lang-btn").forEach(btn => {
|
||||
btn.classList.toggle("is-active", btn.dataset.lang === lang);
|
||||
});
|
||||
}
|
||||
|
||||
async function setLang(lang) {
|
||||
try {
|
||||
const dict = await loadDict(lang);
|
||||
localStorage.setItem(STORAGE_KEY, lang);
|
||||
applyI18n(dict, lang);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (lang !== DEFAULT_LANG) setLang(DEFAULT_LANG);
|
||||
}
|
||||
}
|
||||
|
||||
// Bind Dropdown
|
||||
document.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".js-set-lang");
|
||||
if (btn) {
|
||||
e.preventDefault();
|
||||
setLang(btn.dataset.lang);
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const urlLang = new URLSearchParams(location.search).get("lang");
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const lang = (urlLang || stored || DEFAULT_LANG).slice(0,2);
|
||||
setLang(lang);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user