From 29d8b861124f648e8aa9c1bac515381628c7daa6 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 07:55:30 +0900 Subject: [PATCH] feat(ui): i18n for api reference and models pages --- .../api-reference/APIReferenceView.tsx | 15 ++- .../api-reference/components/CodeBlock.tsx | 4 +- .../api-reference/components/DocLink.tsx | 8 +- .../ModelsAndEndpointsView.tsx | 65 ++++++---- .../components/AllModelsTab.tsx | 114 ++++++++++-------- ui/litellm-dashboard/src/locales/en.json | 70 +++++++++++ ui/litellm-dashboard/src/locales/zh-CN.json | 70 +++++++++++ 7 files changed, 260 insertions(+), 86 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx index 5861cc87e5b..8d433f2ecb2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx @@ -3,6 +3,7 @@ import React from "react"; import { Text, Tab, TabGroup, TabList, TabPanel, TabPanels, Grid } from "@tremor/react"; import CodeBlock from "./components/CodeBlock"; import DocLink from "@/app/(dashboard)/api-reference/components/DocLink"; +import { useTranslation } from "react-i18next"; interface ApiRefProps { proxySettings: { @@ -12,6 +13,7 @@ interface ApiRefProps { } const APIReferenceView: React.FC = ({ proxySettings }) => { + const { t } = useTranslation(); let base_url = ""; const customDocBaseUrl = proxySettings?.LITELLM_UI_API_DOC_BASE_URL; if (customDocBaseUrl && customDocBaseUrl.trim()) { @@ -27,21 +29,18 @@ const APIReferenceView: React.FC = ({ proxySettings }) => { {/* Header row with Docs link on the right */}

- OpenAI Compatible Proxy: API Reference + {t("pages.apiReference.title")}

- - LiteLLM is OpenAI Compatible. This means your API Key works with the OpenAI SDK. Just replace the base_url - to point to your litellm proxy. Example Below{" "} - + {t("pages.apiReference.description")} - OpenAI Python SDK - LlamaIndex - Langchain Py + {t("pages.apiReference.tabOpenAI")} + {t("pages.apiReference.tabLlamaIndex")} + {t("pages.apiReference.tabLangchain")} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx index 8d2fa8acb5d..4bf49b1ed18 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { CheckIcon, ClipboardIcon } from "lucide-react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { useTranslation } from "react-i18next"; interface CodeBlockProps { code: string; @@ -9,6 +10,7 @@ interface CodeBlockProps { } const CodeBlock = ({ code, language }: CodeBlockProps) => { + const { t } = useTranslation(); const [copied, setCopied] = useState(false); const copyToClipboard = () => { navigator.clipboard.writeText(code); @@ -21,7 +23,7 @@ const CodeBlock = ({ code, language }: CodeBlockProps) => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/DocLink.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/DocLink.tsx index a3d0416053e..725a154731b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/DocLink.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/DocLink.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ExternalLink } from "lucide-react"; +import { useTranslation } from "react-i18next"; function cn(...parts: Array) { return parts.filter(Boolean).join(" "); @@ -11,21 +12,22 @@ export type DocLinkProps = { }; const DocLink = ({ href, className }: DocLinkProps) => { + const { t } = useTranslation(); return ( - API Reference Docs + {t("pages.apiReference.docLinkText")} - (opens in a new tab) + {t("pages.apiReference.docLinkSrOnly")} ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 88f4382d7dd..34ae683d52a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -20,6 +20,7 @@ import type { UploadProps } from "antd"; import { Form } from "antd"; import { PlusCircleOutlined } from "@ant-design/icons"; import React, { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import AddModelTab from "../../../components/add_model/add_model_tab"; import HealthCheckComponent from "../../../components/model_dashboard/HealthCheckComponent"; import ModelGroupAliasSettings from "../../../components/model_group_alias_settings"; @@ -49,6 +50,7 @@ interface GlobalRetryPolicyObject { const HEALTH_PAGE_SIZE = 50; const ModelsAndEndpointsView: React.FC = ({ premiumUser, teams }) => { + const { t } = useTranslation(); const { accessToken, token, userRole, userId: userID } = useAuthorized(); const [addModelForm] = Form.useForm(); const [lastRefreshed, setLastRefreshed] = useState(""); @@ -178,9 +180,9 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te }, onChange(info) { if (info.file.status === "done") { - NotificationsManager.success(`${info.file.name} file uploaded successfully`); + NotificationsManager.success(t("pages.modelsAndEndpoints.fileUploadSuccess", { name: info.file.name })); } else if (info.file.status === "error") { - NotificationsManager.fromBackend(`${info.file.name} file upload failed.`); + NotificationsManager.fromBackend(t("pages.modelsAndEndpoints.fileUploadFailed", { name: info.file.name })); } }, }; @@ -207,17 +209,19 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te if (globalRetryPolicy) { payload.router_settings.retry_policy = globalRetryPolicy; } - NotificationsManager.success("Global retry settings saved successfully"); + NotificationsManager.success(t("pages.modelsAndEndpoints.globalRetrySaved")); } else { if (modelGroupRetryPolicy) { payload.router_settings.model_group_retry_policy = modelGroupRetryPolicy; } - NotificationsManager.success(`Retry settings saved successfully for ${selectedModelGroup}`); + NotificationsManager.success( + t("pages.modelsAndEndpoints.modelGroupRetrySaved", { modelGroup: selectedModelGroup }), + ); } await setCallbacksCall(accessToken, payload); } catch (error) { - NotificationsManager.fromBackend("Failed to save retry settings"); + NotificationsManager.fromBackend(t("pages.modelsAndEndpoints.failedToSaveRetry")); } }; @@ -266,7 +270,7 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te return `${field.name.join(".")}: ${field.errors.join(", ")}`; }) .join(" | ") || "Unknown validation error"; - NotificationsManager.fromBackend(`Please fill in the following required fields: ${errorMessages}`); + NotificationsManager.fromBackend(t("pages.modelsAndEndpoints.validationError", { fields: errorMessages })); } }; @@ -297,11 +301,11 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te {/* Model Management Header */}
-

Model Management

+

{t("pages.modelsAndEndpoints.modelManagement")}

{!all_admin_roles.includes(userRole) ? ( -

Add models for teams you are an admin for.

+

{t("pages.modelsAndEndpoints.addModelsForTeams")}

) : ( -

Add and manage models for the proxy

+

{t("pages.modelsAndEndpoints.addAndManageModels")}

)}
{!showMissingProviderBanner && ( @@ -312,7 +316,7 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-[#6366f1] hover:text-[#5558e3] border border-[#6366f1] hover:border-[#5558e3] rounded-lg transition-colors" > - Request Provider + {t("pages.modelsAndEndpoints.requestProvider")} )}
@@ -324,11 +328,10 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te
-

Missing a provider?

-

- The LiteLLM engineering team is constantly adding support for new LLM models, providers, endpoints. If - you don't see the one you need, let us know and we'll prioritize it. -

+

+ {t("pages.modelsAndEndpoints.missingProviderTitle")} +

+

{t("pages.modelsAndEndpoints.missingProviderDesc")}

= ({ premiumUser, te rel="noopener noreferrer" className="flex-shrink-0 inline-flex items-center gap-2 px-4 py-2 bg-[#6366f1] hover:bg-[#5558e3] text-white text-sm font-medium rounded-lg transition-colors" > - Request Provider + {t("pages.modelsAndEndpoints.requestProvider")} = ({ premiumUser, te localStorage.setItem("hideMissingProviderBanner", "true"); }} className="flex-shrink-0 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-colors" - aria-label="Dismiss banner" + aria-label={t("pages.modelsAndEndpoints.dismissBanner")} > = ({ premiumUser, te const isAdmin = all_admin_roles.includes(userRole); const visibleTabs: Array<{ tab: React.ReactElement; panel: React.ReactElement }> = [ { - tab: {isAdmin ? "All Models" : "Your Models"}, + tab: ( + + {isAdmin + ? t("pages.modelsAndEndpoints.tabAllModels") + : t("pages.modelsAndEndpoints.tabYourModels")} + + ), panel: ( = ({ premiumUser, te ]; if (!shouldHideAddModelTab) { visibleTabs.push({ - tab: Add Model, + tab: {t("pages.modelsAndEndpoints.tabAddModel")}, panel: ( = ({ premiumUser, te if (isAdmin) { visibleTabs.push( { - tab: LLM Credentials, + tab: {t("pages.modelsAndEndpoints.tabLLMCredentials")}, panel: ( @@ -449,7 +458,7 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te ), }, { - tab: Pass-Through Endpoints, + tab: {t("pages.modelsAndEndpoints.tabPassThrough")}, panel: ( = ({ premiumUser, te ), }, { - tab: Health Status, + tab: {t("pages.modelsAndEndpoints.tabHealthStatus")}, panel: ( = ({ premiumUser, te ), }, { - tab: Model Retry Settings, + tab: {t("pages.modelsAndEndpoints.tabModelRetrySettings")}, panel: ( = ({ premiumUser, te ), }, { - tab: Model Group Alias, + tab: {t("pages.modelsAndEndpoints.tabModelGroupAlias")}, panel: ( = ({ premiumUser, te ), }, { - tab: Price Data Reload, + tab: {t("pages.modelsAndEndpoints.tabPriceDataReload")}, panel: , }, ); @@ -527,7 +536,11 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te
{visibleTabs.map((t) => t.tab)}
- {lastRefreshed && Last Refreshed: {lastRefreshed}} + {lastRefreshed && ( + + {t("pages.modelsAndEndpoints.lastRefreshed", { time: lastRefreshed })} + + )} { + const { t } = useTranslation(); const { data: modelCostMapData, isLoading: isLoadingModelCostMap } = useModelCostMap(); const { accessToken, userId, userRole, premiumUser } = useAuthorized(); const { data: teams, isLoading: isLoadingTeams } = useTeams(); @@ -204,7 +206,7 @@ const AllModelsTab = ({ try { setDeleteLoading(true); await modelDeleteCall(accessToken, deleteModalModelId); - NotificationsManager.success("Model deleted successfully"); + NotificationsManager.success(t("pages.allModelsTab.modelDeletedSuccess")); queryClient.invalidateQueries({ queryKey: ["models", "list"] }); refetchModels(); } catch (error) { @@ -223,7 +225,9 @@ const AllModelsTab = ({ try { setPausingModelId(modelId); await modelPatchUpdateCall(accessToken, { blocked }, modelId); - NotificationsManager.success(blocked ? "Model paused" : "Model resumed"); + NotificationsManager.success( + blocked ? t("pages.allModelsTab.modelPaused") : t("pages.allModelsTab.modelResumed"), + ); // invalidateQueries already schedules a refetch for active observers // on this key — no need to also call refetchModels() (would double-fetch). queryClient.invalidateQueries({ queryKey: ["models", "list"] }); @@ -244,7 +248,7 @@ const AllModelsTab = ({
- Current Team: + {t("pages.allModelsTab.currentTeam")}
{isLoading ? ( @@ -277,7 +281,7 @@ const AllModelsTab = ({ label: ( - Personal + {t("pages.allModelsTab.personal")} ), }, @@ -300,7 +304,7 @@ const AllModelsTab = ({
- View: + {t("pages.allModelsTab.view")}
{isLoading ? ( @@ -317,7 +321,7 @@ const AllModelsTab = ({ label: ( - Current Team Models + {t("pages.allModelsTab.currentTeamModels")} ), }, @@ -326,7 +330,7 @@ const AllModelsTab = ({ label: ( - All Available Models + {t("pages.allModelsTab.allAvailableModels")} ), }, @@ -343,25 +347,35 @@ const AllModelsTab = ({
{currentTeam === "personal" ? ( - To access these models: Create a Virtual Key without selecting a team on the{" "} - - Virtual Keys page - + + ), + }} + /> ) : ( - To access these models: Create a Virtual Key and select Team as " - {typeof currentTeam !== "string" ? currentTeam.team_alias || currentTeam.team_id : ""}" on - the{" "} - - Virtual Keys page - + + ), + }} + /> )}
@@ -379,7 +393,7 @@ const AllModelsTab = ({
- Filters + {t("pages.allModelsTab.filters")} {/* Reset Filters Button */} @@ -429,7 +443,7 @@ const AllModelsTab = ({ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> - Reset Filters + {t("pages.allModelsTab.resetFilters")}
@@ -437,7 +451,7 @@ const AllModelsTab = ({
@@ -450,11 +464,11 @@ const AllModelsTab = ({ className="w-full" value={selectedModelGroup ?? "all"} onChange={(value) => setSelectedModelGroup(value === "all" ? "all" : value)} - placeholder="Filter by Public Model Name" + placeholder={t("pages.allModelsTab.filterByPublicModelName")} showSearch options={[ - { value: "all", label: "All Models" }, - { value: "wildcard", label: "Wildcard Models (*)" }, + { value: "all", label: t("pages.allModelsTab.allModels") }, + { value: "wildcard", label: t("pages.allModelsTab.wildcardModels") }, ...availableModelGroups.map((group, idx) => ({ value: group, label: group, @@ -469,10 +483,10 @@ const AllModelsTab = ({ className="w-full" value={selectedModelAccessGroupFilter ?? "all"} onChange={(value) => setSelectedModelAccessGroupFilter(value === "all" ? null : value)} - placeholder="Filter by Model Access Group" + placeholder={t("pages.allModelsTab.filterByModelAccessGroup")} showSearch options={[ - { value: "all", label: "All Model Access Groups" }, + { value: "all", label: t("pages.allModelsTab.allModelAccessGroups") }, ...availableModelAccessGroups.map((accessGroup, idx) => ({ value: accessGroup, label: accessGroup, @@ -490,8 +504,12 @@ const AllModelsTab = ({ ) : ( {paginationMeta.total_count > 0 - ? `Showing ${(currentPage - 1) * pageSize + 1} - ${Math.min(currentPage * pageSize, paginationMeta.total_count)} of ${paginationMeta.total_count} results` - : "Showing 0 results"} + ? t("pages.allModelsTab.showingResults", { + from: (currentPage - 1) * pageSize + 1, + to: Math.min(currentPage * pageSize, paginationMeta.total_count), + total: paginationMeta.total_count, + }) + : t("pages.allModelsTab.showingZeroResults")} )} @@ -510,7 +528,7 @@ const AllModelsTab = ({ currentPage === 1 ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "hover:bg-gray-50" }`} > - Previous + {t("common.previous")} )} @@ -530,7 +548,7 @@ const AllModelsTab = ({ : "hover:bg-gray-50" }`} > - Next + {t("common.next")} )}
@@ -569,28 +587,28 @@ const AllModelsTab = ({ Virtual Keys page", + "accessInfoTeam": "To access these models: Create a Virtual Key and select Team as \"{{teamName}}\" on the Virtual Keys page", + "searchPlaceholder": "Search model names...", + "filters": "Filters", + "resetFilters": "Reset Filters", + "filterByPublicModelName": "Filter by Public Model Name", + "allModels": "All Models", + "wildcardModels": "Wildcard Models (*)", + "filterByModelAccessGroup": "Filter by Model Access Group", + "allModelAccessGroups": "All Model Access Groups", + "showingResults": "Showing {{from}} - {{to}} of {{total}} results", + "showingZeroResults": "Showing 0 results", + "modelSettings": "Model Settings", + "deleteModelTitle": "Delete Model", + "deleteAlertMessage": "This action cannot be undone.", + "deleteMessage": "Are you sure you want to delete this model?", + "modelInformation": "Model Information", + "modelName": "Model Name", + "litellmModelName": "LiteLLM Model Name", + "provider": "Provider", + "createdBy": "Created By", + "notSet": "Not Set", + "modelDeletedSuccess": "Model deleted successfully", + "modelPaused": "Model paused", + "modelResumed": "Model resumed" + } } } diff --git a/ui/litellm-dashboard/src/locales/zh-CN.json b/ui/litellm-dashboard/src/locales/zh-CN.json index eb6997e32b3..a8554a71005 100644 --- a/ui/litellm-dashboard/src/locales/zh-CN.json +++ b/ui/litellm-dashboard/src/locales/zh-CN.json @@ -8512,5 +8512,75 @@ "less": "收起", "more": "展开" } + }, + "pages": { + "apiReference": { + "title": "OpenAI 兼容代理:API 参考", + "description": "LiteLLM 与 OpenAI 兼容,这意味着您的 API 密钥可与 OpenAI SDK 配合使用。只需将 base_url 替换为指向您的 litellm 代理即可。示例如下", + "tabOpenAI": "OpenAI Python SDK", + "tabLlamaIndex": "LlamaIndex", + "tabLangchain": "Langchain Py", + "copyCode": "复制代码", + "docLinkTitle": "在新标签页中打开文档", + "docLinkText": "API 参考文档", + "docLinkSrOnly": "(在新标签页中打开)" + }, + "modelsAndEndpoints": { + "modelManagement": "模型管理", + "addModelsForTeams": "为您担任管理员的团队添加模型。", + "addAndManageModels": "为代理添加和管理模型", + "requestProvider": "申请提供商", + "missingProviderTitle": "缺少某个提供商?", + "missingProviderDesc": "LiteLLM 工程团队持续添加对新 LLM 模型、提供商和端点的支持。如果您没有找到所需的提供商,请告知我们,我们将优先处理。", + "dismissBanner": "关闭横幅", + "tabAllModels": "全部模型", + "tabYourModels": "我的模型", + "tabAddModel": "添加模型", + "tabLLMCredentials": "LLM 凭证", + "tabPassThrough": "透传端点", + "tabHealthStatus": "健康检查", + "tabModelRetrySettings": "模型重试设置", + "tabModelGroupAlias": "模型组别名", + "tabPriceDataReload": "价格数据重载", + "lastRefreshed": "上次刷新:{{time}}", + "fileUploadSuccess": "{{name}} 文件上传成功", + "fileUploadFailed": "{{name}} 文件上传失败。", + "globalRetrySaved": "全局重试设置保存成功", + "modelGroupRetrySaved": "{{modelGroup}} 的重试设置保存成功", + "failedToSaveRetry": "保存重试设置失败", + "validationError": "请填写以下必填字段:{{fields}}" + }, + "allModelsTab": { + "currentTeam": "当前团队:", + "personal": "个人", + "view": "视图:", + "currentTeamModels": "当前团队模型", + "allAvailableModels": "全部可用模型", + "accessInfoPersonal": "要访问这些模型:在虚拟密钥页面创建一个虚拟密钥,不要选择团队", + "accessInfoTeam": "要访问这些模型:在虚拟密钥页面创建虚拟密钥并选择团队\"{{teamName}}\"", + "searchPlaceholder": "搜索模型名称...", + "filters": "筛选", + "resetFilters": "重置筛选", + "filterByPublicModelName": "按公开模型名称筛选", + "allModels": "全部模型", + "wildcardModels": "通配符模型 (*)", + "filterByModelAccessGroup": "按模型访问组筛选", + "allModelAccessGroups": "全部模型访问组", + "showingResults": "显示第 {{from}} - {{to}} 条,共 {{total}} 条", + "showingZeroResults": "共 0 条结果", + "modelSettings": "模型设置", + "deleteModelTitle": "删除模型", + "deleteAlertMessage": "此操作无法撤销。", + "deleteMessage": "确定要删除此模型吗?", + "modelInformation": "模型信息", + "modelName": "模型名称", + "litellmModelName": "LiteLLM 模型名称", + "provider": "提供商", + "createdBy": "创建者", + "notSet": "未设置", + "modelDeletedSuccess": "模型已成功删除", + "modelPaused": "模型已暂停", + "modelResumed": "模型已恢复" + } } }