Codapult uses next-intl with cookie-based locale detection. Two locales ship out of the box — English (en) and Russian (ru) — and adding more takes just a few minutes.
How It Works
| Piece | Location |
| ----------------- | ---------------------------------------------- |
| Translation files | messages/en.json, messages/ru.json |
| Locale config | src/i18n/request.ts |
| Locale cookie | locale (set by the LocaleSwitch component) |
The active locale is read from the locale cookie on every request. If the cookie is missing or invalid, Codapult falls back to en.
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 {…}}.
Adding a New Locale
- Create the message file — copy
messages/en.jsonand translate:
cp messages/en.json messages/de.json
- Register the locale in
src/i18n/request.ts:
export const locales = ['en', 'ru', 'de'] as const;
- Done. The
LocaleSwitchcomponent and cookie logic pick up the new locale automatically.
Locale Switcher
The LocaleSwitch component renders a dropdown that sets the locale cookie and reloads the page. It is included in the marketing header and the blog layout.
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 |
The blog utilities in src/lib/blog/ automatically discover available locales per post and populate the availableLocales field in metadata, so the UI can show a language switcher on each article.
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 LocaleSwitch component, and all useTranslations / getTranslations calls — leaving plain strings in their place.