Codapult uses next-intl with URL path-prefix routing (localePrefix: 'as-needed'). Five locales ship out of the box — English (en), Russian (ru), German (de), French (fr), and Japanese (ja) — and adding more takes just a few minutes.
The default locale (en) has no URL prefix — pages are served at /about, /blog, etc. Non-default locales get a prefix: /ru/about, /de/blog, /ja/pricing.
How It Works
| Piece | Location |
|---|---|
| Routing config | src/i18n/routing.ts — defineRouting() with locale list |
| Navigation helpers | src/i18n/navigation.ts — locale-aware Link, useRouter |
| Request config | src/i18n/request.ts — getRequestConfig() |
| Translation files | messages/{en,ru,de,fr,ja}.json |
| Layout | src/app/[locale]/layout.tsx — root layout with [locale] |
The [locale] segment in the App Router captures the active locale from the URL. The proxy layer (src/proxy.ts) integrates next-intl middleware to detect and redirect to the correct locale automatically.
Using Translations
Client Components
'use client';
import { useTranslations } from 'next-intl';
export function Greeting() {
const t = useTranslations('dashboard');
return <h1>{t('welcome')}</h1>;
}
Server Components
import { getTranslations } from 'next-intl/server';
export default async function Page() {
const t = await getTranslations('dashboard');
return <h1>{t('welcome')}</h1>;
}
Message File Structure
Messages are organized by namespace. Use nested keys for logical grouping:
{
"dashboard": {
"welcome": "Welcome back",
"settings": "Settings",
"members": "{count, plural, one {# member} other {# members}}"
},
"auth": {
"signIn": "Sign in",
"signUp": "Create account"
}
}
Pluralization follows the ICU MessageFormat syntax — {count, plural, one {…} other {…}}.
Locale-Aware Navigation
Always use the locale-aware navigation helpers from @/i18n/navigation instead of next/link and next/navigation:
import { Link, useRouter, usePathname } from '@/i18n/navigation';
// Link automatically adds the locale prefix
<Link href="/about">About</Link>;
// Router navigation preserves locale context
const router = useRouter();
router.push('/dashboard');
These wrappers are generated by createNavigation(routing) and ensure all links and programmatic navigation include the correct locale prefix.
Adding a New Locale
- Create the message file — copy
messages/en.jsonand translate:
cp messages/en.json messages/es.json
- Register the locale in
src/i18n/routing.ts:
export const routing = defineRouting({
locales: ['en', 'ru', 'de', 'fr', 'ja', 'es'],
defaultLocale: 'en',
localePrefix: 'as-needed',
});
- Add a label for the locale switcher in
src/components/theme/LocaleSwitcher.tsx:
const localeLabels: Record<string, string> = {
en: 'English',
ru: 'Русский',
de: 'Deutsch',
fr: 'Français',
ja: '日本語',
es: 'Español',
};
- Done. The
LocaleSwitchercomponent and URL routing pick up the new locale automatically. Pages are available at/es/....
Locale Switcher
The LocaleSwitcher component renders a dropdown that navigates to the current page in a different locale. It uses useRouter().replace(pathname, { locale }) from @/i18n/navigation to switch locales via URL. It is included in the marketing header (Navbar).
Blog Localization
Blog posts use a filename convention for translations:
| File | Locale |
|---|---|
content/blog/my-post.mdx | Default (en) |
content/blog/my-post.ru.mdx | Russian |
content/blog/my-post.de.mdx | German |
content/blog/my-post.fr.mdx | French |
content/blog/my-post.ja.mdx | Japanese |
The blog utilities in src/lib/blog/ automatically discover available locales per post and populate the availableLocales field in metadata. A per-post LocaleSwitch component lets readers navigate to the same article in a different locale.
If a post is not translated for the current locale, the reader sees the default-locale version.
Removing the i18n Module
If your product targets a single language, you can remove internationalization entirely via the setup wizard:
npx @codapult/cli setup
The wizard strips next-intl, the messages/ directory, the [locale] segment, the LocaleSwitcher component, and all useTranslations / getTranslations calls — leaving plain strings in their place.
SEO for Multi-Language Sites
Codapult includes built-in SEO support for multi-locale sites:
hreflangtags — Every page automatically generates<link rel="alternate" hreflang="xx" href="..." />tags for all locales via thelocaleAlternates()helper insrc/i18n/routing.ts. This is set viametadata.alternatesin each layout/page.- Canonical URLs — Each locale version has its own canonical URL (e.g.
/de/aboutfor German,/aboutfor English). - Sitemap —
src/app/sitemap.tsgenerates per-locale alternate URLs for every page, so search engines discover all locale variants. - URL structure — Default locale has clean URLs (
/about), non-default locales get path prefixes (/ru/about,/de/about). This is the industry-standard approach recommended by Google. - Blog SEO — Blog posts with locale variants automatically include cross-linked
hreflangtags and locale-specific OG images.