summaryrefslogtreecommitdiff
path: root/src/ollama_web/static
diff options
context:
space:
mode:
Diffstat (limited to 'src/ollama_web/static')
-rw-r--r--src/ollama_web/static/app.js49
-rw-r--r--src/ollama_web/static/i18n.js28
-rw-r--r--src/ollama_web/static/login.js2
-rw-r--r--src/ollama_web/static/mcp.js45
-rw-r--r--src/ollama_web/static/tools.js12
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) {