import { localeCatalog, localeMessages } from "./site-locales.mjs"; const root = document.documentElement;const toggle = document.querySelector(".theme-toggle");const languageSelect = document.querySelector(".language-select");const metaTheme = document.querySelector('meta[name="theme-color"]');const metaDescription = document.querySelector('meta[name="description"]');const metaOgDescription = document.querySelector('meta[property="og:description"]');const media = window.matchMedia("(prefers-color-scheme: dark)");const supportedLocales = new Set(localeCatalog.map((locale) => locale.code));const rtlLocales = new Set(localeCatalog.filter((locale) => locale.direction === "rtl").map((locale) => locale.code));let activeMessages = localeMessages.en; function normalizeLocale(value) { if (!value) return null; const lower = value.toLowerCase(); const aliases = { "zh-cn": "zh-CN", "zh-hans": "zh-CN", "zh-hant": "zh-TW", "zh-hk": "zh-TW", "zh-tw": "zh-TW", ja: "ja-JP", pt: "pt-BR", "pt-br": "pt-BR", }; if (aliases[lower]) return aliases[lower]; return localeCatalog.find((locale) => locale.code.toLowerCase() === lower || lower.startsWith(`${locale.code.toLowerCase()}-`))?.code ?? null;} function selectedLocale() { const fromDocument = normalizeLocale(root.dataset.locale); if (fromDocument) return fromDocument; try { const queryLocale = normalizeLocale(new URLSearchParams(location.search).get("lang")); if (queryLocale) return queryLocale; const storedLocale = normalizeLocale(localStorage.getItem("codexbar-language")); if (storedLocale) return storedLocale; } catch (_) {} for (const language of navigator.languages || [navigator.language]) { const locale = normalizeLocale(language); if (locale) return locale; } return "en";} function message(key) { return activeMessages[key] || localeMessages.en[key] || key;} function applyLocale(locale) { const resolved = supportedLocales.has(locale) ? locale : "en"; activeMessages = { ...localeMessages.en, ...localeMessages[resolved] }; root.lang = resolved; root.dir = rtlLocales.has(resolved) ? "rtl" : "ltr"; root.dataset.locale = resolved; if (languageSelect) languageSelect.value = resolved; document.title = message("meta.title"); metaDescription?.setAttribute("content", message("meta.description")); metaOgDescription?.setAttribute("content", message("meta.ogDescription")); document.querySelectorAll("[data-i18n]").forEach((element) => { element.textContent = message(element.dataset.i18n); }); document.querySelectorAll("[data-i18n-rich]").forEach((element) => { renderRichMessage(element, message(element.dataset.i18nRich)); }); applyAttributeMessages("data-i18n-aria-label", "aria-label"); applyAttributeMessages("data-i18n-title", "title"); applyAttributeMessages("data-i18n-alt", "alt"); updateThemeUI();} function applyAttributeMessages(dataAttribute, targetAttribute) { document.querySelectorAll(`[${dataAttribute}]`).forEach((element) => { element.setAttribute(targetAttribute, message(element.getAttribute(dataAttribute))); });} function renderRichMessage(element, value) { const fragment = document.createDocumentFragment(); const tokenPattern = /\{([a-zA-Z][a-zA-Z0-9]*)\}/g; let cursor = 0; let match; while ((match = tokenPattern.exec(value)) !== null) { fragment.append(document.createTextNode(value.slice(cursor, match.index))); fragment.append(richToken(match[1])); cursor = match.index + match[0].length; } fragment.append(document.createTextNode(value.slice(cursor))); element.replaceChildren(fragment);} function richToken(name) { const codeTokens = { cask: "brew install --cask steipete/tap/codexbar", codexbar: "codexbar", linuxCommand: "brew install steipete/tap/codexbar", upgrade: "brew upgrade", }; if (codeTokens[name]) { const code = document.createElement("code"); code.textContent = codeTokens[name]; return code; } if (name === "releases") { const link = document.createElement("a"); link.href = "https://github.com/steipete/CodexBar/releases/latest"; link.textContent = "GitHub Releases"; return link; } if (name === "issue") { const link = document.createElement("a"); link.href = "https://github.com/steipete/CodexBar/issues/12"; link.textContent = "issue #12"; return link; } return document.createTextNode(`{${name}}`);} function queryTheme() { try { const value = new URLSearchParams(location.search).get("theme"); return value === "light" || value === "dark" ? value : null; } catch (_) { return null; }} function storedTheme() { try { return localStorage.getItem("codexbar-theme"); } catch (_) { return null; }} function effectiveTheme() { return root.dataset.theme || (media.matches ? "dark" : "light");} function updateThemeImages(theme) { document.querySelectorAll("img[data-dark-src]").forEach((image) => { if (!image.dataset.lightSrc) image.dataset.lightSrc = image.getAttribute("src"); image.setAttribute("src", theme === "dark" ? image.dataset.darkSrc : image.dataset.lightSrc); });} function updateThemeUI() { const theme = effectiveTheme(); updateThemeImages(theme); if (toggle) { const label = message(theme === "dark" ? "theme.toLight" : "theme.toDark"); toggle.setAttribute("aria-pressed", theme === "dark" ? "true" : "false"); toggle.setAttribute("aria-label", label); toggle.title = label; } metaTheme?.setAttribute("content", theme === "dark" ? "#0a0a0c" : "#fbfbfc");} toggle?.addEventListener("click", () => { const next = effectiveTheme() === "dark" ? "light" : "dark"; root.dataset.theme = next; try { localStorage.setItem("codexbar-theme", next); } catch (_) {} updateThemeUI();}); media.addEventListener?.("change", () => { if (!queryTheme() && !storedTheme()) updateThemeUI();}); languageSelect?.addEventListener("change", () => { const locale = normalizeLocale(languageSelect.value) || "en"; try { localStorage.setItem("codexbar-language", locale); const url = new URL(location.href); url.searchParams.set("lang", locale); history.replaceState(null, "", url); } catch (_) {} applyLocale(locale);}); const copyButton = document.querySelector(".brew-copy");copyButton?.addEventListener("click", () => { const text = document.getElementById("brew-cmd")?.textContent; if (!text || !navigator.clipboard) return; navigator.clipboard.writeText(text).then(() => { copyButton.classList.add("copied"); copyButton.setAttribute("aria-label", message("clipboard.copied")); setTimeout(() => { copyButton.classList.remove("copied"); copyButton.setAttribute("aria-label", message("clipboard.copy")); }, 1500); });}); applyLocale(selectedLocale());
import { localeCatalog, localeMessages } from "./site-locales.mjs";
const root = document.documentElement;
const toggle = document.querySelector(".theme-toggle");
const languageSelect = document.querySelector(".language-select");
const metaTheme = document.querySelector('meta[name="theme-color"]');
const metaDescription = document.querySelector('meta[name="description"]');
const metaOgDescription = document.querySelector('meta[property="og:description"]');
const media = window.matchMedia("(prefers-color-scheme: dark)");
const supportedLocales = new Set(localeCatalog.map((locale) => locale.code));
const rtlLocales = new Set(localeCatalog.filter((locale) => locale.direction === "rtl").map((locale) => locale.code));
let activeMessages = localeMessages.en;
function normalizeLocale(value) {
if (!value) return null;
const lower = value.toLowerCase();
const aliases = {
"zh-cn": "zh-CN",
"zh-hans": "zh-CN",
"zh-hant": "zh-TW",
"zh-hk": "zh-TW",
"zh-tw": "zh-TW",
ja: "ja-JP",
pt: "pt-BR",
"pt-br": "pt-BR",
};
if (aliases[lower]) return aliases[lower];
return localeCatalog.find((locale) =>
locale.code.toLowerCase() === lower || lower.startsWith(`${locale.code.toLowerCase()}-`))?.code ?? null;
}
function selectedLocale() {
const fromDocument = normalizeLocale(root.dataset.locale);
if (fromDocument) return fromDocument;
try {
const queryLocale = normalizeLocale(new URLSearchParams(location.search).get("lang"));
if (queryLocale) return queryLocale;
const storedLocale = normalizeLocale(localStorage.getItem("codexbar-language"));
if (storedLocale) return storedLocale;
} catch (_) {}
for (const language of navigator.languages || [navigator.language]) {
const locale = normalizeLocale(language);
if (locale) return locale;
}
return "en";
}
function message(key) {
return activeMessages[key] || localeMessages.en[key] || key;
}
function applyLocale(locale) {
const resolved = supportedLocales.has(locale) ? locale : "en";
activeMessages = { ...localeMessages.en, ...localeMessages[resolved] };
root.lang = resolved;
root.dir = rtlLocales.has(resolved) ? "rtl" : "ltr";
root.dataset.locale = resolved;
if (languageSelect) languageSelect.value = resolved;
document.title = message("meta.title");
metaDescription?.setAttribute("content", message("meta.description"));
metaOgDescription?.setAttribute("content", message("meta.ogDescription"));
document.querySelectorAll("[data-i18n]").forEach((element) => {
element.textContent = message(element.dataset.i18n);
});
document.querySelectorAll("[data-i18n-rich]").forEach((element) => {
renderRichMessage(element, message(element.dataset.i18nRich));
});
applyAttributeMessages("data-i18n-aria-label", "aria-label");
applyAttributeMessages("data-i18n-title", "title");
applyAttributeMessages("data-i18n-alt", "alt");
updateThemeUI();
}
function applyAttributeMessages(dataAttribute, targetAttribute) {
document.querySelectorAll(`[${dataAttribute}]`).forEach((element) => {
element.setAttribute(targetAttribute, message(element.getAttribute(dataAttribute)));
});
}
function renderRichMessage(element, value) {
const fragment = document.createDocumentFragment();
const tokenPattern = /\{([a-zA-Z][a-zA-Z0-9]*)\}/g;
let cursor = 0;
let match;
while ((match = tokenPattern.exec(value)) !== null) {
fragment.append(document.createTextNode(value.slice(cursor, match.index)));
fragment.append(richToken(match[1]));
cursor = match.index + match[0].length;
}
fragment.append(document.createTextNode(value.slice(cursor)));
element.replaceChildren(fragment);
}
function richToken(name) {
const codeTokens = {
cask: "brew install --cask steipete/tap/codexbar",
codexbar: "codexbar",
linuxCommand: "brew install steipete/tap/codexbar",
upgrade: "brew upgrade",
};
if (codeTokens[name]) {
const code = document.createElement("code");
code.textContent = codeTokens[name];
return code;
}
if (name === "releases") {
const link = document.createElement("a");
link.href = "https://github.com/steipete/CodexBar/releases/latest";
link.textContent = "GitHub Releases";
return link;
}
if (name === "issue") {
const link = document.createElement("a");
link.href = "https://github.com/steipete/CodexBar/issues/12";
link.textContent = "issue #12";
return link;
}
return document.createTextNode(`{${name}}`);
}
function queryTheme() {
try {
const value = new URLSearchParams(location.search).get("theme");
return value === "light" || value === "dark" ? value : null;
} catch (_) {
return null;
}
}
function storedTheme() {
try {
return localStorage.getItem("codexbar-theme");
} catch (_) {
return null;
}
}
function effectiveTheme() {
return root.dataset.theme || (media.matches ? "dark" : "light");
}
function updateThemeImages(theme) {
document.querySelectorAll("img[data-dark-src]").forEach((image) => {
if (!image.dataset.lightSrc) image.dataset.lightSrc = image.getAttribute("src");
image.setAttribute("src", theme === "dark" ? image.dataset.darkSrc : image.dataset.lightSrc);
});
}
function updateThemeUI() {
const theme = effectiveTheme();
updateThemeImages(theme);
if (toggle) {
const label = message(theme === "dark" ? "theme.toLight" : "theme.toDark");
toggle.setAttribute("aria-pressed", theme === "dark" ? "true" : "false");
toggle.setAttribute("aria-label", label);
toggle.title = label;
}
metaTheme?.setAttribute("content", theme === "dark" ? "#0a0a0c" : "#fbfbfc");
}
toggle?.addEventListener("click", () => {
const next = effectiveTheme() === "dark" ? "light" : "dark";
root.dataset.theme = next;
try {
localStorage.setItem("codexbar-theme", next);
} catch (_) {}
updateThemeUI();
});
media.addEventListener?.("change", () => {
if (!queryTheme() && !storedTheme()) updateThemeUI();
});
languageSelect?.addEventListener("change", () => {
const locale = normalizeLocale(languageSelect.value) || "en";
try {
localStorage.setItem("codexbar-language", locale);
const url = new URL(location.href);
url.searchParams.set("lang", locale);
history.replaceState(null, "", url);
} catch (_) {}
applyLocale(locale);
});
const copyButton = document.querySelector(".brew-copy");
copyButton?.addEventListener("click", () => {
const text = document.getElementById("brew-cmd")?.textContent;
if (!text || !navigator.clipboard) return;
navigator.clipboard.writeText(text).then(() => {
copyButton.classList.add("copied");
copyButton.setAttribute("aria-label", message("clipboard.copied"));
setTimeout(() => {
copyButton.classList.remove("copied");
copyButton.setAttribute("aria-label", message("clipboard.copy"));
}, 1500);
});
});
applyLocale(selectedLocale());