# Next.js i18n Starter > Next.js 16 App Router starter with next-intl 4, locale-driven currency and date formatting, shadcn/ui, RTL support, cookie-based theme rendering, and SEO metadata, structured data, and sitemap generation. Repository: https://github.com/s0vers/next-app-i18n-starter. Live demo: https://next-app-i18n-starter.vercel.app. This file is a public project overview. For repository changes, read the root `AGENTS.md` and the nearest scoped `AGENTS.md`; those files hold the shared instructions for coding assistants. ## Agent skills - [next-intl i18n skill](https://github.com/s0vers/next-app-i18n-starter/blob/main/.agents/skills/next-intl-i18n/SKILL.md): locale routing, request config, messages, formatting, RTL and CJK, localized content, and a message parity check script. - [Next.js SEO skills](https://github.com/s0vers/next-app-i18n-starter/tree/main/.agents/skills): seven focused skills (technical, international, structured data, commerce, content, measurement, AI search), plus a script in nextjs-seo-technical that verifies hreflang, canonicals, and sitemaps on a running site. ## Quick answers for common tasks ### How do I add a new language? 1. Copy `dictionary/en.json` → `dictionary/xx.json` and translate all namespaces. 2. Add the locale to `localeConfig` in `src/i18n/locales.ts` with its label, currency, time zone, font, and Open Graph locale tag. 3. Run `bun run build`. Routing, the switcher, and regional defaults derive from `localeConfig`. ### How does currency/timezone formatting work? Regional defaults come from the active locale. The demo formats values with that locale's currency and time zone. - `src/i18n/request.ts` → `getRequestConfig` sets `timeZone`, `now`, `formats` per locale. - `src/i18n/locales.ts` → `localeConfig` maps locale → currency + IANA time zone. - `src/i18n/regional.ts` → `createRegionalFormats` defines named number and date formats. - Client: `useFormatter()`, `useTimeZone()`, `useNow()` from `next-intl`. - Demo UI: `src/components/LocalizationTab.tsx`. Change the language in the header to see the formats update. ### How do I add a new page? 1. Create `src/app/[locale]/your-page/page.tsx` (Server Component). 2. Await `params`, validate the locale with `hasLocale`, and call `notFound()` otherwise. No `setRequestLocale` is needed because the locale comes from `next/root-params`. 3. Use `getTranslations({ locale, namespace: "YourNamespace" })` for server text. 4. Use `useTranslations("YourNamespace")` in client components. 5. Add keys to all `dictionary/*.json` files. 6. Link with `Link` from `@/i18n/navigation` (not `next/link` directly). ### How do I navigate between locales? Use the `@/i18n/navigation` wrappers. They respect `localePrefix: as-needed`: ```tsx import { Link, useRouter, usePathname } from "@/i18n/navigation"; router.replace(pathname, { locale: "ar" }); ``` ### How do I update SEO metadata? Edit the `Metadata` namespace in all five files: `dictionary/en.json`, `ar.json`, `zh.json`, `es.json`, and `ja.json` (`title`, `description`). The locale layout reads these translations and builds shared metadata through `createLocalizedMetadata` in `src/lib/site.ts`. Do not add `keywords`; search engines do not use the meta-keywords tag for ranking. ### How do I add SEO to a new page? 1. Add `metaTitle` / `metaDescription` keys to a new namespace in all dictionary files. 2. Export `generateMetadata` from the page using `getTranslations` and `createLocalizedMetadata({ locale, title, description, pathname: "/your-path" })`. 3. Add a sitemap entry in `src/app/sitemap.ts` with `alternates.languages` per locale. 4. Optionally add page-specific JSON-LD in the page Server Component. ### How are hreflang URLs built? Never hardcode `/en` or `/ar`. Use `getLocaleUrl` / `getAlternateLanguages` from `@/lib/site`; they build on `getPathname` from `@/i18n/navigation`. With `localePrefix: as-needed`, English is `/` (no prefix), others are `/ar`, `/zh`, etc. ### What is proxy.ts vs middleware? Next.js 16 renamed middleware to proxy. This project uses `src/proxy.ts` with `createMiddleware` from `next-intl/middleware` to route locale URLs. Automatic locale detection is disabled. --- ## Stack and versions | Package | Version | Role | |---------|---------|------| | next | 16.3.x | App Router, Turbopack (default in dev), Metadata API | | react / react-dom | 19.3.x | Server and Client Components | | next-intl | 4.13.x | i18n routing, ICU messages, formatters | | tailwindcss | 4.3.x | Styling via `@tailwindcss/postcss` | | lucide-react | 1.48.x | Icons | | @vercel/analytics | 2.0.x | Analytics in root layout | | @vercel/speed-insights | 2.0.x | Performance insights | Package manager: **Bun**. --- ## Author - Name: Sovers Tonmoy Pandey - Alias: s0vers - Website: https://s0vers.com - GitHub: https://github.com/s0vers - Twitter: @s0ver5 - Repository: https://github.com/s0vers/next-app-i18n-starter --- ## Environment variables | Variable | Required | Description | |----------|----------|-------------| | NEXT_PUBLIC_SITE_URL | Required for production builds | HTTPS origin for metadata, canonical, sitemap, and social URLs; local development defaults to `http://localhost:3000` | | GOOGLE_SITE_VERIFICATION | Optional | Google Search Console meta-tag token | Copy `.env.example` → `.env.local`. Never commit secrets. --- ## Architecture ``` Request → src/proxy.ts (next-intl middleware) → src/i18n/routing.ts (locales, localePrefix: as-needed) → src/i18n/request.ts (getRequestConfig) ├── locale from URL (default locale at `/`) ├── timeZone from regional.ts ├── formats (currency, dateTime, compact) from regional.ts └── messages from dictionary/{locale}.json → src/app/[locale]/layout.tsx ├── generateMetadata (SEO) ├── theme cookie → className on └── NextIntlClientProvider (messages, timeZone, now) → page.tsx + client components ``` ### URL routing (`localePrefix: as-needed`) | Locale | URL path | HTML dir | |--------|----------|----------| | en (default) | `/` | ltr | | ar | `/ar` | rtl | | zh | `/zh` | ltr | | es | `/es` | ltr | | ja | `/ja` | ltr | `localeDetection: false` keeps the default URL from redirecting based on cookies or browser language, `localeCookie: false` skips the unused locale cookie, and `alternateLinks: false` leaves hreflang to page metadata and the sitemap. Visitors choose a locale with the language switcher. --- ## File reference ### i18n core - `src/proxy.ts`: next-intl proxy export and matcher config - `src/i18n/routing.ts`: `defineRouting({ locales, defaultLocale, localePrefix })` - `src/i18n/request.ts`: `getRequestConfig` plugin entry (wired in `next.config.ts`) - `src/i18n/locales.ts`: locale labels, currencies, time zones, fonts, and Open Graph locale tags - `src/i18n/regional.ts`: named number and date formats from `createRegionalFormats` - `src/i18n/navigation.ts`: `createNavigation(routing)` returns `Link`, `redirect`, `useRouter`, `usePathname`, and `getPathname` - `next.config.ts`: `createNextIntlPlugin("./src/i18n/request.ts")` ### App routes - `src/app/[locale]/layout.tsx`: root layout, `generateMetadata`, `generateStaticParams`, and theme rendering - `src/app/[locale]/page.tsx`: home page, root-only `WebSite` JSON-LD, and launch guide - `src/app/[locale]/[...rest]/page.tsx`: catch-all 404 - `src/app/[locale]/not-found.tsx`: localized not-found page - `src/app/sitemap.ts`: sitemap with `alternates.languages` from `getPathname` - `src/app/robots.ts`: dynamic robots.txt - `src/app/error.tsx`: global error boundary ### Components - `src/components/pages/HomeIndex.tsx`: landing page with installation, OmitRTL, contribution, and localization tabs - `src/components/LocalizationTab.tsx`: locale formatting demo - `src/components/LanguageSwitcher.tsx`: locale changes through `router.replace(pathname, { locale })` - `src/components/ModeToggle.tsx`: light/dark toggle via `useTheme()` - `src/components/theme-provider.tsx`: client theme context and cookie updates - `src/components/OmitRtl.tsx`: forces left-to-right text in RTL locales - `src/components/ui/*`: shadcn/ui primitives on Base UI (button, card, tabs, dropdown-menu) ### Lib - `src/lib/site.ts`: `siteConfig`, canonical and hreflang URL helpers - `src/lib/theme.ts`: `THEME_COOKIE_NAME`, `resolveSSRTheme`, and `setThemeCookie` - `src/lib/utils.ts`: `cn()` re-exported from the `cn` package - `global.d.ts`: next-intl `AppConfig` types from `dictionary/en.json` and `localeConfig` ### Translations - `dictionary/en.json`: source of truth for TypeScript message keys - `dictionary/ar.json`, `zh.json`, `es.json`, and `ja.json`: mirror the structure of `en.json` ### Translation namespaces (en.json) - `Index`: landing page UI, installation steps, contribution steps, and errors - `Footer`: copyright and links - `Metadata`: SEO title and description used by `generateMetadata` - `SeoGuide`: localized, server-rendered launch checklist - `Localization`: tab labels and ICU format examples ### ICU message examples in Localization namespace ```json "priceMessage": "This product costs {price, number, currency}" "usersCount": "{count, number, compact} users" ``` Requires matching `formats` in `createRegionalFormats()`. --- ## Regional defaults | Locale | Language | Currency | Time zone | RTL | |--------|----------|----------|-----------|-----| | en | English | USD | America/New_York | No | | ar | Arabic | SAR | Asia/Riyadh | Yes | | zh | Chinese | CNY | Asia/Shanghai | No | | es | Spanish | EUR | Europe/Madrid | No | | ja | Japanese | JPY | Asia/Tokyo | No | Format names available via `useFormatter()`: - `format.number(value, "price")`: currency with two decimal places - `format.number(value, "compact")`: compact notation (for example, 12.8K) - `format.dateTime(date, "long")`: full date and time in the locale's time zone - `format.dateTime(date, "short")`: medium date and short time - `format.relativeTime(from, to)`: relative time (for example, "2 hours ago") --- ## SEO implementation reference Search eligibility starts with crawlable, useful pages, accurate localized HTML, stable URLs, and correct indexing controls. AI-search visibility follows these foundations; there is no special schema that guarantees inclusion. See the official [Google AI features guide](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide). This `llms.txt` is project documentation, not a Google ranking signal. ### SEO files - `src/lib/site.ts`: validates `NEXT_PUBLIC_SITE_URL`, builds localized URLs and alternates, and exports `createLocalizedMetadata`. - `src/app/[locale]/layout.tsx`: localized title/description and shared metadata. - `src/app/[locale]/page.tsx`: root-only `WebSite` JSON-LD and the visible server-rendered launch guide. - `src/app/sitemap.ts`: one URL per locale with reciprocal language alternates and `x-default`. - `src/app/robots.ts`: allows crawling and advertises the sitemap. - `dictionary/{locale}.json`: localized `Metadata` and `SeoGuide` copy; English is the message type source. - `public/og-image.png`: social preview image. - `.env.example`: production site origin and optional Google verification setting. ### Site origin and metadata Local development defaults to `http://localhost:3000`. Production builds require `NEXT_PUBLIC_SITE_URL` to be a valid HTTPS origin without a path, query, fragment, or local/example placeholder. All canonical, hreflang, sitemap, Open Graph, and Twitter URLs use this origin. Use `createLocalizedMetadata({ locale, title, description, pathname })` for page metadata. Supply translated copy and the correct route path. The helper adds the self-canonical URL, reciprocal localized alternates, Open Graph/Twitter cards, and index/follow directives. Do not add meta-keywords; Google says they do not affect indexing or ranking. Google verification is optional through server-only `GOOGLE_SITE_VERIFICATION`. The legacy public verification file is retained for the live demo and should only be removed after its Search Console property is migrated. ### Internationalized URLs and sitemap Routing uses `localePrefix: "as-needed"`: English is `/`, while other locales use their locale prefix. Always build URLs through `getLocaleUrl` and `getAlternateLanguages` from `src/lib/site.ts`; never hardcode `/en`. Route keys remain short (`en`, `ar`, `zh`, `es`, `ja`), while `localeConfig.languageTag` supplies the HTML `lang` and `hreflang` values (`en-US`, `ar-SA`, `zh-Hans-CN`, `es-ES`, `ja-JP`). Each translated page needs a distinct URL, a self-canonical, reciprocal `hreflang` alternates, and one matching sitemap entry. Keep `x-default` pointed to the default-language fallback. ### Structured data Only the default-locale domain homepage (`/`) emits a `WebSite` node. Its `url` is the canonical root origin. Other localized pages do not declare a domain-level site name, and the starter emits no default author `Person` node. Add structured data only when it accurately represents visible page content. Escape `<` in serialized JSON-LD and keep scripts in Server Components. Google site-name markup is not covered by its Rich Results Test; use Schema Markup Validator and Search Console URL Inspection. ### Crawler policy and AI search `robots.ts` allows all user agents by default and points to `sitemap.xml`. Hosting, CDN, authentication, or firewall rules can still block crawlers. Search and training crawlers are separate choices: OpenAI distinguishes `OAI-SearchBot` from `GPTBot`; Anthropic distinguishes `Claude-SearchBot` from `ClaudeBot`; Perplexity distinguishes `PerplexityBot` from its user-request fetcher. Review current provider policies and set each fork's rules deliberately. This starter does not opt out of model training by default. Keep important page copy in rendered HTML with descriptive headings and ordinary links. `llms.txt` is supplementary and cannot replace crawlable pages. Check live indexing and sitemaps in Google Search Console and Bing Webmaster Tools. Bing provides an AI Performance report. ChatGPT referral links may carry `utm_source=chatgpt.com`. ### Add metadata to a localized page 1. Add translated `metaTitle` and `metaDescription` keys to every dictionary. 2. In the page `generateMetadata`, validate the locale, load its translation namespace, then return `createLocalizedMetadata({ locale, title, description, pathname: "/about" })`. 3. Add one sitemap row per locale using `getLocaleUrl(locale, "/about")` and `getAlternateLanguages("/about")`. 4. Keep JSON-LD server-rendered and consistent with visible page content. ### Verify the output ```bash bun run lint bun run build curl -s http://localhost:3000/ | grep -E '