feat: add internationalization message platform

This commit is contained in:
donghyeon-ka
2026-07-26 16:17:39 +09:00
parent b8c0444217
commit 668bf05b48
57 changed files with 1711 additions and 201 deletions
+3 -1
View File
@@ -50,7 +50,9 @@ describe("async UI state matrix", () => {
render(<AsyncSurface state={state}>existing content</AsyncSurface>);
expect(screen.getByText("existing content")).toBeVisible();
expect(screen.getByRole("status")).toHaveTextContent("refreshing");
expect(screen.getByRole("status")).toHaveTextContent(
"최신 정보를 확인하고 있습니다.",
);
});
it("connects stale retry and conflict resolution to real callbacks", async () => {
@@ -60,7 +60,9 @@ describe("chunk recovery boundary classification", () => {
</FeatureBoundary>,
);
expect(screen.getByRole("alert")).toHaveTextContent("error.render_failure");
expect(screen.getByRole("alert")).toHaveTextContent(
"화면을 표시하지 못했습니다.",
);
expect(recover).not.toHaveBeenCalled();
});
});
@@ -189,7 +189,7 @@ describe("design-system platform interactions", () => {
await user.click(screen.getByRole("button", { name: "중복" }));
expect(screen.getByText("×2")).toBeVisible();
await user.click(screen.getByRole("button", { name: "다섯 개" }));
const region = screen.getByRole("region", { name: "Notifications" });
const region = screen.getByRole("region", { name: "알림" });
expect(within(region).getAllByRole("article")).toHaveLength(3);
expect(within(region).queryByText("알림 0")).not.toBeInTheDocument();
expect(within(region).getByText("알림 4")).toBeVisible();
+120
View File
@@ -0,0 +1,120 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it } from "vitest";
import {
Button,
Drawer,
Pagination,
Tabs,
} from "../../src/presentation/design-system/index.js";
import {
LocaleProvider,
useLocale,
type SupportedLocale,
} from "../../src/presentation/i18n/index.js";
function LocaleHarness() {
const { direction, locale, message, setLocale } = useLocale();
const [drawerOpen, setDrawerOpen] = useState(false);
return (
<>
<label htmlFor="test-locale">{message("shell.locale")}</label>
<select
id="test-locale"
onChange={(event) =>
setLocale(event.currentTarget.value as SupportedLocale)
}
value={locale}
>
<option value="ko-KR"></option>
<option value="en-US">English</option>
<option value="ar-EG">RTL</option>
</select>
<output>{`${locale}:${direction}`}</output>
<Button onClick={() => setDrawerOpen(true)}>
{message("shell.menu")}
</Button>
<Drawer
closeLabel={message("shell.closeMenu")}
onClose={() => setDrawerOpen(false)}
open={drawerOpen}
title={message("shell.menu")}
>
content
</Drawer>
</>
);
}
describe("locale provider runtime", () => {
it("switches copy and synchronizes the document language and direction", async () => {
const user = userEvent.setup();
render(
<LocaleProvider>
<LocaleHarness />
</LocaleProvider>,
);
expect(document.documentElement).toHaveAttribute("lang", "ko-KR");
await user.selectOptions(screen.getByLabelText("언어"), "en-US");
expect(screen.getByLabelText("Language")).toHaveValue("en-US");
expect(document.documentElement).toHaveAttribute("lang", "en-US");
expect(document.documentElement).toHaveAttribute("dir", "ltr");
await user.selectOptions(screen.getByLabelText("Language"), "ar-EG");
expect(document.documentElement).toHaveAttribute("lang", "ar-EG");
expect(document.documentElement).toHaveAttribute("dir", "rtl");
await user.click(screen.getByRole("button", { name: "Menu" }));
expect(screen.getByRole("dialog", { name: "Menu" })).toHaveAttribute(
"open",
);
});
it("reverses horizontal tab focus semantics in RTL", async () => {
const user = userEvent.setup();
render(
<LocaleProvider initialLocale="ar-EG">
<Tabs
activation="manual"
label="sections"
tabs={[
{ id: "one", label: "One", panel: "One panel" },
{ id: "two", label: "Two", panel: "Two panel" },
{ id: "three", label: "Three", panel: "Three panel" },
]}
/>
</LocaleProvider>,
);
const first = screen.getByRole("tab", { name: "One" });
first.focus();
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("tab", { name: "Three" })).toHaveFocus();
await user.keyboard("{ArrowLeft}");
expect(first).toHaveFocus();
});
it("keeps pagination semantics while direction-aware icons remain decorative", () => {
render(
<LocaleProvider initialLocale="ar-EG">
<Pagination
label="pages"
nextLabel="Next page"
onChange={() => {}}
page={2}
pageCount={3}
pageLabel={(page) => `Page ${page}`}
previousLabel="Previous page"
/>
</LocaleProvider>,
);
expect(
screen.getByRole("button", { name: "Previous page" }),
).toBeEnabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
});
+1 -1
View File
@@ -86,7 +86,7 @@ describe("page template slot contracts", () => {
/>,
);
expect(screen.getByRole("heading", { level: 1, name: "Offline" })).toBeVisible();
expect(screen.getByText("SAFE-123")).toBeVisible();
expect(screen.getByText(/SAFE-123/)).toBeVisible();
expect(document.body).not.toHaveTextContent("stack");
});
});
+4 -2
View File
@@ -28,7 +28,9 @@ describe("render recovery boundaries", () => {
</FeatureBoundary>,
);
expect(screen.getByRole("alert")).toHaveTextContent("error.render_failure");
expect(screen.getByRole("alert")).toHaveTextContent(
"화면을 표시하지 못했습니다.",
);
expect(onRenderFailure).toHaveBeenCalledWith({
routeId: "APP_HOME",
buildId: "build-a",
@@ -83,7 +85,7 @@ describe("render recovery boundaries", () => {
</FeatureBoundary>,
);
shouldThrow = false;
await user.click(screen.getByRole("button", { name: "retry" }));
await user.click(screen.getByRole("button", { name: "다시 시도" }));
view.rerender(
<FeatureBoundary routeId="APP_HOME" buildId="build-a">
<Recoverable />