Internationalization (i18n)
Explore the architectural patterns for building prefix-based, search-engine-friendly localized routing in Dinou.
In Dinou, internationalization is built on standard web primitives. You have the choice between Dynamic request-time routing or Static pre-compiled routing, using custom dictionaries or standard packages.
💡 Overview
Dinou provides complete flexibility for internationalization. Depending on whether you prefer dynamic request-time translations or pre-compiled static HTML pages, the configuration is structured into two main pathways:
src/about/page.tsx). Uses Express middleware to intercept prefixes and rewrite URLs internally.src/[lang]/about/page.tsx) matched natively by the file-system router.⚡ Part 1: Dynamic Localized Routing
This approach intercepts incoming language routes (like /es/about), extracts the locale, rewrites the URL to standard paths (/about) internally, and resolves the active translation dynamically at request-time.
Common Server Configuration
Eject the framework with npm run eject, then open dinou/core/server.js. Insert this prefix-aware middleware immediately after the cookie parser middleware:
// dinou/core/server.js
app.use(appUseCookieParser);
// 🌐 PREFIX-AWARE i18n ROUTING MIDDLEWARE
app.use((req, res, next) => {
const match = req.path.match(/^(/____rsc_payload(?:_old)?(?:_static)?____)?/(es|en)(/|$)/);
let locale = req.cookies?.locale || "en";
if (match) {
const prefix = match[1] || "";
locale = match[2];
if (req.cookies?.locale !== locale) {
res.cookie("locale", locale, { maxAge: 31536000000, httpOnly: true });
}
// Remove the language prefix internally to match standard routes
const remaining = req.url.substring(match[0].length - 1) || "/";
req.url = prefix + remaining;
}
req.locale = locale;
next();
});Expose the active req.locale inside the request context. In server.js, update getContext and getContextForServerFunctionEndpoint:
function getContext(req, res) {
return {
req: {
cookies: { ...req.cookies },
headers: { ... },
query: { ...req.query },
path: req.path,
method: req.method,
locale: req.locale, // 👈 Expose locale to request context
},
res: { ... }
};
}
function getContextForServerFunctionEndpoint(req, res) {
return {
req: {
cookies: { ...req.cookies },
headers: { ... },
query: { ...req.query },
path: req.path,
method: req.method,
locale: req.locale, // 👈 Expose locale to request context
},
res: { ... }
};
}Also, pass the locale to the dynamic HTML compilation subprocess in the Express wildcard GET handler (app.get(/^\/.*\/?$/)) inside contextForChild:
// In server.js wildcard route handler
const contextForChild = {
req: {
query: { ...req.query },
cookies: { ...req.cookies },
headers: { ... },
path: req.path,
method: req.method,
locale: req.locale, // 👈 Propagate locale here
},
};🟢 Option 1.A: Custom Lightweight Server Lookup
A simple server-side lookup system. Best when translations are only used to present static text content inside Server Components with zero package dependencies.
⚠️ Server-Only Constraint
useTranslation()). If a Client Component requires localized strings, they must be resolved on the server and explicitly passed down via props.1. Defining page_functions.ts
// src/about/page_functions.ts
import { getContext } from "dinou";
export function dynamic() {
return true; // Evaluate route dynamically at request-time (to access request cookies/locale)
}
const translations = {
en: { title: "Custom i18n", welcome: "Welcome!" },
es: { title: "i18n Personalizado", welcome: "¡Bienvenido!" },
};
export async function getProps() {
const ctx = getContext();
const locale = ctx?.req?.locale || "en";
const t = translations[locale] || translations.en;
return {
page: {
t,
currentLocale: locale,
},
};
}2. Rendering inside page.tsx
// src/about/page.tsx
import { Link } from "dinou";
export default function Page({ t, currentLocale }) {
return (
<div>
<h1>{t.title}</h1>
<p>{t.welcome}</p>
<div className="flex gap-4">
<Link href="/en/about">English</Link>
<Link href="/es/about">Español</Link>
</div>
</div>
);
}🔵 Option 1.B: Standard i18next & Client Hooks
Integrates the official i18next engine. Exposes translation hooks to Client Components so they can dynamically translate strings after hydration.
1. Installation
npm install i18next react-i18next2. Server Configuration (i18n.ts)
// src/i18n-real/i18n.ts
import i18next from "i18next";
import { getContext } from "dinou";
if (!i18next.isInitialized) {
i18next.init({
resources: {
en: {
translation: {
title: "Standard i18n Demo",
clientText: "This text is translated inside a CLIENT component using useTranslation!",
},
},
es: {
translation: {
title: "Demostración de i18n Estándar",
clientText: "¡Este texto se traduce dentro de un componente de CLIENTE usando useTranslation!",
},
},
},
fallbackLng: "en",
interpolation: { escapeValue: false },
});
}
export function getT() {
const ctx = getContext();
const locale = ctx?.req?.locale || "en";
return i18next.getFixedT(locale); // Safe for concurrent requests
}3. Page Functions (page_functions.ts)
// src/i18n-real/page_functions.ts
import { getContext } from "dinou";
import { getT } from "./i18n";
export function dynamic() {
return true; // Evaluate route dynamically at request-time (to access request cookies/locale)
}
export async function getProps() {
const ctx = getContext();
const locale = ctx?.req?.locale || "en";
const t = getT();
return {
page: {
title: t("title"),
currentLocale: locale,
},
};
}4. Client Context Provider (I18nProvider.tsx)
// src/i18n-real/I18nProvider.tsx
"use client";
import { ReactNode, useEffect } from "react";
import i18next from "i18next";
import { I18nextProvider } from "react-i18next";
const clientResources = {
en: {
translation: {
title: "Standard i18n Demo",
clientText: "This text is translated inside a CLIENT component using useTranslation!",
},
},
es: {
translation: {
title: "Demostración de i18n Estándar",
clientText: "¡Este texto se traduce dentro de un componente de CLIENTE usando useTranslation!",
},
},
};
const i18nInstance = i18next.createInstance();
export function I18nProvider({ locale, children }: { locale: string; children: ReactNode }) {
if (!i18nInstance.isInitialized) {
i18nInstance.init({
resources: clientResources,
lng: locale,
fallbackLng: "en",
interpolation: { escapeValue: false },
});
}
useEffect(() => {
if (i18nInstance.language !== locale) {
i18nInstance.changeLanguage(locale);
}
}, [locale]);
return <I18nextProvider i18n={i18nInstance}>{children}</I18nextProvider>;
}5. Consuming in Client Components (ClientComponent.tsx)
// src/i18n-real/ClientComponent.tsx
"use client";
import { useTranslation } from "react-i18next";
export default function ClientComponent() {
const { t } = useTranslation();
return <div>{t("clientText")}</div>;
}6. Page Integration (page.tsx)
// src/i18n-real/page.tsx
import { Link } from "dinou";
import { I18nProvider } from "./I18nProvider";
import ClientComponent from "./ClientComponent";
export default function Page({ title, currentLocale }) {
return (
<I18nProvider locale={currentLocale}>
<div>
<h1>{title}</h1>
<ClientComponent />
<div className="flex gap-4">
<Link href="/en/i18n-real">English</Link>
<Link href="/es/i18n-real">Español</Link>
</div>
</div>
</I18nProvider>
);
}🌐 Part 2: Static Localized Routing (SSG)
Instead of rewriting URLs, pages are nested inside a dynamic folder structure (e.g. src/[lang]/about/page.tsx) and pre-compiled during server startup into static HTML files via getStaticPaths().
Simplified Server Middleware
Since the file-system router naturally matches the route prefix folder structure, we do not need to rewrite request paths. The middleware only updates cookies and sets req.locale for dynamic hooks:
// dinou/core/server.js
app.use((req, res, next) => {
const match = req.path.match(/^(/____rsc_payload(?:_old)?(?:_static)?____)?/(es|en)(/|$)/);
let locale = req.cookies?.locale || "en";
if (match) {
locale = match[2];
if (req.cookies?.locale !== locale) {
res.cookie("locale", locale, { maxAge: 31536000000, httpOnly: true });
}
// 🚨 NOTE: No URL rewriting is needed here!
}
req.locale = locale;
next();
});💡 Context Propagation Best Practice
params.lang), you should still propagate the req.locale property inside server.js (in getContext, getContextForServerFunctionEndpoint, and contextForChild) as explained in Part 1: Common Server Configuration. This is required for two reasons: (1) so that Server Functions (actions) triggered from these static pages can detect the user's active locale at request-time, and (2) to ensure consistent behavior in development mode, where all pages are rendered dynamically on the fly.🟢 Option 2.A: Custom Dictionary SSG Lookup
Uses a custom translations dictionary within dynamic parameters. Fully compiled at build/startup time for maximum speed.
⚠️ Server-Only Constraint
useTranslation()). If a Client Component requires localized strings, they must be resolved on the server and explicitly passed down via props.1. Defining page_functions.ts
// src/[lang]/about/page_functions.ts
// 1. Tell Dinou which language paths to pre-compile as static HTML files at startup
export async function getStaticPaths() {
return [
{ lang: "en" },
{ lang: "es" },
];
}
const translations = {
en: { title: "Static i18n Page", welcome: "Statically compiled!" },
es: { title: "Página de i18n Estática", welcome: "¡Compilado estáticamente!" },
};
// 2. Read the lang parameter directly from the route parameters
export async function getProps(params) {
const lang = params.lang || "en";
const t = translations[lang] || translations.en;
return {
page: {
t,
currentLocale: lang,
},
};
}2. Rendering inside page.tsx
// src/[lang]/about/page.tsx
import { Link } from "dinou";
export default function Page({ t, currentLocale }) {
return (
<div>
<h1>{t.title}</h1>
<p>{t.welcome}</p>
<div className="flex gap-4">
<Link href="/en/about">English</Link>
<Link href="/es/about">Español</Link>
</div>
</div>
);
}🔵 Option 2.B: Standard i18next SSG Resolution
Utilizes the i18next package inside a static pre-compiled context by leveraging i18next.getFixedT(lang).
1. Defining page_functions.ts
// src/[lang]/about/page_functions.ts
import i18next from "i18next";
// Ensure i18next is initialized for the static compilation run
if (!i18next.isInitialized) {
i18next.init({
resources: {
en: {
translation: {
title: "Static Page (i18next)",
welcome: "This HTML page was pre-compiled statically via i18next!",
},
},
es: {
translation: {
title: "Página Estática (i18next)",
welcome: "¡Esta página se pre-compiló estáticamente con i18next!",
},
},
},
fallbackLng: "en",
interpolation: { escapeValue: false },
});
}
export async function getStaticPaths() {
return [
{ lang: "en" },
{ lang: "es" },
];
}
export async function getProps(params) {
const lang = params.lang || "en";
const t = i18next.getFixedT(lang); // Get translation function fixed to route parameter lang
return {
page: {
title: t("title"),
welcome: t("welcome"),
currentLocale: lang,
},
};
}2. Rendering inside page.tsx
In the page file, render the pre-translated props and wrap the tree in the client provider if client components also need translations:
// src/[lang]/about/page.tsx
import { Link } from "dinou";
import { I18nProvider } from "../../i18n-real/I18nProvider"; // Import client provider
import ClientComponent from "../../i18n-real/ClientComponent"; // Client component using hooks
export default function Page({ title, welcome, currentLocale }) {
return (
<I18nProvider locale={currentLocale}>
<div>
<h1>{title}</h1>
<p>{welcome}</p>
{/* Translates correctly on the client side using useTranslation hooks */}
<ClientComponent />
<div className="flex gap-4">
<Link href="/en/about">English</Link>
<Link href="/es/about">Español</Link>
</div>
</div>
</I18nProvider>
);
}