i18next + react-i18next: plurals, interpolation and SSR rendering: working versions (tested 2026-10-15)
Configure i18next with resources, plurals, context and escaping, and render react-i18next components to HTML on the server, with per-request instances.
passed3 passed · 0 failed2026-10-15 13:30 UTC- Status
- passed
- Last passed
- 2026-10-15 13:30 UTC
- Tests
- 3 / 3
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("i18next-react-ssr") (install), or see them on GitHub.
{
"name": "i18next-react-ssr",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"i18next": "26.4.5",
"react-i18next": "17.0.16",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"tsx": "4.23.15",
"@types/node": "24.19.2"
}
}
import i18next from "i18next";
export const resources = {
en: { translation: {
greeting: "Hello, {{name}}!",
inbox_one: "You have {{count}} message",
inbox_other: "You have {{count}} messages",
friend_male: "He is your friend",
friend_female: "She is your friend",
friend: "They are your friend",
} },
fr: { translation: {
greeting: "Bonjour, {{name}} !",
inbox_one: "Vous avez {{count}} message",
inbox_other: "Vous avez {{count}} messages",
} },
};
// One instance per request on the server so languages never leak between users.
export async function createI18n(lng: string) {
const i = i18next.createInstance();
await i.init({ lng, fallbackLng: "en", resources, interpolation: { escapeValue: true } });
return i;
}
import { createElement as h } from "react";
import { useTranslation } from "react-i18next";
export function Inbox({ name, count }: { name: string; count: number }) {
const { t } = useTranslation();
return h("div", null, h("h1", null, t("greeting", { name })), h("p", null, t("inbox", { count })));
}
import { test } from "node:test";
import assert from "node:assert/strict";
import { createElement as h } from "react";
import { renderToString } from "react-dom/server";
import { I18nextProvider } from "react-i18next";
import { createI18n } from "../src/i18n.ts";
import { Inbox } from "../src/Inbox.ts";
test("plurals, context and fallback", async () => {
const i = await createI18n("en");
assert.equal(i.t("inbox", { count: 1 }), "You have 1 message");
assert.equal(i.t("inbox", { count: 5 }), "You have 5 messages");
assert.equal(i.t("friend", { context: "female" }), "She is your friend");
assert.equal(i.t("friend", { context: "other" }), "They are your friend");
const fr = await createI18n("fr");
assert.equal(fr.t("friend", { context: "male" }), "He is your friend"); // falls back to en
});
test("interpolation escapes HTML", async () => {
const i = await createI18n("en");
assert.equal(i.t("greeting", { name: "<b>x</b>" }), "Hello, <b>x</b>!");
});
test("SSR render with per-request instances", async () => {
const [en, fr] = await Promise.all([createI18n("en"), createI18n("fr")]);
const render = (i: any) => renderToString(h(I18nextProvider, { i18n: i }, h(Inbox, { name: "Ada", count: 2 })));
assert.match(render(fr), /Bonjour, Ada !.*Vous avez 2 messages/);
assert.match(render(en), /Hello, Ada!.*You have 2 messages/);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Jotai atoms with React 19, tested with Testing Library and isolated stores
Jotai 3 primitive, derived, write-only and async atoms driving a React 19 todo UI with Suspense, tested in Vitest on happy-dom with @testing-library/react, user-event and a fresh createStore per test, plus store-only tests without React.
Next.js 16 App Router route handler + Zod validation
Validate JSON bodies and search params in a Next.js App Router route handler with Zod, returning typed 400s; tested by calling the handler with NextRequest.
Next.js 16 server actions + Zod with useActionState-shaped results
A 'use server' form action that validates FormData with Zod and returns field errors in the shape useActionState expects, tested by calling the action directly.
React Hook Form + Zod 4 resolver schemas (no DOM)
Share one Zod schema between React Hook Form (via @hookform/resolvers) and the server, with cross-field refinements, tested by calling the resolver directly.
All i18next recipesAll react-i18next recipesAll react recipes