From ae6818f83c6bd834364b87d9dc525b901855f7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B9=E9=9B=AA?= <2b@yorha.xyz> Date: Thu, 11 Jun 2026 04:03:17 +0900 Subject: [PATCH] feat(ui): i18n for chat page --- .../src/components/chat/ChatPage.tsx | 97 ++++++++++++------- ui/litellm-dashboard/src/locales/en.json | 38 ++++++++ ui/litellm-dashboard/src/locales/zh-CN.json | 38 ++++++++ 3 files changed, 138 insertions(+), 35 deletions(-) diff --git a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx index 0e729c8ae9d..3c0138286e9 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx @@ -1,6 +1,8 @@ "use client"; -import React, { useCallback, useEffect, useRef, useState, useLayoutEffect } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState, useLayoutEffect } from "react"; +import { useTranslation } from "react-i18next"; +import type { TFunction } from "i18next"; import { Tooltip, Skeleton, Popover } from "antd"; import MessageManager from "@/components/molecules/message_manager"; import { @@ -42,17 +44,23 @@ interface ChatPageProps { userEmail?: string; } -const SUGGESTIONS = ["Write", "Learn", "Code", "Brainstorm"]; const MAX_COMPARISON_MODELS = 3; const LOCALSTORAGE_MODEL_KEY = "litellm_chat_selected_models"; -function getGreeting(): string { +function getGreetingKey(): string { const h = new Date().getHours(); - if (h >= 5 && h < 12) return "Good morning"; - if (h >= 12 && h < 17) return "Good afternoon"; - return "Good evening"; + if (h >= 5 && h < 12) return "chat.chatPage.goodMorning"; + if (h >= 12 && h < 17) return "chat.chatPage.goodAfternoon"; + return "chat.chatPage.goodEvening"; } +export const getSuggestions = (t: TFunction) => [ + t("chat.chatPage.suggestionWrite"), + t("chat.chatPage.suggestionLearn"), + t("chat.chatPage.suggestionCode"), + t("chat.chatPage.suggestionBrainstorm"), +]; + // Build the chat UI URL respecting server root path (e.g. /api/v1/ui/chat) function getChatUrl(root: string, id?: string): string { return id ? `${root}/ui/chat?id=${id}` : `${root}/ui/chat`; @@ -129,6 +137,8 @@ async function streamToModel( } const ChatPage: React.FC = ({ accessToken, userRole, userId, userEmail }) => { + const { t } = useTranslation(); + const suggestions = useMemo(() => getSuggestions(t), [t]); const router = useRouter(); const searchParams = useSearchParams(); const activeConversationId = searchParams.get("id"); @@ -219,7 +229,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user localStorage.setItem(LOCALSTORAGE_MODEL_KEY, JSON.stringify([names[0]])); } }) - .catch(() => MessageManager.error("Could not load models")) + .catch(() => MessageManager.error(t("chat.chatPage.couldNotLoadModels"))) .finally(() => setIsLoadingModels(false)); }, [accessToken]); @@ -344,11 +354,11 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user } catch (err: unknown) { if (err instanceof Error && err.name === "AbortError") { updateLastAssistantMessage(convId!, { - content: accumulatedContent + " [stopped]", + content: accumulatedContent + " " + t("chat.chatPage.streamStopped"), }); } else { updateLastAssistantMessage(convId!, { - content: "[Something went wrong. The partial response has been saved.]", + content: t("chat.chatPage.streamError"), }); } } finally { @@ -530,7 +540,9 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user ? !activeConversation || activeConversation.messages.length === 0 : comparisonExchanges.length === 0; const displayName = userEmail?.split("@")[0] ?? userId ?? ""; - const greeting = displayName ? `${getGreeting()}, ${displayName}` : getGreeting(); + const greeting = displayName + ? t("chat.chatPage.greetingWithName", { name: displayName, greeting: t(getGreetingKey()) }) + : t(getGreetingKey()); const dashboardUrl = getDashboardUrl(uiRoot); // Filtered models: selected ones float to the top, then alphabetical @@ -552,7 +564,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user autoFocus value={modelSearchText} onChange={(e) => setModelSearchText(e.target.value)} - placeholder="Search models..." + placeholder={t("chat.chatPage.searchModelsPlaceholder")} style={{ width: "100%", padding: "6px 10px", @@ -566,7 +578,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user {selectedModels.length >= MAX_COMPARISON_MODELS && (
- Max {MAX_COMPARISON_MODELS} models selected — deselect one to change. + {t("chat.chatPage.maxModelsSelected", { count: MAX_COMPARISON_MODELS })}
)}
@@ -717,7 +729,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user }} > {selectedModels.length === 0 ? ( - Select model + {t("chat.chatPage.selectModel")} ) : selectedModels.length === 1 ? ( <> {(() => { @@ -798,7 +810,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user value={inputText} onChange={(e) => setInputText(e.target.value)} onKeyDown={handleKeyDown} - placeholder={inConversation ? "Send a message..." : "How can I help you today?"} + placeholder={inConversation ? t("chat.chatPage.sendMessagePlaceholder") : t("chat.chatPage.howCanIHelp")} style={{ width: "100%", minHeight: inConversation ? 52 : 80, @@ -870,9 +882,9 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user > {inConversation ? selectedMCPServers.length > 0 - ? `${selectedMCPServers.length} tool${selectedMCPServers.length > 1 ? "s" : ""} connected` + ? t("chat.chatPage.toolsConnected", { count: selectedMCPServers.length }) : "" - : selectedModels[0] || "No model"} + : selectedModels[0] || t("chat.chatPage.noModel")} )} {isAnyStreaming ? ( @@ -917,7 +929,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user transition: "background 0.15s", }} > - Send + {t("chat.chatPage.send")} )}
@@ -969,7 +981,10 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user LiteLLM )} - +

)} @@ -1230,7 +1255,7 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user {/* Suggestion chips — only in single-model mode */} {!isComparisonMode && (
- {SUGGESTIONS.map((s) => ( + {suggestions.map((s) => ( diff --git a/ui/litellm-dashboard/src/locales/en.json b/ui/litellm-dashboard/src/locales/en.json index 666aaa13b5f..285bfb3cc5b 100644 --- a/ui/litellm-dashboard/src/locales/en.json +++ b/ui/litellm-dashboard/src/locales/en.json @@ -6892,6 +6892,44 @@ "toolCall": "Tool call", "arguments": "Arguments", "result": "Result" + }, + "chatPage": { + "goodMorning": "Good morning", + "goodAfternoon": "Good afternoon", + "goodEvening": "Good evening", + "greetingWithName": "{{greeting}}, {{name}}", + "couldNotLoadModels": "Could not load models", + "streamStopped": "[stopped]", + "streamError": "[Something went wrong. The partial response has been saved.]", + "searchModelsPlaceholder": "Search models...", + "maxModelsSelected": "Max {{count}} models selected — deselect one to change.", + "selectModel": "Select model", + "toolsConnected_one": "{{count}} tool connected", + "toolsConnected_other": "{{count}} tools connected", + "noModel": "No model", + "send": "Send", + "sendMessagePlaceholder": "Send a message...", + "howCanIHelp": "How can I help you today?", + "expandSidebar": "Expand sidebar", + "collapseSidebar": "Collapse sidebar", + "newChat": "New chat", + "searchChats": "Search chats", + "chats": "Chats", + "apps": "Apps", + "credentials": "Credentials", + "backToConsole": "Back to Developer Console UI", + "storageWarning": "Chat history won't be saved in this browser session.", + "compareModels": "Compare {{count}} models", + "sendToCompare": "Send a message to see responses side-by-side", + "blankStateDesc": "Chat with 100+ LLMs + MCP tools — authenticate once, use them here.", + "openApps": "Open Apps →", + "suggestionWrite": "Write", + "suggestionLearn": "Learn", + "suggestionCode": "Code", + "suggestionBrainstorm": "Brainstorm", + "responseN": "Response {{n}}", + "generating": "Generating…", + "scrollToBottom": "Scroll to bottom" } } } diff --git a/ui/litellm-dashboard/src/locales/zh-CN.json b/ui/litellm-dashboard/src/locales/zh-CN.json index c320dcfd059..3f2d96ebebc 100644 --- a/ui/litellm-dashboard/src/locales/zh-CN.json +++ b/ui/litellm-dashboard/src/locales/zh-CN.json @@ -6892,6 +6892,44 @@ "toolCall": "工具调用", "arguments": "参数", "result": "结果" + }, + "chatPage": { + "goodMorning": "早上好", + "goodAfternoon": "下午好", + "goodEvening": "晚上好", + "greetingWithName": "{{greeting}},{{name}}", + "couldNotLoadModels": "无法加载模型", + "streamStopped": "[已停止]", + "streamError": "[出现问题,已保存部分响应。]", + "searchModelsPlaceholder": "搜索模型...", + "maxModelsSelected": "最多选择 {{count}} 个模型 — 请先取消选择一个。", + "selectModel": "选择模型", + "toolsConnected_one": "已连接 {{count}} 个工具", + "toolsConnected_other": "已连接 {{count}} 个工具", + "noModel": "无模型", + "send": "发送", + "sendMessagePlaceholder": "发送消息...", + "howCanIHelp": "今天有什么可以帮您?", + "expandSidebar": "展开侧边栏", + "collapseSidebar": "收起侧边栏", + "newChat": "新建对话", + "searchChats": "搜索对话", + "chats": "对话", + "apps": "应用", + "credentials": "凭证", + "backToConsole": "返回开发者控制台", + "storageWarning": "本次浏览器会话中不会保存对话历史。", + "compareModels": "对比 {{count}} 个模型", + "sendToCompare": "发送消息以并排查看各模型响应", + "blankStateDesc": "与 100+ 个大模型及 MCP 工具对话 — 一次认证,随时使用。", + "openApps": "打开应用 →", + "suggestionWrite": "写作", + "suggestionLearn": "学习", + "suggestionCode": "编程", + "suggestionBrainstorm": "头脑风暴", + "responseN": "响应 {{n}}", + "generating": "生成中…", + "scrollToBottom": "滚动到底部" } } }