diff options
Diffstat (limited to 'src/ollama_web/static')
| -rw-r--r-- | src/ollama_web/static/app.js | 49 | ||||
| -rw-r--r-- | src/ollama_web/static/i18n.js | 28 | ||||
| -rw-r--r-- | src/ollama_web/static/login.js | 2 | ||||
| -rw-r--r-- | src/ollama_web/static/mcp.js | 45 | ||||
| -rw-r--r-- | src/ollama_web/static/tools.js | 12 |
5 files changed, 86 insertions, 50 deletions
diff --git a/src/ollama_web/static/app.js b/src/ollama_web/static/app.js index 00269b3..492f9e4 100644 --- a/src/ollama_web/static/app.js +++ b/src/ollama_web/static/app.js @@ -170,7 +170,7 @@ function addMessage(role) { div.className = `message ${role}`; const roleEl = document.createElement("div"); roleEl.className = "role"; - roleEl.textContent = role === "user" ? "あなた" : "アシスタント"; + roleEl.textContent = role === "user" ? t("roles.user") : t("roles.assistant"); const body = document.createElement("div"); body.className = "body"; div.append(roleEl, body); @@ -190,7 +190,7 @@ function addToolBubble(parentExtras, name, args) { det.className = "tool"; const label = typeof window.toolLabel === "function" ? window.toolLabel(name) : name; const sum = document.createElement("summary"); - sum.textContent = `${label} を実行中…`; + sum.textContent = t("status.executing_tool", "Executing {label}…").replace("{label}", label); det.appendChild(sum); if (args && Object.keys(args).length) { const a = document.createElement("div"); @@ -210,7 +210,7 @@ function addThinkingBubble(parentExtras) { const det = document.createElement("details"); det.className = "thinking"; const sum = document.createElement("summary"); - sum.textContent = "思考プロセス"; + sum.textContent = t("status.thinking"); const body = document.createElement("div"); det.append(sum, body); parentExtras.appendChild(det); @@ -261,7 +261,7 @@ function renderSessionList(sessions) { if (!sessions.length) { const li = document.createElement("li"); li.className = "empty"; - li.textContent = "セッションがありません"; + li.textContent = t("chat.no_sessions"); sessionListEl.appendChild(li); return; } @@ -274,8 +274,8 @@ function renderSessionList(sessions) { title.textContent = s.title || s.id; const del = document.createElement("button"); del.className = "delete"; - del.textContent = "削除"; - del.title = "セッションを削除"; + del.textContent = t("common.delete"); + del.title = t("chat.delete_session"); del.addEventListener("click", (e) => { e.stopPropagation(); deleteSession(s.id); @@ -295,12 +295,12 @@ async function createSession() { await selectSession(session.id); } catch (err) { console.error("failed to create session", err); - alert("セッションの作成に失敗しました"); + alert(t("chat.create_failed")); } } async function deleteSession(id) { - if (!confirm("このセッションを削除しますか?")) return; + if (!confirm(t("chat.delete_confirm"))) return; try { const res = await csrfFetch(`/api/sessions/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error(res.statusText); @@ -314,7 +314,7 @@ async function deleteSession(id) { await loadSessions(); } catch (err) { console.error("failed to delete session", err); - alert("セッションの削除に失敗しました"); + alert(t("chat.delete_failed")); } } @@ -381,7 +381,7 @@ function renderSession() { return f ? f.name : fid; }) .join(", "); - info.textContent = `添付: ${names}`; + info.textContent = t("chat.attachment_label", "Attached: {names}").replace("{names}", names); body.appendChild(info); } } else if (m.role === "assistant") { @@ -437,7 +437,7 @@ async function uploadFiles(files) { await selectSession(currentSessionId); } catch (err) { console.error("failed to upload files", err); - alert("ファイルのアップロードに失敗しました"); + alert(t("chat.upload_failed")); } } @@ -489,7 +489,7 @@ async function send() { let displayContent = text; if (pendingFiles.length) { const names = pendingFiles.map((f) => f.name).join(", "); - displayContent += (text ? "\n\n" : "") + `添付ファイル: ${names}`; + displayContent += (text ? "\n\n" : "") + t("chat.attachment_files", "Attached files: {names}").replace("{names}", names); } userBody.innerHTML = renderMarkdown(displayContent); highlightIn(userBody); @@ -527,7 +527,9 @@ async function send() { }); } catch (err) { assistantBody.textContent = - err.name === "AbortError" ? "生成を停止しました" : `エラー: ${err.message}`; + err.name === "AbortError" + ? t("chat.stopped") + : t("chat.error_prefix", "Error: {message}").replace("{message}", err.message); abortController = null; cancelBtn.disabled = true; sendBtn.disabled = false; @@ -535,7 +537,7 @@ async function send() { } if (!res.ok || !res.body) { - assistantBody.textContent = `エラー: ${res.status}`; + assistantBody.textContent = t("chat.error_prefix", "Error: {message}").replace("{message}", res.status); abortController = null; cancelBtn.disabled = true; sendBtn.disabled = false; @@ -606,7 +608,10 @@ async function send() { } else if (ev.type === "tool_end") { const b = pendingTools.shift(); if (b) { - b.sum.textContent = b.sum.textContent.replace("実行中…", "完了"); + b.sum.textContent = b.sum.textContent.replace( + t("status.running", "Running…"), + t("status.done") + ); b.result.textContent = (ev.result || "").slice(0, 2000); chatEl.scrollTop = chatEl.scrollHeight; } @@ -621,7 +626,7 @@ async function send() { waiting.className = "waiting-msg"; waiting.style.color = "var(--muted)"; waiting.style.fontStyle = "italic"; - waiting.textContent = "ollama が回答を生成中…"; + waiting.textContent = t("chat.generating"); assistantBody.appendChild(waiting); } } else if (ev.type === "status") { @@ -634,7 +639,7 @@ async function send() { waiting.className = "waiting-msg"; waiting.style.color = "var(--muted)"; waiting.style.fontStyle = "italic"; - waiting.textContent = ev.message || "処理中…"; + waiting.textContent = ev.message || t("chat.processing"); assistantBody.appendChild(waiting); } } else if (ev.type === "thinking") { @@ -651,7 +656,7 @@ async function send() { } const span = document.createElement("span"); span.style.color = "#f85149"; - span.textContent = `エラー: ${ev.message}`; + span.textContent = t("chat.error_prefix", "Error: {message}").replace("{message}", ev.message); assistantBody.appendChild(span); } else if (ev.type === "done") { break; @@ -688,7 +693,7 @@ async function send() { } if (!assistantText && !pendingTools.length && !thinkingBody && !gotError) { - assistantBody.textContent = "(応答なし)"; + assistantBody.textContent = t("chat.no_response"); } abortController = null; @@ -704,7 +709,7 @@ async function send() { if (errorHtml && chatEl.lastElementChild) { const lastBody = chatEl.lastElementChild.querySelector(".body"); - if (lastBody && lastBody.textContent.trim() === "(応答なし)") { + if (lastBody && lastBody.textContent.trim() === t("chat.no_response")) { lastBody.innerHTML = errorHtml; } } @@ -736,7 +741,7 @@ async function clearMessages() { }); if (!res.ok) throw new Error(res.statusText); const data = await res.json(); - if (!data.cleared) throw new Error("クリアに失敗しました"); + if (!data.cleared) throw new Error(t("chat.clear_failed")); if (currentSession) { currentSession.messages = []; @@ -747,7 +752,7 @@ async function clearMessages() { renderAttachments(); } catch (err) { console.error("failed to clear messages", err); - alert("メッセージのクリアに失敗しました"); + alert(t("chat.clear_messages_failed")); } } diff --git a/src/ollama_web/static/i18n.js b/src/ollama_web/static/i18n.js new file mode 100644 index 0000000..985b452 --- /dev/null +++ b/src/ollama_web/static/i18n.js @@ -0,0 +1,28 @@ +/** Small i18n helper for the browser. + * + * The server injects ``window.I18N`` as a nested message object. This module + * exposes ``t(key, fallback)`` to resolve dot-separated keys safely. + */ +(function () { + "use strict"; + + function t(key, fallback) { + if (typeof key !== "string") { + return fallback !== undefined ? fallback : String(key); + } + const parts = key.split("."); + let current = window.I18N; + for (const part of parts) { + if (current == null || typeof current !== "object" || !(part in current)) { + return fallback !== undefined ? fallback : key; + } + current = current[part]; + } + if (typeof current === "string") { + return current; + } + return fallback !== undefined ? fallback : key; + } + + window.t = t; +})(); diff --git a/src/ollama_web/static/login.js b/src/ollama_web/static/login.js index 1f91868..4c9151b 100644 --- a/src/ollama_web/static/login.js +++ b/src/ollama_web/static/login.js @@ -14,7 +14,7 @@ form.addEventListener("submit", async (event) => { body: JSON.stringify({ pin: pinInput.value }), }); if (!res.ok) { - errorEl.textContent = "PINが違います"; + errorEl.textContent = t("login.pin_invalid", "Invalid PIN"); return; } const requestedNext = new URLSearchParams(window.location.search).get("next") || "/"; diff --git a/src/ollama_web/static/mcp.js b/src/ollama_web/static/mcp.js index ff3a27b..dc36b03 100644 --- a/src/ollama_web/static/mcp.js +++ b/src/ollama_web/static/mcp.js @@ -51,13 +51,13 @@ const header = document.createElement("div"); header.className = "mcp-server-header"; - const nameInput = createInput(name, "サーバー名", "text"); + const nameInput = createInput(name, t("mcp.server_name"), "text"); nameInput.className = "mcp-server-name"; const delBtn = document.createElement("button"); delBtn.type = "button"; delBtn.className = "mcp-server-delete"; - delBtn.textContent = "削除"; + delBtn.textContent = t("common.delete"); delBtn.addEventListener("click", () => card.remove()); header.append(nameInput, delBtn); @@ -65,8 +65,8 @@ const typeSelect = document.createElement("select"); typeSelect.className = "mcp-server-type"; typeSelect.innerHTML = ` - <option value="stdio">stdio</option> - <option value="http">HTTP Stream</option> + <option value="stdio">${t("mcp.stdio", "stdio")}</option> + <option value="http">${t("mcp.http_stream", "HTTP Stream")}</option> `; const isHttp = "url" in server; typeSelect.value = isHttp ? "http" : "stdio"; @@ -78,7 +78,7 @@ const wrap = document.createElement("label"); wrap.className = "mcp-field"; const span = document.createElement("span"); - span.textContent = `${label} ${required ? "(必須)" : "(任意)"}`; + span.textContent = `${label} ${required ? t("mcp.required") : t("mcp.optional")}`; wrap.append(span, control); return wrap; } @@ -87,11 +87,11 @@ fieldsEl.innerHTML = ""; const type = typeSelect.value; if (type === "stdio") { - const command = createInput(server.command || "", "例: npx", "text"); + const command = createInput(server.command || "", t("mcp.examples.command"), "text"); command.dataset.key = "command"; const args = createTextarea( (server.args || []).join("\n"), - "例: -y @modelcontextprotocol/server-filesystem /tmp", + t("mcp.examples.args"), 3 ); args.dataset.key = "args"; @@ -99,41 +99,41 @@ Object.entries(server.env || {}) .map(([k, v]) => `${k}=${v}`) .join("\n"), - "例: API_KEY=sk-xxx", + t("mcp.examples.env"), 3 ); env.dataset.key = "env"; - const cwd = createInput(server.cwd || "", "例: C:\\Users\\Home", "text"); + const cwd = createInput(server.cwd || "", t("mcp.examples.cwd"), "text"); cwd.dataset.key = "cwd"; fieldsEl.append( - wrapField("command", command, true), - wrapField("args", args, false), - wrapField("env", env, false), - wrapField("cwd", cwd, false) + wrapField(t("mcp.command"), command, true), + wrapField(t("mcp.args"), args, false), + wrapField(t("mcp.env"), env, false), + wrapField(t("mcp.cwd"), cwd, false) ); } else { - const url = createInput(server.url || "", "例: http://127.0.0.1:9000/mcp", "text"); + const url = createInput(server.url || "", t("mcp.examples.url"), "text"); url.dataset.key = "url"; const headers = createTextarea( Object.entries(server.headers || {}) .map(([k, v]) => `${k}=${v}`) .join("\n"), - "例: Authorization=Bearer xxx", + t("mcp.examples.headers"), 3 ); headers.dataset.key = "headers"; const timeout = createInput( server.timeout !== undefined ? String(server.timeout) : "", - "例: 30", + t("mcp.examples.timeout"), "number" ); timeout.dataset.key = "timeout"; fieldsEl.append( - wrapField("url", url, true), - wrapField("headers", headers, false), - wrapField("timeout", timeout, false) + wrapField(t("mcp.url"), url, true), + wrapField(t("mcp.headers"), headers, false), + wrapField(t("mcp.timeout"), timeout, false) ); } } @@ -247,15 +247,16 @@ }); if (!res.ok) { const data = await res.json().catch(() => ({})); - alert(`保存に失敗しました: ${(data.error || []).join(" / ") || res.statusText}`); + const errorText = (data.error || []).join(" / ") || res.statusText; + alert(t("mcp_messages.save_failed", "Failed to save: {error}").replace("{error}", errorText)); return; } currentConfig = await res.json(); - alert("MCP設定を保存しました"); + alert(t("mcp_messages.saved")); closeModal(); } catch (err) { console.error("failed to save MCP config", err); - alert("MCP設定の保存に失敗しました"); + alert(t("mcp_messages.save_error")); } }); diff --git a/src/ollama_web/static/tools.js b/src/ollama_web/static/tools.js index 59d5c02..ab392ca 100644 --- a/src/ollama_web/static/tools.js +++ b/src/ollama_web/static/tools.js @@ -1,15 +1,17 @@ "use strict"; window.TOOLS = [ - { name: "web_search", label: "Web検索" }, - { name: "scrape_url", label: "スクレイピング" }, - { name: "search_and_fetch", label: "ファイル検索・取得" }, - { name: "pdf_to_text", label: "PDFテキスト変換" }, + { name: "web_search", i18n_key: "tools.web_search" }, + { name: "scrape_url", i18n_key: "tools.scrape_url" }, + { name: "search_and_fetch", i18n_key: "tools.search_and_fetch" }, + { name: "pdf_to_text", i18n_key: "tools.pdf_to_text" }, ]; window.toolLabel = function toolLabel(name) { const builtin = window.TOOLS.find((t) => t.name === name); - if (builtin) return builtin.label; + if (builtin) { + return typeof window.t === "function" ? window.t(builtin.i18n_key, builtin.name) : builtin.name; + } const mcpMatch = /^mcp__([^_]+)__(.+)$/.exec(name); if (mcpMatch) { |
