feat(webui): add i18n support and locale switcher
This commit is contained in:
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 d’exé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": "Qu’avez-vous en tête ?",
|
||||
"placeholderOpening": "Ouverture d’une 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": "L’assistant est en train d’écrire",
|
||||
"toolSingle": "Utilisation d’un outil",
|
||||
"toolMany": "{{count}} outils utilisés"
|
||||
},
|
||||
"code": {
|
||||
"fallbackLanguage": "code",
|
||||
"copyAria": "Copier le code",
|
||||
"copy": "Copier",
|
||||
"copied": "Copié"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "コピーしました"
|
||||
}
|
||||
}
|
||||
@@ -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": "복사됨"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "已复制"
|
||||
}
|
||||
}
|
||||
@@ -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": "已複製"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user