MCPcopy Index your code
hub / github.com/S0vers/next-app-i18n-starter

github.com/S0vers/next-app-i18n-starter @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
59 symbols 149 edges 31 files 0 documented · 0%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Next.js 16 i18n Starter

A production-ready boilerplate for multilingual Next.js applications. Built with Next.js 16, React 19, next-intl 4, and shadcn/ui — with locale-driven currency and date formatting, RTL support, cookie-based theme SSR, and full SEO.

Author: Sovers Tonmoy Pandey (S0vers) · GitHub · @s0ver5

Live demo: next-app-i18n-starter.vercel.app


Table of contents


Features

Area What's included
Framework Next.js 16 App Router, Server Components, Turbopack dev server
i18n next-intl 4 — ICU messages, useFormatter, locale-driven currency/timezone
Languages English, Arabic (RTL), Chinese, Spanish, Japanese
Formatting Currency, dates, compact numbers, relative time — all driven by locale
UI shadcn/ui components, Tailwind CSS 4, light/dark theme
RTL Automatic dir="rtl" for Arabic + OmitRTL utility for LTR islands
SEO metadataBase, hreflang, JSON-LD, dynamic sitemap/robots, OG image
DX TypeScript, typed translation keys via global.d.ts, ESLint flat config

Prerequisites

  • Bun 1.x (recommended) or Node.js 24+
  • Basic familiarity with Next.js App Router and React Server Components

Getting started

# Clone
git clone https://github.com/S0vers/next-app-i18n-starter.git
cd next-app-i18n-starter

# Install
bun install

# Optional: set production URL for local SEO preview
cp .env.example .env.local
# Edit .env.local → NEXT_PUBLIC_SITE_URL=https://your-domain.com

# Run
bun dev

Open http://localhost:3000. Use the language switcher in the header to see translations and regional formatting update.


Project structure

next-app-i18n-starter/
├── dictionary/                     # Translation JSON files
│   ├── en.json                     # English (TypeScript source of truth)
│   ├── ar.json                     # Arabic
│   ├── zh.json                     # Chinese
│   ├── es.json                     # Spanish
│   └── ja.json                     # Japanese
├── public/
│   ├── llms.txt                    # Machine-readable context for AI tools
│   ├── og-image.png                # Open Graph image (1200×630)
│   └── favicon.ico
├── src/
│   ├── app/
│   │   ├── [locale]/               # All pages are locale-scoped
│   │   │   ├── layout.tsx          # Metadata, theme SSR, providers
│   │   │   ├── page.tsx            # Home + JSON-LD structured data
│   │   │   ├── not-found.tsx       # Localized 404
│   │   │   └── [...rest]/          # Catch-all → not-found
│   │   ├── globals.css             # Tailwind + CSS variables
│   │   ├── robots.ts               # Dynamic robots.txt
│   │   └── sitemap.ts              # Sitemap with hreflang alternates
│   ├── components/
│   │   ├── pages/HomeIndex.tsx     # Landing page (hero + tabs)
│   │   ├── LocalizationTab.tsx     # Locale formatting demo
│   │   ├── LanguageSwitcher.tsx    # Locale dropdown
│   │   ├── ModeToggle.tsx          # Light/dark toggle
│   │   ├── OmmitRlt.tsx            # OmitRTL utility
│   │   ├── theme-provider.tsx      # Client theme context
│   │   └── ui/                     # shadcn/ui primitives
│   ├── i18n/
│   │   ├── request.ts              # getRequestConfig (core i18n setup)
│   │   ├── routing.ts              # Locales + URL prefix strategy
│   │   ├── regional.ts             # Per-locale currency & timezone
│   │   └── navigation.ts           # Localized Link, useRouter, getPathname
│   ├── lib/
│   │   ├── site.ts                 # Site URL, author, SEO constants
│   │   ├── theme.ts                # Theme cookie helpers
│   │   └── utils.ts                # cn() class merge helper
│   └── proxy.ts                    # next-intl proxy (Next.js 16)
├── .env.example
├── global.d.ts                     # Typed IntlMessages from en.json
├── next.config.ts
├── package.json
└── tsconfig.json

