I18n
Internationalize your docs
Serve documentation in multiple languages.
Folder structure#
index.mdx
getting-started.mdx
Content loader#
Create locale-aware content:
tsx
import { defineContent, z } from "fromsrc";
const locales = ["en", "ja", "zh"] as const;
type Locale = (typeof locales)[number];
export function createLocalizedContent(locale: Locale) {
return defineContent({
dir: `docs/${locale}`,
schema: z.object({
title: z.string(),
description: z.string().optional(),
order: z.number().optional(),
}),
});
}
export const en = createLocalizedContent("en");
export const ja = createLocalizedContent("ja");
export const zh = createLocalizedContent("zh");Route structure#
Use Next.js dynamic segments:
page.tsx
Page component#
tsx
import { notFound } from "next/navigation";
import { en, ja, zh } from "@/lib/content";
import { MDX } from "@/components/mdx";
const content = { en, ja, zh };
interface Props {
params: Promise<{ locale: string; slug: string[] }>;
}
export async function generateStaticParams() {
const locales = ["en", "ja", "zh"];
const params = [];
for (const locale of locales) {
const docs = await content[locale].getAllDocs();
for (const doc of docs) {
params.push({ locale, slug: doc.slug });
}
}
return params;
}
export default async function DocPage({ params }: Props) {
const { locale, slug } = await params;
if (!content[locale]) notFound();
const doc = await content[locale].getDoc(slug);
if (!doc) notFound();
return (
<article>
<h1>{doc.title}</h1>
<MDX source={doc.content} />
</article>
);
}Locale switcher#
tsx
"use client";
import { usePathname, useRouter } from "next/navigation";
const locales = [
{ code: "en", name: "english" },
{ code: "ja", name: "japanese" },
{ code: "zh", name: "chinese" },
];
export function LocaleSwitcher() {
const pathname = usePathname();
const router = useRouter();
const current = locales.find((l) => pathname.startsWith(`/${l.code}`));
const switchLocale = (code: string) => {
const segments = pathname.split("/");
segments[1] = code;
router.push(segments.join("/"));
};
return (
<select
value={current?.code || "en"}
onChange={(e) => switchLocale(e.target.value)}
className="bg-surface border border-line rounded px-2 py-1 text-sm"
>
{locales.map((locale) => (
<option key={locale.code} value={locale.code}>
{locale.name}
</option>
))}
</select>
);
}Proxy configuration#
Redirect based on accept-language header:
tsx
import { NextRequest } from "next/server";
const locales = ["en", "ja", "zh"];
const defaultLocale = "en";
export async function proxy(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const hasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return;
const acceptLanguage = request.headers.get("accept-language") || "";
const preferred = acceptLanguage.split(",")[0].split("-")[0];
const locale = locales.includes(preferred) ? preferred : defaultLocale;
return Response.redirect(new URL(`/${locale}${pathname}`, request.url));
}Translations file#
Store UI strings separately:
tsx
const translations = {
en: {
search: "search",
toc: "on this page",
edit: "edit this page",
prev: "previous",
next: "next",
},
ja: {
search: "search",
toc: "contents",
edit: "edit",
prev: "back",
next: "next",
},
zh: {
search: "search",
toc: "contents",
edit: "edit",
prev: "back",
next: "next",
},
};
export function useTranslations(locale: string) {
return translations[locale] || translations.en;
}Localized metadata#
tsx
export async function generateMetadata({ params }: Props) {
const { locale, slug } = await params;
const doc = await content[locale].getDoc(slug);
return {
title: doc?.title,
description: doc?.description,
alternates: {
languages: Object.fromEntries(
locales.map((l) => [l, `/${l}/docs/${slug.join("/")}`])
),
},
};
}RTL support#
Handle right-to-left languages:
tsx
const rtlLocales = ["ar", "he", "fa"];
export function DocLayout({ locale, children }) {
const dir = rtlLocales.includes(locale) ? "rtl" : "ltr";
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}