Wire root layout provider stack and locale-scoped layout with i18n

Root layout wraps children with LoggerProvider, ApiClientProvider, and
ErrorBoundary. Locale layout validates lang param against 9 supported
languages and provides a request-scoped I18nProvider.
This commit is contained in:
2026-04-15 00:34:16 +03:00
parent fc57556010
commit 858b8e1d1f
4 changed files with 601 additions and 43 deletions
+1
View File
@@ -22,6 +22,7 @@
},
"dependencies": {
"@modern-js/app-tools": "2.70.8",
"@modern-js/runtime": "^3.1.3",
"@module-federation/enhanced": "2.3.2",
"@module-federation/modern-js": "2.3.2",
"@opentelemetry/api": "^1.9.1",
+524 -42
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
import { useState, useEffect, type ReactNode } from "react";
import { useParams, useNavigate } from "@modern-js/runtime/router";
import { isLanguage, type Language } from "@/i18n/resolver";
import { createI18nInstance } from "@/i18n/config";
import { I18nProvider } from "@/i18n/provider";
import type i18next from "i18next";
/**
* Locale-scoped layout. Validates the `lang` URL segment against the
* 9 supported languages, creates a request-scoped i18n instance, and
* wraps children with `<I18nProvider>`.
*
* Invalid `lang` values redirect to `/ru/` (default locale).
*/
export default function LangLayout({ children }: { children: ReactNode }): JSX.Element | null {
const params = useParams<{ lang: string }>();
const navigate = useNavigate();
const lang = params.lang ?? "";
const [i18n, setI18n] = useState<typeof i18next | null>(null);
const [validLang, setValidLang] = useState<Language | null>(null);
useEffect(() => {
if (!isLanguage(lang)) {
void navigate("/ru/", { replace: true });
return;
}
setValidLang(lang);
let cancelled = false;
void createI18nInstance({ locale: lang }).then((instance) => {
if (!cancelled) {
setI18n(instance);
}
});
return () => {
cancelled = true;
};
}, [lang, navigate]);
if (!validLang || !i18n) {
return null;
}
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
}
+28 -1
View File
@@ -1,5 +1,32 @@
import type { ReactNode } from "react";
import { ErrorBoundary } from "@/ui/errors/ErrorBoundary";
import { LoggerProvider } from "@/observability/logger/provider";
import { createRootLogger } from "@/observability/logger/root";
import { ApiClientProvider } from "@/shared/api/provider";
import { ApiClient } from "@/shared/api/client";
const logger = createRootLogger();
const apiClient = new ApiClient({
baseUrl: process.env["API_BASE_URL"] ?? "/api",
locale: "ru",
logger,
});
/**
* Root layout — wraps the entire app with global providers.
* Provider order (outermost to innermost):
* LoggerProvider > ApiClientProvider > ErrorBoundary > children
*
* The locale-specific I18nProvider lives in src/routes/[lang]/layout.tsx
* so that each language segment gets its own i18n instance.
*/
export default function RootLayout({ children }: { children: ReactNode }): JSX.Element {
return <>{children}</>;
return (
<LoggerProvider logger={logger}>
<ApiClientProvider client={apiClient}>
<ErrorBoundary>{children}</ErrorBoundary>
</ApiClientProvider>
</LoggerProvider>
);
}