add: 添加 i18n 翻译

This commit is contained in:
ci
2026-06-19 13:05:35 +08:00
parent 5ebe562a62
commit 73dfe12c95
8 changed files with 654 additions and 189 deletions
+53 -10
View File
@@ -1,7 +1,9 @@
// app.js — entry point. Routes hash changes to view renderers and
// keeps the top-nav active state in sync.
// app.js — entry point. Routes hash changes to view renderers, keeps
// the top-nav active state in sync, and re-renders chrome strings when
// the locale changes.
import { onRoute } from "./state.js";
import { t, currentLocale, setLocale, onLocale } from "./i18n.js";
import { renderRemotes } from "./views/remotes.js";
import { renderBrowse } from "./views/browser.js";
import { renderJobs, renderNewJob, stopJobPolling } from "./views/jobs.js";
@@ -19,12 +21,13 @@ const views = {
"configure-edit": renderConfigureEdit,
};
let currentRoute = { name: "remotes", params: {} };
function setActiveNav(routeName) {
const links = document.querySelectorAll("#nav-links a");
for (const a of links) {
const target = a.dataset.route;
let isActive = target === routeName;
// "Configure" is active on both configure-new and configure-edit
if (target === "configure" && routeName.startsWith("configure-")) {
isActive = true;
}
@@ -32,26 +35,66 @@ function setActiveNav(routeName) {
}
}
onRoute(async (route) => {
function applyChromeStrings() {
// nav links
document.querySelectorAll("#nav-links a").forEach((a) => {
const key = a.dataset.route === "configure" ? "nav.configure" : `nav.${a.dataset.route}`;
a.textContent = t(key);
});
const newJobBtn = document.querySelector(".nav-new-job");
if (newJobBtn) newJobBtn.textContent = t("nav.new_job");
const lt = document.getElementById("loading-title");
if (lt) lt.textContent = t("loading.title");
const lb = document.getElementById("loading-body");
if (lb) lb.innerHTML = t("loading.connecting");
// Anything with data-i18n attribute (footer)
document.querySelectorAll("[data-i18n]").forEach((el) => {
el.textContent = t(el.dataset.i18n);
});
// lang buttons
const locale = currentLocale();
document.querySelectorAll(".lang-btn").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.lang === locale);
});
}
function wireLangSwitcher() {
document.querySelectorAll(".lang-btn").forEach((btn) => {
btn.addEventListener("click", () => setLocale(btn.dataset.lang));
});
}
onRoute((route) => {
currentRoute = route;
setActiveNav(route.name);
// Stop jobs polling when leaving the jobs view
if (route.name !== "jobs") {
stopJobPolling();
}
const renderer = views[route.name] || renderRemotes;
try {
await renderer(route.params);
} catch (e) {
renderer(route.params).catch((e) => {
console.error("view error", e);
const app = document.getElementById("app");
if (app) {
app.innerHTML = `<div class="empty"><h3>Something went wrong</h3><p>${escapeHtml(e.message)}</p></div>`;
app.innerHTML = `<div class="empty"><h3>${t("error.title")}</h3><p>${escapeHtml(e.message)}</p></div>`;
}
}
});
});
// Re-render everything on locale change so views pick up new strings.
onLocale(() => {
applyChromeStrings();
const renderer = views[currentRoute.name] || renderRemotes;
renderer(currentRoute.params).catch((e) => {
console.error("view error on locale change", e);
});
});
// Initial chrome paint + switcher wiring.
applyChromeStrings();
wireLangSwitcher();
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&amp;")