Internationalization

This template uses next-intl with the App Router pattern. All i18n configuration flows through three files:

  1. **src/i18n/routing.ts** — which locales exist and how URLs are shaped
  2. **src/i18n/request.ts** — per-request config (messages, timezone, formats)
  3. **src/i18n/navigation.ts** — locale-aware navigation wrappers

How routing works

Configured in src/i18n/routing.ts:

export const routing = defineRouting({
  locales: ["en", "ar", "zh", "es", "ja"],
  defaultLocale: "en",
  localeDetection: true,
  localePrefix: "as-needed",
});

With localePrefix: "as-needed":

Locale URL Notes
en / Default locale has no prefix
ar /ar
zh /zh
es /es
ja /ja

src/proxy.ts runs on every request to detect locale from URL, cookie, or Accept-Language header.

Always use navigation from @/i18n/navigation, not next/link or next/navigation directly:

import { Link, useRouter, usePathname } from "@/i18n/navigation";

// Switch locale
const router = useRouter();
const pathname = usePathname();
router.replace(pathname, { locale: "ar" });

Locale-driven formatting

Currency, dates, and time zones are not user-configurable dropdowns — they follow the active locale. This is the recommended next-intl pattern for regional formatting.

**src/i18n/regional.ts** maps each locale to defaults:

Locale Currency Time zone
en USD America/New_York
ar SAR Asia/Riyadh
zh CNY Asia/Shanghai
es EUR Europe/Madrid
ja JPY Asia/Tokyo

**src/i18n/request.ts** applies them on every request:

export default getRequestConfig(async ({ requestLocale }) => {
  const locale = /* validated against routing.locales */;

  return {
    locale,
    timeZone: resolveTimeZone(locale),
    now: new Date(),
    formats: createRegionalFormats(resolveCurrency(locale)),
    messages: (await import(`../../dictionary/${locale}.json`)).default,
  };
});

In components, use next-intl hooks:

"use client";
import { useFormatter, useTimeZone, useNow } from "next-intl";

const format = useFormatter();
const now = useNow({ updateInterval: 30_000 });

format.number(29.99, "price");           // → "$29.99" (en) or "¥30" (ja)
format.dateTime(now, "long");              // → locale + timezone aware
format.relativeTime(twoHoursAgo, now);     // → "2 hours ago"

In translation messages, use ICU syntax:

{
  "priceMessage": "This product costs {price, number, currency}",
  "usersCount": "{count, number, compact} users"
}

The Localization tab on the home page demonstrates all of this. Switch language in the header — prices and dates update instantly.

Translation files

All strings live in dictionary/{locale}.json. Namespaces:

Namespace Used for
Index Landing page UI, tabs, installation steps
Footer Copyright, links
Metadata SEO title, description, keywords (generateMetadata)
Localization Formatting demo tab labels

TypeScript enforces key consistency via global.d.ts:

import en from "./dictionary/en.json";
type IntlMessages = typeof en;

When you add a key to en.json, TypeScript will error until you add it to all other locale files.

Server vs client components

Server Component (page or layout):

import { setRequestLocale, getTranslations } from "next-intl/server";

export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  setRequestLocale(locale); // Required for static rendering

  const t = await getTranslations({ locale, namespace: "Index" });
  return <h1>{t("title")}</h1>;
}

Client Component:

"use client";
import { useTranslations } from "next-intl";

export function MyComponent() {
  const t = useTranslations("Index");
  return 

{t("description")}

;
}

The root layout wraps children in NextIntlClientProvider with messages, timeZone, and now from the server.

Adding a new language

Example: adding French (fr)

  1. Create translation file bash cp dictionary/en.json dictionary/fr.json # Translate all values in fr.json
  2. Register locale in src/i18n/routing.ts: ts locales: ["en", "ar", "zh", "es", "ja", "fr"],
  3. Add regional defaults in src/i18n/regional.ts: ts fr: { currency: "EUR", timeZone: "Europe/Paris" },
  4. Add UI label in src/components/LanguageSwitcher.tsx: ts fr: "Français",
  5. Add OpenGraph locale in src/lib/site.ts: ts fr: "fr_FR",
  6. Build to verify types: bun run build

