fix(ui): complete blog dropdown localization and address review comments

This commit is contained in:
J_Sinclair 2026-09-07 18:12:58 +08:00
parent 180d1650fb
commit eb999e069b
6 changed files with 87 additions and 11 deletions

View file

@ -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 (

View file

@ -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(
<I18nextProvider i18n={i18n}>
<BlogDropdown />
</I18nextProvider>,
);
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(
<I18nextProvider i18n={i18n}>
<BlogDropdown />
</I18nextProvider>,
);
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();
});
});

View file

@ -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 = () => {
<div className="flex items-center gap-2 px-2 py-1.5 text-sm">
<span className="text-destructive">{t("Failed to load posts")}</span>
<Button variant="outline" size="sm" onClick={() => refetch()}>
Retry
{t("Retry")}
</Button>
</div>
);
@ -65,7 +65,7 @@ export const BlogDropdown: React.FC = () => {
{post.title}
</h5>
<span className="text-muted-foreground" style={{ fontSize: 11 }}>
{formatDate(post.date)}
{formatDate(post.date, i18n.language)}
</span>
<p className="line-clamp-2">{post.description}</p>
</a>

View file

@ -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);

View file

@ -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",

View file

@ -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": "路由设置",