plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
2 changed files with 102 additions and 0 deletions
Showing only changes of commit a8c648c818 - Show all commits
+41
View File
@@ -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__/);
});
});
+61
View File
@@ -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,
});
}