From eb999e069b03c60342bd343d73cb728011d5eace Mon Sep 17 00:00:00 2001 From: J_Sinclair <56301946+E-R-Butch@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:12:58 +0800 Subject: [PATCH] fix(ui): complete blog dropdown localization and address review comments --- .../src/components/LanguageSwitcher.tsx | 4 +- .../BlogDropdown.integration.test.tsx | 78 +++++++++++++++++++ .../Navbar/BlogDropdown/BlogDropdown.tsx | 10 +-- .../src/i18n/LanguageProvider.tsx | 4 +- ui/litellm-dashboard/src/i18n/locales/en.json | 1 + .../src/i18n/locales/zh-CN.json | 1 + 6 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx diff --git a/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx index a81a34fcb44..c95565e1869 100644 --- a/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx +++ b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx @@ -20,9 +20,7 @@ export default function LanguageSwitcher() { void i18n.changeLanguage(language); try { window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language); - } catch { - // Storage can be disabled by the browser; keep the current selection usable - } + } catch {} }; return ( diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx new file mode 100644 index 00000000000..fc69c1a59ea --- /dev/null +++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx @@ -0,0 +1,78 @@ +import { act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { I18nextProvider } from "react-i18next"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createDashboardI18n } from "@/i18n"; +import { renderWithProviders, screen } from "../../../../tests/test-utils"; +import { BlogDropdown } from "./BlogDropdown"; + +const mockRefetch = vi.fn(); +let mockIsError = false; + +vi.mock("@/app/(dashboard)/hooks/useDisableBlogPosts", () => ({ + useDisableBlogPosts: () => false, +})); + +vi.mock("@/app/(dashboard)/hooks/blogPosts/useBlogPosts", () => ({ + useBlogPosts: () => ({ + data: { posts: [{ title: "Post One", date: "2026-02-01", description: "Description one", url: "https://example.com/1" }] }, + isLoading: false, + isError: mockIsError, + refetch: mockRefetch, + }), +})); + +async function openDropdown() { + await userEvent.setup().hover(screen.getByRole("button", { name: "Blog" })); +} + +describe("blog dropdown language changes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockIsError = false; + }); + + it("should translate Retry after switching languages and still refetch posts", async () => { + mockIsError = true; + const user = userEvent.setup(); + const i18n = createDashboardI18n(); + renderWithProviders( + + + , + ); + + await openDropdown(); + expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument(); + await act(async () => { + await i18n.changeLanguage("zh-CN"); + }); + expect(screen.getByText("加载文章失败")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "重试" })); + expect(mockRefetch).toHaveBeenCalledTimes(1); + }); + + it("should update visible dates when switching between English and Chinese", async () => { + const i18n = createDashboardI18n(); + renderWithProviders( + + + , + ); + + await openDropdown(); + expect(await screen.findByText("Feb 1, 2026")).toBeInTheDocument(); + await act(async () => { + await i18n.changeLanguage("zh-CN"); + }); + expect(screen.getByText("2026年2月1日")).toBeInTheDocument(); + expect(screen.queryByText("Feb 1, 2026")).not.toBeInTheDocument(); + expect(screen.getByText("Post One")).toBeInTheDocument(); + await act(async () => { + await i18n.changeLanguage("en"); + }); + expect(screen.getByText("Feb 1, 2026")).toBeInTheDocument(); + expect(screen.queryByText("2026年2月1日")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx index aa9eeb93cba..19880e07b16 100644 --- a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx @@ -13,9 +13,9 @@ import { import { ChevronDown, LoaderCircle } from "lucide-react"; import React from "react"; -function formatDate(dateStr: string): string { +function formatDate(dateStr: string, language: string): string { const date = new Date(dateStr + "T00:00:00"); - return date.toLocaleDateString("en-US", { + return date.toLocaleDateString(language === "zh-CN" ? "zh-CN" : "en-US", { month: "short", day: "numeric", year: "numeric", @@ -23,7 +23,7 @@ function formatDate(dateStr: string): string { } export const BlogDropdown: React.FC = () => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const disableBlogPosts = useDisableBlogPosts(); const { data, isLoading, isError, refetch } = useBlogPosts(); @@ -46,7 +46,7 @@ export const BlogDropdown: React.FC = () => {
{t("Failed to load posts")}
); @@ -65,7 +65,7 @@ export const BlogDropdown: React.FC = () => { {post.title} - {formatDate(post.date)} + {formatDate(post.date, i18n.language)}

{post.description}

diff --git a/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx b/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx index de9efbb532c..c68a018b044 100644 --- a/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx +++ b/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx @@ -15,9 +15,7 @@ export default function LanguageProvider({ children }: { children: ReactNode }) try { const stored = window.localStorage.getItem(LANGUAGE_STORAGE_KEY); if (isLanguage(stored)) void instance.changeLanguage(stored); - } catch { - // Storage can be disabled by the browser; language selection still works in memory - } + } catch {} updateDocumentLanguage(instance.language); return () => { instance.off("languageChanged", updateDocumentLanguage); diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json index 4886c58c131..67ea7246186 100644 --- a/ui/litellm-dashboard/src/i18n/locales/en.json +++ b/ui/litellm-dashboard/src/i18n/locales/en.json @@ -73,6 +73,7 @@ "Prompts": "Prompts", "Read the docs": "Read the docs", "Response Cache": "Response Cache", + "Retry": "Retry", "Role": "Role", "Route every request to the cheapest model that can handle it, no prompt changes needed.": "Route every request to the cheapest model that can handle it, no prompt changes needed.", "Router Settings": "Router Settings", diff --git a/ui/litellm-dashboard/src/i18n/locales/zh-CN.json b/ui/litellm-dashboard/src/i18n/locales/zh-CN.json index bf2706ad15c..0bb1cf75bb7 100644 --- a/ui/litellm-dashboard/src/i18n/locales/zh-CN.json +++ b/ui/litellm-dashboard/src/i18n/locales/zh-CN.json @@ -73,6 +73,7 @@ "Prompts": "提示词", "Read the docs": "查看文档", "Response Cache": "响应缓存", + "Retry": "重试", "Role": "角色", "Route every request to the cheapest model that can handle it, no prompt changes needed.": "自动将每个请求分配给能够处理它的最低成本模型,无需修改提示词。", "Router Settings": "路由设置",