feat(webui): add i18n support and locale switcher

This commit is contained in:
Xubin Ren
2026-04-19 06:39:06 +00:00
parent be10ba1f0d
commit 4650b23d75
34 changed files with 6654 additions and 65 deletions
+93
View File
@@ -0,0 +1,93 @@
export const LOCALE_STORAGE_KEY = "nanobot.locale";
export const supportedLocales = [
{ code: "en", label: "English", nativeLabel: "English" },
{ code: "zh-CN", label: "Chinese (Simplified)", nativeLabel: "简体中文" },
{ code: "zh-TW", label: "Chinese (Traditional)", nativeLabel: "繁體中文" },
{ code: "fr", label: "French", nativeLabel: "Français" },
{ code: "ja", label: "Japanese", nativeLabel: "日本語" },
{ code: "ko", label: "Korean", nativeLabel: "한국어" },
{ code: "es", label: "Spanish", nativeLabel: "Español" },
{ code: "vi", label: "Vietnamese", nativeLabel: "Tiếng Việt" },
{ code: "id", label: "Indonesian", nativeLabel: "Bahasa Indonesia" },
] as const;
export type SupportedLocale = (typeof supportedLocales)[number]["code"];
export const defaultLocale: SupportedLocale = "en";
export const fallbackLocale: SupportedLocale = "en";
export function normalizeLocale(
input: string | null | undefined,
): SupportedLocale {
if (!input) return defaultLocale;
const trimmed = input.trim();
if (!trimmed) return defaultLocale;
const exact = supportedLocales.find((locale) => locale.code === trimmed);
if (exact) return exact.code;
const lower = trimmed.toLowerCase();
if (lower === "zh" || lower.startsWith("zh-cn") || lower.startsWith("zh-sg")) {
return "zh-CN";
}
if (
lower.startsWith("zh-tw") ||
lower.startsWith("zh-hk") ||
lower.startsWith("zh-mo") ||
lower.startsWith("zh-hant")
) {
return "zh-TW";
}
const base = lower.split("-")[0];
const baseMatch = supportedLocales.find(
(locale) => locale.code.toLowerCase() === base,
);
return baseMatch?.code ?? defaultLocale;
}
export function readStoredLocale(): SupportedLocale | null {
if (typeof window === "undefined") return null;
try {
const raw = window.localStorage.getItem(LOCALE_STORAGE_KEY);
return raw ? normalizeLocale(raw) : null;
} catch {
return null;
}
}
export function detectNavigatorLocale(): SupportedLocale {
if (typeof navigator === "undefined") return defaultLocale;
const candidates = [
...(navigator.languages ?? []),
navigator.language,
].filter(Boolean);
for (const locale of candidates) {
const normalized = normalizeLocale(locale);
if (normalized) return normalized;
}
return defaultLocale;
}
export function resolveInitialLocale(): SupportedLocale {
return readStoredLocale() ?? detectNavigatorLocale();
}
export function persistLocale(locale: SupportedLocale): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(LOCALE_STORAGE_KEY, locale);
} catch {
// ignore storage errors
}
}
export function applyDocumentLocale(locale: SupportedLocale): void {
if (typeof document === "undefined") return;
document.documentElement.lang = locale;
}
export function localeOption(locale: SupportedLocale) {
return supportedLocales.find((entry) => entry.code === locale) ?? supportedLocales[0];
}
+72
View File
@@ -0,0 +1,72 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import {
applyDocumentLocale,
defaultLocale,
fallbackLocale,
LOCALE_STORAGE_KEY,
normalizeLocale,
persistLocale,
resolveInitialLocale,
type SupportedLocale,
} from "./config";
import enCommon from "./locales/en/common.json";
import zhCNCommon from "./locales/zh-CN/common.json";
import zhTWCommon from "./locales/zh-TW/common.json";
import frCommon from "./locales/fr/common.json";
import jaCommon from "./locales/ja/common.json";
import koCommon from "./locales/ko/common.json";
import esCommon from "./locales/es/common.json";
import viCommon from "./locales/vi/common.json";
import idCommon from "./locales/id/common.json";
export const resources = {
en: { common: enCommon },
"zh-CN": { common: zhCNCommon },
"zh-TW": { common: zhTWCommon },
fr: { common: frCommon },
ja: { common: jaCommon },
ko: { common: koCommon },
es: { common: esCommon },
vi: { common: viCommon },
id: { common: idCommon },
} as const;
export function currentLocale(): SupportedLocale {
return normalizeLocale(i18n.resolvedLanguage ?? i18n.language ?? defaultLocale);
}
export async function setAppLanguage(locale: SupportedLocale): Promise<void> {
await i18n.changeLanguage(locale);
}
if (!i18n.isInitialized) {
void i18n
.use(initReactI18next)
.init({
resources,
lng: resolveInitialLocale(),
fallbackLng: fallbackLocale,
defaultNS: "common",
ns: ["common"],
interpolation: {
escapeValue: false,
},
returnNull: false,
supportedLngs: Object.keys(resources),
});
}
const syncLocaleSideEffects = (language: string) => {
const locale = normalizeLocale(language);
applyDocumentLocale(locale);
persistLocale(locale);
};
syncLocaleSideEffects(currentLocale());
i18n.on("languageChanged", syncLocaleSideEffects);
export { LOCALE_STORAGE_KEY };
export default i18n;
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "Connecting to nanobot…",
"boot": "Loading nanobot…"
},
"error": {
"title": "Couldn't reach nanobot",
"gatewayHint": "Make sure the gateway is running (`nanobot web`) and that this page is open on the same machine."
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "nanobot web UI — chat with your nanobot workspace."
}
},
"sidebar": {
"collapse": "Collapse sidebar",
"toggleTheme": "Toggle theme",
"newChat": "New chat",
"recent": "Recent",
"refreshSessions": "Refresh sessions",
"language": {
"label": "Language",
"ariaLabel": "Change language"
}
},
"chat": {
"fallbackTitle": "Chat {{id}}",
"loading": "Loading…",
"noSessions": "No sessions yet.",
"actions": "Chat actions for {{title}}",
"delete": "Delete",
"newChat": "New chat"
},
"deleteConfirm": {
"title": "Delete “{{title}}”?",
"description": "The session file will be removed from disk. This cannot be undone.",
"cancel": "Cancel",
"confirm": "Delete"
},
"connection": {
"idle": "Idle",
"connecting": "Connecting…",
"open": "Connected",
"reconnecting": "Reconnecting…",
"closed": "Disconnected",
"error": "Connection error"
},
"thread": {
"loadingConversation": "Loading conversation…",
"empty": {
"description": "Ask questions, continue local work, or start a new thread."
},
"header": {
"toggleSidebar": "Toggle sidebar"
},
"composer": {
"placeholderThread": "Type your message…",
"placeholderHero": "What's on your mind?",
"placeholderOpening": "Opening a new chat…",
"inputAria": "Message input",
"sendHint": "Enter to send · Shift+Enter for newline",
"send": "Send message"
},
"scrollToBottom": "Scroll to bottom"
},
"message": {
"streaming": "streaming",
"assistantTyping": "Assistant is typing",
"toolSingle": "Using a tool",
"toolMany": "Used {{count}} tools"
},
"code": {
"fallbackLanguage": "code",
"copyAria": "Copy code",
"copy": "Copy",
"copied": "Copied"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "Conectando con nanobot…",
"boot": "Cargando nanobot…"
},
"error": {
"title": "No se pudo conectar con nanobot",
"gatewayHint": "Asegúrate de que la gateway esté en ejecución (`nanobot web`) y de que esta página esté abierta en la misma máquina."
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "Interfaz web de nanobot: conversa con tu espacio de trabajo de nanobot."
}
},
"sidebar": {
"collapse": "Contraer barra lateral",
"toggleTheme": "Cambiar tema",
"newChat": "Nuevo chat",
"recent": "Recientes",
"refreshSessions": "Actualizar sesiones",
"language": {
"label": "Idioma",
"ariaLabel": "Cambiar idioma"
}
},
"chat": {
"fallbackTitle": "Chat {{id}}",
"loading": "Cargando…",
"noSessions": "Todavía no hay sesiones.",
"actions": "Acciones del chat {{title}}",
"delete": "Eliminar",
"newChat": "Nuevo chat"
},
"deleteConfirm": {
"title": "¿Eliminar “{{title}}”?",
"description": "El archivo de sesión se eliminará del disco. Esta acción no se puede deshacer.",
"cancel": "Cancelar",
"confirm": "Eliminar"
},
"connection": {
"idle": "Inactivo",
"connecting": "Conectando…",
"open": "Conectado",
"reconnecting": "Reconectando…",
"closed": "Desconectado",
"error": "Error de conexión"
},
"thread": {
"loadingConversation": "Cargando conversación…",
"empty": {
"description": "Haz preguntas, continúa tu trabajo local o inicia un nuevo hilo."
},
"header": {
"toggleSidebar": "Mostrar u ocultar la barra lateral"
},
"composer": {
"placeholderThread": "Escribe tu mensaje…",
"placeholderHero": "¿Qué tienes en mente?",
"placeholderOpening": "Abriendo un nuevo chat…",
"inputAria": "Entrada de mensaje",
"sendHint": "Enter para enviar · Shift+Enter para nueva línea",
"send": "Enviar mensaje"
},
"scrollToBottom": "Desplazarse al final"
},
"message": {
"streaming": "transmitiendo",
"assistantTyping": "El asistente está escribiendo",
"toolSingle": "Usando una herramienta",
"toolMany": "Se usaron {{count}} herramientas"
},
"code": {
"fallbackLanguage": "código",
"copyAria": "Copiar código",
"copy": "Copiar",
"copied": "Copiado"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "Connexion à nanobot…",
"boot": "Chargement de nanobot…"
},
"error": {
"title": "Impossible de joindre nanobot",
"gatewayHint": "Assurez-vous que la gateway est en cours dexécution (`nanobot web`) et que cette page est ouverte sur la même machine."
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "Interface web nanobot — discutez avec votre espace de travail nanobot."
}
},
"sidebar": {
"collapse": "Réduire la barre latérale",
"toggleTheme": "Changer de thème",
"newChat": "Nouvelle discussion",
"recent": "Récentes",
"refreshSessions": "Actualiser les sessions",
"language": {
"label": "Langue",
"ariaLabel": "Changer de langue"
}
},
"chat": {
"fallbackTitle": "Discussion {{id}}",
"loading": "Chargement…",
"noSessions": "Aucune session pour le moment.",
"actions": "Actions de la discussion {{title}}",
"delete": "Supprimer",
"newChat": "Nouvelle discussion"
},
"deleteConfirm": {
"title": "Supprimer « {{title}} » ?",
"description": "Le fichier de session sera supprimé du disque. Cette action est irréversible.",
"cancel": "Annuler",
"confirm": "Supprimer"
},
"connection": {
"idle": "Inactif",
"connecting": "Connexion…",
"open": "Connecté",
"reconnecting": "Reconnexion…",
"closed": "Déconnecté",
"error": "Erreur de connexion"
},
"thread": {
"loadingConversation": "Chargement de la conversation…",
"empty": {
"description": "Posez des questions, poursuivez votre travail local ou démarrez un nouveau fil."
},
"header": {
"toggleSidebar": "Afficher ou masquer la barre latérale"
},
"composer": {
"placeholderThread": "Saisissez votre message…",
"placeholderHero": "Quavez-vous en tête ?",
"placeholderOpening": "Ouverture dune nouvelle discussion…",
"inputAria": "Champ de message",
"sendHint": "Entrée pour envoyer · Maj+Entrée pour un retour à la ligne",
"send": "Envoyer le message"
},
"scrollToBottom": "Faire défiler vers le bas"
},
"message": {
"streaming": "en cours de génération",
"assistantTyping": "Lassistant est en train d’écrire",
"toolSingle": "Utilisation dun outil",
"toolMany": "{{count}} outils utilisés"
},
"code": {
"fallbackLanguage": "code",
"copyAria": "Copier le code",
"copy": "Copier",
"copied": "Copié"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "Menghubungkan ke nanobot…",
"boot": "Memuat nanobot…"
},
"error": {
"title": "Tidak dapat menjangkau nanobot",
"gatewayHint": "Pastikan gateway sedang berjalan (`nanobot web`) dan halaman ini dibuka pada mesin yang sama."
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "UI web nanobot — ngobrol dengan workspace nanobot Anda."
}
},
"sidebar": {
"collapse": "Ciutkan sidebar",
"toggleTheme": "Ganti tema",
"newChat": "Obrolan baru",
"recent": "Terbaru",
"refreshSessions": "Segarkan sesi",
"language": {
"label": "Bahasa",
"ariaLabel": "Ganti bahasa"
}
},
"chat": {
"fallbackTitle": "Obrolan {{id}}",
"loading": "Memuat…",
"noSessions": "Belum ada sesi.",
"actions": "Aksi obrolan untuk {{title}}",
"delete": "Hapus",
"newChat": "Obrolan baru"
},
"deleteConfirm": {
"title": "Hapus “{{title}}”?",
"description": "File sesi akan dihapus dari disk. Tindakan ini tidak dapat dibatalkan.",
"cancel": "Batal",
"confirm": "Hapus"
},
"connection": {
"idle": "Idle",
"connecting": "Menghubungkan…",
"open": "Terhubung",
"reconnecting": "Menyambung ulang…",
"closed": "Terputus",
"error": "Kesalahan koneksi"
},
"thread": {
"loadingConversation": "Memuat percakapan…",
"empty": {
"description": "Ajukan pertanyaan, lanjutkan pekerjaan lokal, atau mulai thread baru."
},
"header": {
"toggleSidebar": "Tampilkan atau sembunyikan sidebar"
},
"composer": {
"placeholderThread": "Ketik pesan Anda…",
"placeholderHero": "Apa yang sedang Anda pikirkan?",
"placeholderOpening": "Membuka obrolan baru…",
"inputAria": "Input pesan",
"sendHint": "Enter untuk kirim · Shift+Enter untuk baris baru",
"send": "Kirim pesan"
},
"scrollToBottom": "Gulir ke bawah"
},
"message": {
"streaming": "sedang mengalir",
"assistantTyping": "Asisten sedang mengetik",
"toolSingle": "Menggunakan sebuah alat",
"toolMany": "Menggunakan {{count}} alat"
},
"code": {
"fallbackLanguage": "kode",
"copyAria": "Salin kode",
"copy": "Salin",
"copied": "Tersalin"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "nanobot に接続中…",
"boot": "nanobot を読み込み中…"
},
"error": {
"title": "nanobot に接続できませんでした",
"gatewayHint": "gateway`nanobot web`)が起動しており、このページが同じマシン上で開かれていることを確認してください。"
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "nanobot Web UI — nanobot ワークスペースと会話します。"
}
},
"sidebar": {
"collapse": "サイドバーを閉じる",
"toggleTheme": "テーマを切り替える",
"newChat": "新しいチャット",
"recent": "最近のチャット",
"refreshSessions": "セッションを更新",
"language": {
"label": "言語",
"ariaLabel": "言語を変更"
}
},
"chat": {
"fallbackTitle": "チャット {{id}}",
"loading": "読み込み中…",
"noSessions": "まだセッションがありません。",
"actions": "「{{title}}」のチャット操作",
"delete": "削除",
"newChat": "新しいチャット"
},
"deleteConfirm": {
"title": "「{{title}}」を削除しますか?",
"description": "セッションファイルはディスクから削除されます。この操作は元に戻せません。",
"cancel": "キャンセル",
"confirm": "削除"
},
"connection": {
"idle": "待機中",
"connecting": "接続中…",
"open": "接続済み",
"reconnecting": "再接続中…",
"closed": "切断済み",
"error": "接続エラー"
},
"thread": {
"loadingConversation": "会話を読み込み中…",
"empty": {
"description": "質問したり、ローカル作業を続けたり、新しいスレッドを始めたりできます。"
},
"header": {
"toggleSidebar": "サイドバーを切り替える"
},
"composer": {
"placeholderThread": "メッセージを入力…",
"placeholderHero": "何を考えていますか?",
"placeholderOpening": "新しいチャットを開いています…",
"inputAria": "メッセージ入力欄",
"sendHint": "Enter で送信 · Shift+Enter で改行",
"send": "メッセージを送信"
},
"scrollToBottom": "一番下へスクロール"
},
"message": {
"streaming": "生成中",
"assistantTyping": "アシスタントが入力中",
"toolSingle": "ツールを使用中",
"toolMany": "{{count}} 個のツールを使用"
},
"code": {
"fallbackLanguage": "コード",
"copyAria": "コードをコピー",
"copy": "コピー",
"copied": "コピーしました"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "nanobot에 연결 중…",
"boot": "nanobot 불러오는 중…"
},
"error": {
"title": "nanobot에 연결할 수 없습니다",
"gatewayHint": "gateway(`nanobot web`)가 실행 중인지, 그리고 이 페이지가 같은 머신에서 열려 있는지 확인하세요."
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "nanobot 웹 UI — nanobot 작업공간과 대화하세요."
}
},
"sidebar": {
"collapse": "사이드바 접기",
"toggleTheme": "테마 전환",
"newChat": "새 채팅",
"recent": "최근 대화",
"refreshSessions": "세션 새로고침",
"language": {
"label": "언어",
"ariaLabel": "언어 변경"
}
},
"chat": {
"fallbackTitle": "채팅 {{id}}",
"loading": "불러오는 중…",
"noSessions": "아직 세션이 없습니다.",
"actions": "{{title}} 채팅 작업",
"delete": "삭제",
"newChat": "새 채팅"
},
"deleteConfirm": {
"title": "“{{title}}”을(를) 삭제할까요?",
"description": "세션 파일이 디스크에서 제거됩니다. 이 작업은 되돌릴 수 없습니다.",
"cancel": "취소",
"confirm": "삭제"
},
"connection": {
"idle": "대기 중",
"connecting": "연결 중…",
"open": "연결됨",
"reconnecting": "재연결 중…",
"closed": "연결 끊김",
"error": "연결 오류"
},
"thread": {
"loadingConversation": "대화 불러오는 중…",
"empty": {
"description": "질문을 하거나, 로컬 작업을 이어가거나, 새 스레드를 시작할 수 있습니다."
},
"header": {
"toggleSidebar": "사이드바 전환"
},
"composer": {
"placeholderThread": "메시지를 입력하세요…",
"placeholderHero": "무슨 생각을 하고 있나요?",
"placeholderOpening": "새 채팅을 여는 중…",
"inputAria": "메시지 입력",
"sendHint": "Enter로 전송 · Shift+Enter로 줄바꿈",
"send": "메시지 보내기"
},
"scrollToBottom": "맨 아래로 스크롤"
},
"message": {
"streaming": "생성 중",
"assistantTyping": "도우미가 입력 중",
"toolSingle": "도구 사용 중",
"toolMany": "도구 {{count}}개 사용됨"
},
"code": {
"fallbackLanguage": "코드",
"copyAria": "코드 복사",
"copy": "복사",
"copied": "복사됨"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "Đang kết nối tới nanobot…",
"boot": "Đang tải nanobot…"
},
"error": {
"title": "Không thể kết nối tới nanobot",
"gatewayHint": "Hãy chắc chắn gateway đang chạy (`nanobot web`) và trang này được mở trên cùng máy."
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "Giao diện web nanobot — trò chuyện với workspace nanobot của bạn."
}
},
"sidebar": {
"collapse": "Thu gọn thanh bên",
"toggleTheme": "Chuyển giao diện",
"newChat": "Cuộc trò chuyện mới",
"recent": "Gần đây",
"refreshSessions": "Làm mới phiên",
"language": {
"label": "Ngôn ngữ",
"ariaLabel": "Đổi ngôn ngữ"
}
},
"chat": {
"fallbackTitle": "Trò chuyện {{id}}",
"loading": "Đang tải…",
"noSessions": "Chưa có phiên nào.",
"actions": "Tác vụ cho cuộc trò chuyện {{title}}",
"delete": "Xóa",
"newChat": "Cuộc trò chuyện mới"
},
"deleteConfirm": {
"title": "Xóa “{{title}}”?",
"description": "Tệp phiên sẽ bị xóa khỏi đĩa. Không thể hoàn tác thao tác này.",
"cancel": "Hủy",
"confirm": "Xóa"
},
"connection": {
"idle": "Rảnh",
"connecting": "Đang kết nối…",
"open": "Đã kết nối",
"reconnecting": "Đang kết nối lại…",
"closed": "Đã ngắt kết nối",
"error": "Lỗi kết nối"
},
"thread": {
"loadingConversation": "Đang tải cuộc trò chuyện…",
"empty": {
"description": "Hãy đặt câu hỏi, tiếp tục công việc cục bộ hoặc bắt đầu một luồng mới."
},
"header": {
"toggleSidebar": "Bật/tắt thanh bên"
},
"composer": {
"placeholderThread": "Nhập tin nhắn…",
"placeholderHero": "Bạn đang nghĩ gì?",
"placeholderOpening": "Đang mở cuộc trò chuyện mới…",
"inputAria": "Ô nhập tin nhắn",
"sendHint": "Enter để gửi · Shift+Enter để xuống dòng",
"send": "Gửi tin nhắn"
},
"scrollToBottom": "Cuộn xuống cuối"
},
"message": {
"streaming": "đang truyền",
"assistantTyping": "Trợ lý đang nhập",
"toolSingle": "Đang dùng một công cụ",
"toolMany": "Đã dùng {{count}} công cụ"
},
"code": {
"fallbackLanguage": "mã",
"copyAria": "Sao chép mã",
"copy": "Sao chép",
"copied": "Đã sao chép"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "正在连接 nanobot…",
"boot": "正在加载 nanobot…"
},
"error": {
"title": "无法连接到 nanobot",
"gatewayHint": "请确认 gateway 已启动(`nanobot web`),并且当前页面与 gateway 运行在同一台机器上。"
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "nanobot Web UI —— 与你的 nanobot 工作区对话。"
}
},
"sidebar": {
"collapse": "收起侧边栏",
"toggleTheme": "切换主题",
"newChat": "新建对话",
"recent": "最近对话",
"refreshSessions": "刷新会话",
"language": {
"label": "语言",
"ariaLabel": "切换语言"
}
},
"chat": {
"fallbackTitle": "对话 {{id}}",
"loading": "加载中…",
"noSessions": "还没有会话。",
"actions": "“{{title}}” 的会话操作",
"delete": "删除",
"newChat": "新建对话"
},
"deleteConfirm": {
"title": "删除“{{title}}”?",
"description": "这个会话文件会从磁盘中删除,且无法撤销。",
"cancel": "取消",
"confirm": "删除"
},
"connection": {
"idle": "空闲",
"connecting": "连接中…",
"open": "已连接",
"reconnecting": "重连中…",
"closed": "已断开",
"error": "连接出错"
},
"thread": {
"loadingConversation": "正在加载对话…",
"empty": {
"description": "可以提问、继续本地工作,或者开启一个新线程。"
},
"header": {
"toggleSidebar": "切换侧边栏"
},
"composer": {
"placeholderThread": "输入消息…",
"placeholderHero": "你在想什么?",
"placeholderOpening": "正在打开新对话…",
"inputAria": "消息输入框",
"sendHint": "Enter 发送 · Shift+Enter 换行",
"send": "发送消息"
},
"scrollToBottom": "滚动到底部"
},
"message": {
"streaming": "流式输出中",
"assistantTyping": "助手正在输入",
"toolSingle": "正在使用工具",
"toolMany": "已使用 {{count}} 个工具"
},
"code": {
"fallbackLanguage": "代码",
"copyAria": "复制代码",
"copy": "复制",
"copied": "已复制"
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"app": {
"brand": "nanobot",
"loading": {
"connecting": "正在連線到 nanobot…",
"boot": "正在載入 nanobot…"
},
"error": {
"title": "無法連線到 nanobot",
"gatewayHint": "請確認 gateway 已啟動(`nanobot web`),並且目前頁面與 gateway 在同一台機器上開啟。"
},
"documentTitle": {
"base": "nanobot",
"chat": "{{title}} · nanobot"
},
"meta": {
"description": "nanobot Web UI —— 與你的 nanobot 工作區對話。"
}
},
"sidebar": {
"collapse": "收合側邊欄",
"toggleTheme": "切換主題",
"newChat": "新增對話",
"recent": "最近對話",
"refreshSessions": "重新整理會話",
"language": {
"label": "語言",
"ariaLabel": "切換語言"
}
},
"chat": {
"fallbackTitle": "對話 {{id}}",
"loading": "載入中…",
"noSessions": "目前還沒有會話。",
"actions": "「{{title}}」的會話操作",
"delete": "刪除",
"newChat": "新增對話"
},
"deleteConfirm": {
"title": "刪除「{{title}}」?",
"description": "這個會話檔案會從磁碟中移除,而且無法復原。",
"cancel": "取消",
"confirm": "刪除"
},
"connection": {
"idle": "閒置",
"connecting": "連線中…",
"open": "已連線",
"reconnecting": "重新連線中…",
"closed": "已中斷",
"error": "連線錯誤"
},
"thread": {
"loadingConversation": "正在載入對話…",
"empty": {
"description": "你可以提問、延續本地工作,或是開始新的執行緒。"
},
"header": {
"toggleSidebar": "切換側邊欄"
},
"composer": {
"placeholderThread": "輸入訊息…",
"placeholderHero": "你在想什麼?",
"placeholderOpening": "正在開啟新對話…",
"inputAria": "訊息輸入框",
"sendHint": "Enter 送出 · Shift+Enter 換行",
"send": "送出訊息"
},
"scrollToBottom": "捲動到底部"
},
"message": {
"streaming": "串流輸出中",
"assistantTyping": "助理正在輸入",
"toolSingle": "正在使用工具",
"toolMany": "已使用 {{count}} 個工具"
},
"code": {
"fallbackLanguage": "程式碼",
"copyAria": "複製程式碼",
"copy": "複製",
"copied": "已複製"
}
}