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 11:54 UTC- Status
- passed
- Last passed
- 2026-10-15 11:54 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 38730348195
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
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.
React Router 8 data loaders and actions tested with createStaticHandler
Route loaders and actions with redirects, validation errors and 404 responses in React Router's data APIs, tested offline with createStaticHandler and plain Requests.
All i18next recipesAll react-i18next recipesAll react recipes