diff --git a/src/i18n/__tests__/index.test.tsx b/src/i18n/__tests__/index.test.tsx new file mode 100644 index 00000000..270937d3 --- /dev/null +++ b/src/i18n/__tests__/index.test.tsx @@ -0,0 +1,50 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; +import { I18nProvider, useI18n } from "@/i18n"; + +function LanguageProbe() { + const { language, t } = useI18n(); + return ( +
+ {language} + {t("language.label")} +
+ ); +} + +describe("I18nProvider", () => { + beforeEach(() => { + window.localStorage.clear(); + document.documentElement.lang = ""; + }); + + it("defaults first-time visitors to Simplified Chinese", async () => { + render( + + + + ); + + expect(screen.getByTestId("language")).toHaveTextContent("zh-CN"); + expect(screen.getByTestId("label")).toHaveTextContent("语言"); + await waitFor(() => { + expect(window.localStorage.getItem("popiart-node-language")).toBe("zh-CN"); + expect(document.documentElement.lang).toBe("zh-CN"); + }); + }); + + it("keeps an existing saved language preference", async () => { + window.localStorage.setItem("popiart-node-language", "ja"); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByTestId("language")).toHaveTextContent("ja"); + expect(screen.getByTestId("label")).toHaveTextContent("言語"); + }); + }); +}); diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index a4b09180..7328e52e 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -8,6 +8,7 @@ type Dictionary = typeof en; export type TranslationKey = keyof Dictionary; const STORAGE_KEY = "popiart-node-language"; +const DEFAULT_LANGUAGE: Language = "zh-CN"; export const LANGUAGE_OPTIONS: { value: Language; label: string }[] = [ { value: "en", label: "English" }, @@ -2442,15 +2443,6 @@ const dictionaries: Record = { ja, }; -function detectLanguage(): Language { - if (typeof navigator === "undefined") return "en"; - const language = navigator.language.toLowerCase(); - if (language.startsWith("ja")) return "ja"; - if (language.includes("tw") || language.includes("hk") || language.includes("hant")) return "zh-TW"; - if (language.startsWith("zh")) return "zh-CN"; - return "en"; -} - function interpolate(text: string, values?: Record) { if (!values) return text; return text.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? `{${key}}`)); @@ -2465,11 +2457,11 @@ interface I18nContextValue { const I18nContext = createContext(null); export function I18nProvider({ children }: { children: React.ReactNode }) { - const [language, setLanguageState] = useState("en"); + const [language, setLanguageState] = useState(DEFAULT_LANGUAGE); useEffect(() => { const stored = window.localStorage.getItem(STORAGE_KEY) as Language | null; - setLanguageState(stored && stored in dictionaries ? stored : detectLanguage()); + setLanguageState(stored && stored in dictionaries ? stored : DEFAULT_LANGUAGE); }, []); useEffect(() => { diff --git a/src/utils/__tests__/loginRequiredModal.test.ts b/src/utils/__tests__/loginRequiredModal.test.ts new file mode 100644 index 00000000..6ca13232 --- /dev/null +++ b/src/utils/__tests__/loginRequiredModal.test.ts @@ -0,0 +1,26 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("openLoginRequiredModal", () => { + beforeEach(() => { + vi.resetModules(); + }); + + it("uses the Ant Design 6 mask.closable option instead of deprecated maskClosable", async () => { + const confirm = vi.fn(() => ({ + destroy: vi.fn(), + update: vi.fn(), + })); + + vi.doMock("antd", () => ({ + Modal: { confirm }, + })); + + const { openLoginRequiredModal } = await import("@/utils/loginRequiredModal"); + openLoginRequiredModal(); + + expect(confirm).toHaveBeenCalledWith(expect.objectContaining({ + mask: { closable: false }, + })); + expect(confirm.mock.calls[0][0]).not.toHaveProperty("maskClosable"); + }); +}); diff --git a/src/utils/loginRequiredModal.ts b/src/utils/loginRequiredModal.ts index 40ceb3e6..634044f1 100644 --- a/src/utils/loginRequiredModal.ts +++ b/src/utils/loginRequiredModal.ts @@ -29,7 +29,7 @@ export function openLoginRequiredModal() { width: 360, icon: null, keyboard: false, - maskClosable: false, + mask: { closable: false }, onOk: () => { window.location.assign(LOGIN_REDIRECT_URL); },