Browse Source

Default first-run UI to Simplified Chinese

The app now starts I18nProvider in zh-CN when no saved language exists, while preserving explicit saved choices. The login-required Ant Design modal also uses the v6 mask.closable shape so the console no longer warns about maskClosable.

Constraint: First-run locale should not override an existing saved language preference
Constraint: Ant Design 6 deprecates maskClosable on Modal.confirm
Rejected: Keep browser-language detection | first entry needs deterministic Simplified Chinese
Confidence: high
Scope-risk: narrow
Directive: Keep provider-less useI18n fallback stable for isolated tests unless all callers are wrapped in I18nProvider
Tested: npm run test:run -- src/i18n/__tests__/index.test.tsx src/components/__tests__/TemplateExplorerView.test.tsx src/components/__tests__/Header.test.tsx src/utils/__tests__/loginRequiredModal.test.ts src/utils/__tests__/authFetch.test.ts src/store/__tests__/userStore.test.ts
Tested: npm run build
Not-tested: npm run lint | existing Next 16 next lint script resolves to nonexistent /lint directory
feature/gateway
jiajia 2 months ago
parent
commit
02abc69d1e
  1. 50
      src/i18n/__tests__/index.test.tsx
  2. 14
      src/i18n/index.tsx
  3. 26
      src/utils/__tests__/loginRequiredModal.test.ts
  4. 2
      src/utils/loginRequiredModal.ts

50
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 (
<div>
<span data-testid="language">{language}</span>
<span data-testid="label">{t("language.label")}</span>
</div>
);
}
describe("I18nProvider", () => {
beforeEach(() => {
window.localStorage.clear();
document.documentElement.lang = "";
});
it("defaults first-time visitors to Simplified Chinese", async () => {
render(
<I18nProvider>
<LanguageProbe />
</I18nProvider>
);
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(
<I18nProvider>
<LanguageProbe />
</I18nProvider>
);
await waitFor(() => {
expect(screen.getByTestId("language")).toHaveTextContent("ja");
expect(screen.getByTestId("label")).toHaveTextContent("言語");
});
});
});

14
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<Language, Dictionary> = {
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<string, string | number>) {
if (!values) return text;
return text.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? `{${key}}`));
@ -2465,11 +2457,11 @@ interface I18nContextValue {
const I18nContext = createContext<I18nContextValue | null>(null);
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [language, setLanguageState] = useState<Language>("en");
const [language, setLanguageState] = useState<Language>(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(() => {

26
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");
});
});

2
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);
},

Loading…
Cancel
Save