Upgrade i18next v26 + react-i18next v17 with SSR-safe init
Split initI18n() into separate server/client initialization paths inspired by remix-i18next's architecture: - initI18nServer(lng): no LanguageDetector, per-request language from Accept-Language header - initI18nClient(): LanguageDetector reads <html lang> first to match server, then localStorage and navigator - detectLanguage(request): parses Accept-Language for best match Fixes rehydration errors caused by v26 removing initImmediate option (replaced with initAsync) and LanguageDetector running on the server where browser APIs don't exist. Also makes <html lang> dynamic instead of hardcoded "en". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4be716687a
commit
79f000fcc5
10 changed files with 166 additions and 40 deletions
|
|
@ -10,7 +10,7 @@
|
|||
"types": "./src/index.ts",
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"i18next": ">=23",
|
||||
"react-i18next": ">=14"
|
||||
"i18next": ">=26",
|
||||
"react-i18next": ">=17"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,26 +6,68 @@ import de from "./locales/de.ts";
|
|||
|
||||
export const defaultNS = "common";
|
||||
|
||||
export const supportedLngs = ["en", "de"] as const;
|
||||
export type SupportedLng = (typeof supportedLngs)[number];
|
||||
|
||||
export const resources = {
|
||||
en: { common: en.common, planner: en.planner, journal: en.journal },
|
||||
de: { common: de.common, planner: de.planner, journal: de.journal },
|
||||
} as const;
|
||||
|
||||
export function initI18n() {
|
||||
return i18n
|
||||
const commonOptions = {
|
||||
resources,
|
||||
defaultNS,
|
||||
fallbackLng: "en" as const,
|
||||
supportedLngs: [...supportedLngs],
|
||||
interpolation: { escapeValue: false },
|
||||
initAsync: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize i18next for server-side rendering.
|
||||
* No language detector — language is determined from the request.
|
||||
*/
|
||||
export function initI18nServer(lng: SupportedLng = "en") {
|
||||
if (i18n.isInitialized) {
|
||||
i18n.changeLanguage(lng);
|
||||
return;
|
||||
}
|
||||
i18n.use(initReactI18next).init({ ...commonOptions, lng });
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize i18next for the client.
|
||||
* Uses LanguageDetector with htmlTag priority so hydration matches the server.
|
||||
*/
|
||||
export function initI18nClient() {
|
||||
if (i18n.isInitialized) return;
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
defaultNS,
|
||||
fallbackLng: "en",
|
||||
supportedLngs: ["en", "de"],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
...commonOptions,
|
||||
detection: {
|
||||
order: ["htmlTag", "localStorage", "navigator"],
|
||||
caches: ["localStorage"],
|
||||
},
|
||||
showSupportNotice: false,
|
||||
initImmediate: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the best supported language from a request's Accept-Language header.
|
||||
*/
|
||||
export function detectLanguage(request: Request): SupportedLng {
|
||||
const header = request.headers.get("Accept-Language") ?? "";
|
||||
for (const part of header.split(",")) {
|
||||
const lang = part.split(";")[0]?.trim().toLowerCase();
|
||||
if (!lang) continue;
|
||||
for (const supported of supportedLngs) {
|
||||
if (lang === supported || lang.startsWith(supported + "-")) {
|
||||
return supported;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "en";
|
||||
}
|
||||
|
||||
export { i18n };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue