plan/react-rewrite #1
@@ -0,0 +1,41 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createI18nInstance } from "./config.js";
|
||||
import { serializeI18nForHydration, hydrateI18nFromWindow } from "./serializer.js";
|
||||
|
||||
describe("serializeI18nForHydration", () => {
|
||||
it("returns a JSON string containing locale and resources", async () => {
|
||||
const i18n = await createI18nInstance({ locale: "ru" });
|
||||
const json = serializeI18nForHydration(i18n);
|
||||
const parsed = JSON.parse(json);
|
||||
expect(parsed.locale).toBe("ru");
|
||||
expect(parsed.resources).toBeDefined();
|
||||
expect(parsed.resources.common).toBeDefined();
|
||||
expect(parsed.resources.common["BOARD"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hydrateI18nFromWindow", () => {
|
||||
afterEach(() => {
|
||||
// Clean up the global
|
||||
if (typeof globalThis !== "undefined") {
|
||||
delete (globalThis as Record<string, unknown>).__I18N__;
|
||||
}
|
||||
});
|
||||
|
||||
it("roundtrips: serialize → inject into globalThis.__I18N__ → hydrate", async () => {
|
||||
const original = await createI18nInstance({ locale: "ru" });
|
||||
const json = serializeI18nForHydration(original);
|
||||
const payload = JSON.parse(json);
|
||||
|
||||
// Simulate the SSR injection into the global scope
|
||||
(globalThis as Record<string, unknown>).__I18N__ = payload;
|
||||
|
||||
const hydrated = await hydrateI18nFromWindow();
|
||||
expect(hydrated.language).toBe("ru");
|
||||
expect(hydrated.t("BOARD.DEPARTURE")).toBe(original.t("BOARD.DEPARTURE"));
|
||||
});
|
||||
|
||||
it("throws if __I18N__ is not set", async () => {
|
||||
await expect(hydrateI18nFromWindow()).rejects.toThrow(/__I18N__/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type i18next from "i18next";
|
||||
import { createI18nInstance } from "./config.js";
|
||||
import { isLanguage } from "./resolver.js";
|
||||
|
||||
interface I18nPayload {
|
||||
locale: string;
|
||||
resources: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the loaded locale bundle from an i18next instance into a JSON
|
||||
* string suitable for injection into the SSR HTML under `window.__I18N__`.
|
||||
*/
|
||||
export function serializeI18nForHydration(i18n: typeof i18next): string {
|
||||
const locale = i18n.language;
|
||||
const resources: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
const namespaces = Array.isArray(i18n.options.ns)
|
||||
? i18n.options.ns
|
||||
: ([i18n.options.ns].filter(Boolean) as string[]);
|
||||
|
||||
for (const ns of namespaces) {
|
||||
const bundle = i18n.getResourceBundle(locale, ns) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (bundle) {
|
||||
resources[ns] = bundle;
|
||||
}
|
||||
}
|
||||
|
||||
const payload: I18nPayload = { locale, resources };
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehydrates an i18next instance from the SSR-injected `window.__I18N__` payload.
|
||||
* Called once on the client before React hydration begins.
|
||||
*/
|
||||
export async function hydrateI18nFromWindow(): Promise<typeof i18next> {
|
||||
const raw = (globalThis as Record<string, unknown>).__I18N__ as
|
||||
| I18nPayload
|
||||
| undefined;
|
||||
if (!raw) {
|
||||
throw new Error(
|
||||
"Cannot hydrate i18n: globalThis.__I18N__ is not set. " +
|
||||
"Ensure the SSR payload includes the serialized i18n data.",
|
||||
);
|
||||
}
|
||||
|
||||
const locale = raw.locale;
|
||||
if (!isLanguage(locale)) {
|
||||
throw new Error(
|
||||
`Cannot hydrate i18n: invalid locale "${locale}" in __I18N__ payload.`,
|
||||
);
|
||||
}
|
||||
|
||||
return createI18nInstance({
|
||||
locale,
|
||||
initialResources: raw.resources,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user