Minimal Next.js 16 + next-intl v4 multilingual starter with App Router. JA/EN ready out of the box.
Built from the same architecture powering 32blog.com (120+ articles, 3 languages).
- App Router — Server Components by default,
paramsasPromise(Next.js 15+) - next-intl v4 — Type-safe translations, locale routing, language switcher
- Static Generation — All locale pages pre-rendered at build time via
generateStaticParams - Tailwind CSS v4 — Utility-first styling with dark mode support
- Turbopack — Fast dev server (
next dev --turbopack) - SEO —
hreflangalternates auto-generated in metadata
git clone https://github.com/omitsu-dev/nextjs-i18n-starter.git
cd nextjs-i18n-starter
npm install
npm run devOpen http://localhost:3000. You'll be redirected to /en or /ja based on your browser language.
├── app/
│ ├── layout.tsx # Root layout
│ ├── globals.css # Tailwind CSS entry
│ └── [locale]/
│ ├── layout.tsx # Locale layout + NextIntlClientProvider
│ ├── page.tsx # Home page
│ └── about/
│ └── page.tsx # About page
├── components/
│ ├── Header.tsx # Navigation (Server Component)
│ ├── Footer.tsx # Footer (Server Component)
│ └── LanguageSwitcher.tsx # Language toggle (Client Component)
├── i18n/
│ ├── routing.ts # Locale definitions
│ ├── request.ts # Message loader
│ ├── navigation.ts # Locale-aware Link, useRouter, etc.
│ └── messages/
│ ├── en.json # English translations
│ └── ja.json # Japanese translations
├── proxy.ts # Locale detection & routing (Next.js 16)
└── next.config.ts # next-intl plugin
- Add the locale to
i18n/routing.ts:
export const routing = defineRouting({
locales: ["en", "ja", "es"], // Add "es"
defaultLocale: "en",
});-
Create
i18n/messages/es.jsonwith the same keys asen.json -
Update the type union in
i18n/request.tsandapp/[locale]/layout.tsx -
Add a label in
components/LanguageSwitcher.tsx:
const localeLabels: Record<string, string> = {
en: "EN",
ja: "JA",
es: "ES",
};- Create
app/[locale]/your-page/page.tsx:
import { getTranslations, setRequestLocale } from "next-intl/server";
type Props = { params: Promise<{ locale: string }> };
export default async function YourPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("yourPage");
return <h1>{t("heading")}</h1>;
}- Add translations to each
messages/*.json:
{
"yourPage": {
"heading": "Your Page Title"
}
}Use getTranslations from next-intl/server:
import { getTranslations } from "next-intl/server";
const t = await getTranslations("namespace");
return <p>{t("key")}</p>;Use useTranslations from next-intl:
"use client";
import { useTranslations } from "next-intl";
const t = useTranslations("namespace");
return <button>{t("key")}</button>;Use Link from @/i18n/navigation instead of next/link:
import { Link } from "@/i18n/navigation";
// Automatically prefixes with current locale
<Link href="/about">About</Link>| Package | Version |
|---|---|
| Next.js | 16.x |
| next-intl | 4.x |
| React | 19.x |
| Tailwind CSS | 4.x |
| TypeScript | 5.x |
- Next.js i18n Pitfalls with next-intl — Common mistakes and how to avoid them