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
+46 -26
View File
@@ -28,7 +28,8 @@ import {
RouteBoundary,
} from "../boundaries/render-error-boundary.jsx";
import { ChunkRecoveryBoundary } from "../boundaries/chunk-recovery-boundary.js";
import { PageHeader } from "../design-system/index.js";
import { Button, PageHeader } from "../design-system/index.js";
import { LocaleProvider, useLocale } from "../i18n/index.js";
import { AppShell } from "../layouts/app-shell.jsx";
import { useApplication } from "../providers/application-provider.js";
import { SessionProvider, useSession } from "../providers/session-provider.jsx";
@@ -54,6 +55,8 @@ export function useRouteInput(): ParsedRouteInput {
}
function RouteLoadingSurface({ definition }: { definition: RouteDefinition }) {
const { message, resolve } = useLocale();
const title = resolve(`route.${definition.routeId}.title`);
return (
<section
className="ui-page route-loading"
@@ -62,8 +65,10 @@ function RouteLoadingSurface({ definition }: { definition: RouteDefinition }) {
data-surface={definition.loadingSurface}
>
<div className="ui-skeleton" aria-hidden="true" />
<p> .</p>
<span className="visually-hidden">{definition.title} </span>
<p>{message("route.loading")}</p>
<span className="visually-hidden">
{message("route.loadingNamed", { title })}
</span>
</section>
);
}
@@ -73,35 +78,41 @@ function RouteFailureSurface({
}: {
definition?: RouteDefinition;
}) {
const { message } = useLocale();
return (
<section
className="ui-page"
data-surface={definition?.errorSurface ?? "route-boundary"}
>
<PageHeader
title="화면을 표시하지 못했습니다."
description="잠시 후 다시 시도해 주세요. 문제가 계속되면 운영 지원 참조 정보를 확인하세요."
title={message("route.failure.title")}
description={message("route.failure.description")}
/>
</section>
);
}
function InvalidRouteSurface({ code }: { code: string }) {
const { message } = useLocale();
return (
<section className="ui-page" data-surface="invalid-route">
<PageHeader
title="올바르지 않은 주소입니다."
description="주소의 경로 또는 검색 조건을 확인해 주세요."
title={message("route.invalid.title")}
description={message("route.invalid.description")}
/>
<p data-route-error={code}> .</p>
<p data-route-error={code}>{message("route.invalid.action")}</p>
</section>
);
}
function RouteLifecycle({ definition }: { definition: RouteDefinition }) {
const location = useLocation();
const { message, resolve } = useLocale();
useEffect(() => {
document.title = `${definition.title} · Frontend Skeleton`;
document.title = message("route.documentTitle", {
title: resolve(`route.${definition.routeId}.title`),
appName: message("common.appName"),
});
const main = document.getElementById("main-content");
main?.focus({ preventScroll: true });
try {
@@ -111,7 +122,7 @@ function RouteLifecycle({ definition }: { definition: RouteDefinition }) {
} catch {
// Non-browser test hosts may not implement scrolling.
}
}, [definition, location.key, location.pathname]);
}, [definition, location.key, location.pathname, message, resolve]);
return null;
}
@@ -146,6 +157,7 @@ function ProtectedRoute({
}) {
const location = useLocation();
const { sessionState, beginSignIn, recover } = useSession();
const { message } = useLocale();
const [pending, setPending] = useState(false);
const [failed, setFailed] = useState(false);
const decision = decideRouteAccess(routeId, sessionState);
@@ -173,8 +185,8 @@ function ProtectedRoute({
return (
<section className="ui-page" data-surface="auth-integration-required">
<PageHeader
title="로그인 연동이 필요합니다."
description="외부 인증 소유자가 연결되면 이 보호 라우트를 사용할 수 있습니다."
title={message("route.auth.integration.title")}
description={message("route.auth.integration.description")}
/>
</section>
);
@@ -183,24 +195,30 @@ function ProtectedRoute({
return (
<section className="ui-page" data-surface="authentication-required">
<PageHeader
title={recovering ? "세션을 복구하고 있습니다." : "세션이 필요합니다."}
title={
recovering
? message("route.auth.recovering.title")
: message("route.auth.required.title")
}
description={
recovering
? "기존 세션 확인을 계속하려면 복구를 실행하세요."
: "이 화면은 인증 연동 지점을 확인하기 위한 보호 라우트입니다."
? message("route.auth.recovering.description")
: message("route.auth.required.description")
}
/>
<button
className="ui-button"
type="button"
<Button
disabled={pending}
onClick={() => void continueSession()}
>
{pending ? "처리 중…" : recovering ? "세션 복구" : "로그인 시작"}
</button>
{pending
? message("common.processing")
: recovering
? message("action.recoverSession")
: message("action.signIn")}
</Button>
{failed ? (
<p className="ui-terminal-error" role="alert">
.
{message("shell.session.actionFailed")}
</p>
) : null}
</section>
@@ -300,10 +318,12 @@ export function AppRouter({
[basename, buildId],
);
return (
<ThemeProvider>
<SessionProvider>
<RouterProvider router={router} />
</SessionProvider>
</ThemeProvider>
<LocaleProvider>
<ThemeProvider>
<SessionProvider>
<RouterProvider router={router} />
</SessionProvider>
</ThemeProvider>
</LocaleProvider>
);
}