Adding a new page

// src/app/[locale]/about/page.tsx
import { setRequestLocale, getTranslations } from "next-intl/server";

export default async function AboutPage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  setRequestLocale(locale);
  const t = await getTranslations({ locale, namespace: "About" });

  return <h1>{t("title")}</h1>;
}

Add "About" namespace to every dictionary/*.json. Link to it with:

import { Link } from "@/i18n/navigation";
<Link href="https://github.com/S0vers/next-app-i18n-starter/raw/main/about">{t("aboutLink")}</Link>

OmitRTL

Arabic sets dir="rtl" on <html>. Some content (code, terminal commands, logos, formatted numbers) should stay left-to-right.

Wrap those elements with OmitRTL:

import OmitRTL from "@/components/OmmitRlt";

function CodeBlock({ code }: { code: string }) {
  return (
    <OmitRTL omitRTL>
      <pre><code>{code}</code></pre>
    </OmitRTL>
  );
}

The same utility is published as [react-omit-rtl](https://www.npmjs.com/package/react-omit-rtl) on npm.


Theme system

Light/dark mode without flash-of-unstyled-content and without <script> tags (React 19 compatible).

How it works:

  1. Server (layout.tsx) reads the theme cookie and sets className="light" or "dark" on <html> before paint.
  2. Client (theme-provider.tsx) syncs toggles to cookie + localStorage via useSyncExternalStore for system preference.
  3. Toggle (ModeToggle.tsx) switches between light and dark.

No blocking scripts. No next-themes dependency.


SEO

This template ships with a complete, locale-aware SEO setup using the Next.js 16 Metadata API, next-intl URL helpers, JSON-LD structured data, and dynamic sitemap/robots generation. Everything is designed to work correctly with localePrefix: "as-needed" routing.

SEO architecture overview

┌─────────────────────────────────────────────────────────────────┐
│  src/lib/site.ts                                                │
│  siteConfig.url, author, openGraphLocales                       │
└──────────────────────────┬──────────────────────────────────────┘
                           │
         ┌─────────────────┼─────────────────┐
         ▼                 ▼                 ▼
  layout.tsx          page.tsx          sitemap.ts / robots.ts
  generateMetadata    JSON-LD           crawl directives
  (head tags)         (structured data)
         │
         ▼
  dictionary/{locale}/Metadata  ←  translated title, description, keywords
         │
         ▼
  getPathname({ locale, href })  ←  correct URLs per locale (as-needed)

Files involved:

| File | SEO

Extension points exported contracts — how you extend this code

OmitRTLProps (Interface)
(no doc)
src/components/OmmitRlt.tsx

Core symbols most depended-on inside this repo

cn
called by 21
src/lib/utils.ts
getLocaleUrl
called by 2
src/lib/site.ts
getAlternateLanguages
called by 2
src/lib/site.ts
serializeJsonLd
called by 2
src/app/[locale]/page.tsx
readStoredTheme
called by 1
src/components/theme-provider.tsx
applyTheme
called by 1
src/components/theme-provider.tsx
useTheme
called by 1
src/components/theme-provider.tsx
createRegionalFormats
called by 1
src/i18n/regional.ts

Shape

Function 58
Interface 1

Languages

TypeScript100%

Modules by API surface

src/components/ui/dropdown-menu.tsx15 symbols
src/components/ui/card.tsx7 symbols
src/components/ui/tabs.tsx4 symbols
src/components/theme-provider.tsx4 symbols
src/components/pages/HomeIndex.tsx4 symbols
src/lib/theme.ts3 symbols
src/app/[locale]/layout.tsx3 symbols
src/lib/site.ts2 symbols
src/components/OmmitRlt.tsx2 symbols
src/components/LocalizationTab.tsx2 symbols
src/app/[locale]/page.tsx2 symbols
src/lib/utils.ts1 symbols

For agents

$ claude mcp add next-app-i18n-starter \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact