fix(webui): sync localized preboot copy

This commit is contained in:
chengyongru
2026-07-13 23:28:19 +08:00
committed by chengyongru
parent 1e7518a207
commit 6b5820ea89
3 changed files with 103 additions and 15 deletions
+18 -11
View File
@@ -90,6 +90,17 @@
} catch {}
})();
</script>
<title>nanobot</title>
</head>
<body class="bg-background text-foreground antialiased">
<div id="root">
<div class="boot-splash">
<div class="boot-splash-inner">
<span class="boot-dot" aria-hidden="true"></span>
<span data-boot-copy>Loading nanobot…</span>
</div>
</div>
</div>
<script>
(function () {
var localeKey = "nanobot.locale";
@@ -122,6 +133,10 @@
boot: "Cargando nanobot…",
description: "Interfaz web de nanobot: conversa con tu espacio de trabajo de nanobot."
},
"pt-BR": {
boot: "Carregando nanobot…",
description: "Interface web do nanobot — converse com o seu workspace do nanobot."
},
vi: {
boot: "Đang tải nanobot…",
description: "Giao diện web nanobot — trò chuyện với workspace nanobot của bạn."
@@ -149,6 +164,9 @@
) {
return "zh-TW";
}
if (lower === "pt" || lower.indexOf("pt-") === 0) {
return "pt-BR";
}
var base = lower.split("-")[0];
return copy[base] ? base : "en";
}
@@ -170,17 +188,6 @@
} catch {}
})();
</script>
<title>nanobot</title>
</head>
<body class="bg-background text-foreground antialiased">
<div id="root">
<div class="boot-splash">
<div class="boot-splash-inner">
<span class="boot-dot" aria-hidden="true"></span>
<span data-boot-copy>Loading nanobot…</span>
</div>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+2 -2
View File
@@ -165,7 +165,7 @@
"webuiDefaultAccess": "Acesso padrão",
"cliAppsCatalog": "Catálogo",
"cliAppsFilter": "Filtro",
"engine": "Engine",
"engine": "Motor",
"logs": "Logs",
"diagnostics": "Diagnóstico",
"contextWindow": "Janela de contexto",
@@ -588,7 +588,7 @@
"editTitle": "Editar automação",
"save": "Salvar",
"deleteTitle": "Excluir automação",
"deleteDescription": "Isso remove {{name}} do store de cron. As mensagens anteriores da conversa permanecem na sessão.",
"deleteDescription": "Isso remove {{name}} do armazenamento do cron. As mensagens anteriores da conversa permanecem na sessão.",
"cancel": "Cancelar",
"status": {
"system": "Sistema",
+82 -1
View File
@@ -1,3 +1,7 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { runInNewContext } from "node:vm";
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
@@ -5,7 +9,11 @@ import { describe, expect, it, vi } from "vitest";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import { ThreadComposer } from "@/components/thread/ThreadComposer";
import { resources } from "@/i18n";
import { LOCALE_STORAGE_KEY, resolveInitialLocale } from "@/i18n/config";
import {
LOCALE_STORAGE_KEY,
resolveInitialLocale,
supportedLocales,
} from "@/i18n/config";
const QUICK_ACTION_KEYS = ["plan", "analyze", "brainstorm", "code", "summarize", "more"];
const IMAGE_QUICK_ACTION_KEYS = ["icon", "sticker", "poster", "product", "portrait", "edit"];
@@ -128,6 +136,46 @@ const LOCALIZED_WORKSPACE_COPY_KEYS = [
"workspace.dialog.usePath",
"workspace.dialog.absolutePathRequired",
];
const INDEX_HTML = readFileSync(resolve(process.cwd(), "index.html"), "utf8");
const PREBOOT_SCRIPT = INDEX_HTML.match(
/<script>\s*(\(function \(\) \{\s*var localeKey = "nanobot\.locale";[\s\S]*?\}\)\(\);)\s*<\/script>/,
)?.[1];
const BOOT_COPY_MARKUP = '<span data-boot-copy>Loading nanobot…</span>';
function runPrebootLocale(storedLocale: string) {
if (!PREBOOT_SCRIPT) throw new Error("Could not find the preboot locale script in index.html");
const documentElement = { lang: "" };
const meta = {
content: "",
setAttribute(name: string, value: string) {
if (name === "content") this.content = value;
},
};
const boot = { textContent: "" };
runInNewContext(PREBOOT_SCRIPT, {
localStorage: {
getItem: (key: string) => key === LOCALE_STORAGE_KEY ? storedLocale : null,
},
navigator: { languages: [], language: "en" },
document: {
documentElement,
querySelector: (selector: string) => {
if (selector === '[data-i18n-meta="description"]') return meta;
if (selector === "[data-boot-copy]") return boot;
return null;
},
},
});
return {
lang: documentElement.lang,
boot: boot.textContent,
description: meta.content,
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
@@ -156,6 +204,39 @@ function interpolationKeys(value: unknown): string[] {
}
describe("webui i18n", () => {
it("runs preboot localization after the splash copy exists", () => {
expect(INDEX_HTML.indexOf(BOOT_COPY_MARKUP)).toBeGreaterThan(-1);
expect(INDEX_HTML.indexOf(PREBOOT_SCRIPT ?? "")).toBeGreaterThan(
INDEX_HTML.indexOf(BOOT_COPY_MARKUP),
);
});
it("keeps preboot copy aligned with every registered locale", () => {
for (const { code } of supportedLocales) {
const result = runPrebootLocale(code);
const expected = resources[code].common.app;
expect({ code, ...result }).toEqual({
code,
lang: code,
boot: expected.loading.boot,
description: expected.meta.description,
});
}
});
it("normalizes Portuguese locales before the app bundle loads", () => {
const expected = resources["pt-BR"].common.app;
for (const locale of ["pt", "pt-PT"]) {
expect(runPrebootLocale(locale)).toEqual({
lang: "pt-BR",
boot: expected.loading.boot,
description: expected.meta.description,
});
}
});
it("defaults to English until the user chooses another language", () => {
localStorage.removeItem(LOCALE_STORAGE_KEY);
expect(resolveInitialLocale()).toBe("en");