A workflow for App Router developers
Next.js App Router Localization: A Practical Workflow
A useful localization workflow keeps source copy in code review, gives translators context, and returns validated messages to the application. Start with one namespace and one target language before changing every route.
By SaaS Localization Planner · Reviewed
Choose how locales reach your App Router pages
Decide whether each language needs a public URL, or whether locale is an authenticated user preference. For public acquisition pages, language-specific URLs give you distinct pages to link to and describe. For a private dashboard, a preference-based approach can be sufficient. Record that decision before moving your route tree.
Next.js’s internationalization guide describes a locale route segment and dictionaries. next-intl’s routing setup covers locale-based navigation. A dynamic locale segment needs build-time parameters if you export static HTML. Request-time locale negotiation requires a suitable runtime or hosting-layer solution; a static export cannot run a Next.js proxy.
Keep a known-good branch and a list of current URLs before changing routes. Recheck canonical URLs, language alternatives, sitemap entries and redirect behavior after a routing change. Adding a translation file alone does not complete multilingual SEO.
Keep translation JSON small and reviewable
Use stable message identifiers describing intent, such as Dashboard.greeting. Avoid using the English sentence as the identifier if ordinary copy edits should not create new keys. Give translators the screen, audience and placeholder meaning. Keep one source language authoritative.
messages/en.json
{
"Dashboard": {
"title": "Your workspace",
"greeting": "Welcome, {name}!"
}
}messages/de.json
{
"Dashboard": {
"title": "Dein Arbeitsbereich",
"greeting": "Willkommen, {name}!"
}
}Both files keep the same nested shape and the {name} placeholder. These two strings illustrate the structure; they are not a professionally reviewed product translation. Add plural and select messages only after checking the syntax your runtime expects.
Group by feature when the inventory grows. Include checkout errors, empty states and transactional copy in your inventory, with separate review requirements for sensitive wording. The AI vs human translation guide can help decide where automated drafts are appropriate.
A minimal next-intl starting point
Install next-intl, commit the resolved lockfile and retain your existing Next.js settings. The official App Router starter separates request configuration, the Next.js plugin and translated components. These small examples deliberately fix the locale to English so you can first verify message rendering.
// src/i18n/request.ts — minimal single-locale starting point
import {getRequestConfig} from 'next-intl/server';
export default getRequestConfig(async () => ({
locale: 'en',
messages: (await import('../../messages/en.json')).default
}));// next.config.ts — merge with your existing settings
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
export default withNextIntl({});// src/app/page.tsx — a Server Component
import {getTranslations} from 'next-intl/server';
export default async function Dashboard() {
const t = await getTranslations('Dashboard');
return <main><h1>{t('title')}</h1><p>{t('greeting', {name: 'Alex'})}</p></main>;
}This is a Server Component example. To translate Client Components, configure NextIntlClientProvider as documented; do not expose translation-management credentials in its props. Keep the correct html lang attribute in your root layout.
Once rendering works, implement locale selection using the official routing setup rather than importing arbitrary filenames from user input. Allow only supported locale identifiers, load the matching message bundle and test unsupported locales. The single-locale snippets above do not implement a language switcher or routing. The current docs use proxy.ts for Next.js 16 and later; older versions use middleware.ts. Check the version-specific instructions before copying routing code.
Connect translation management without changing the runtime
The translation manager stores keys, language content and review status. next-intl renders the resulting messages. Keep this boundary explicit: upload a selected source namespace, translate and review it, then export the expected JSON structure into a separate branch or review artifact.
Compare SimpleLocalize vs Tolgee by hosting needs, file fidelity and team access. If a managed file workflow fits, try a one-language SimpleLocalize pilot (affiliate link, new tab). Check nested export settings; a flattened file may not match the lookup structure your app uses.
Affiliate link: we may earn a commission if you sign up, at no extra cost to you. This does not affect our rankings or recommendations.
Use the GitHub Actions guide to automate file transfers after a manual round trip succeeds. Do not make every application build depend on a live management API unless you have designed its failure behavior. Reviewed, versioned messages make rollbacks easier to explain.
Validate JSON keys before release
Save the following as scripts/check-messages.mjs. It accepts nested objects with string leaves, rejects arrays and non-string leaves, and fails when target key paths differ from the source. It intentionally supports simple nested identifiers; use your runtime’s tooling for other message structures.
import {readFileSync} from 'node:fs';
function keys(value, path = '') {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Expected a JSON object at ' + (path || '/'));
}
return Object.entries(value).flatMap(([key, child]) => {
if (key.includes('.') || key.includes('/')) {
throw new Error('Use simple nested message keys: ' + key);
}
const next = path + '/' + key;
if (typeof child === 'string') return [next];
return keys(child, next);
}).sort();
}
const [sourceFile, ...targets] = process.argv.slice(2);
if (!sourceFile || targets.length === 0) {
throw new Error('Usage: node check-messages.mjs source.json target.json [...]');
}
const read = (file) => JSON.parse(readFileSync(file, 'utf8'));
const source = keys(read(sourceFile));
if (source.length === 0) throw new Error('Source has no messages');
for (const file of targets) {
const target = keys(read(file));
if (source.join('\n') !== target.join('\n')) {
throw new Error('Message keys differ: ' + file);
}
}
console.log('Message keys match');node scripts/check-messages.mjs messages/en.json messages/de.jsonThis checks structure only. It cannot validate ICU grammar, placeholder parity, linguistic quality or rendered layout. Those need separate checks and human review; matching keys can still contain broken messages.
- Run your application build and type checks against the imported message files.
- Render missing messages, plural cases and formatted dates and numbers in each supported locale.
- Inspect long translations at phone width, including buttons, checkout and error states.
- Exercise language switching, navigation, canonical URLs and language alternatives on public pages.
- Review the translation diff and preserve a rollback before publishing.
Use the localization checklist for release ownership and the cost guide to budget recurring review work.
Sources and testing scope
Checked October 20, 2026: Next.js internationalization, Next.js static exports, next-intl App Router setup and next-intl locale routing.
Locally verified: JSON parsing, matching nested keys, and rejection of missing keys, extra keys, invalid JSON, arrays and numeric leaves. Next.js configuration and component snippets were checked for TypeScript/TSX syntax only. No vendor account was connected and no claim is made that a complete locale-routing application or live translation sync was tested.