From openclaudia-openclaudia-skills
Adds full internationalization (i18n) to Next.js App Router projects using next-intl, with SEO-friendly locale routing, hreflang sitemaps, 14+ languages, and bulk translation.
npx claudepluginhub joshuarweaver/cascade-communication --plugin openclaudia-openclaudia-skillsThis skill uses the workspace's default tool permissions.
Add complete internationalization to a Next.js (App Router) project using **next-intl v4**. This skill handles routing, translation files, sitemap hreflang, and bulk translation across all locales.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Add complete internationalization to a Next.js (App Router) project using next-intl v4. This skill handles routing, translation files, sitemap hreflang, and bulk translation across all locales.
package.json) — must be 13+ with App Routernext-intl, next-i18next, [locale] routes)npm install next-intl
Create 4 files under src/i18n/:
src/i18n/config.tsexport const locales = ['en', 'es', 'fr', 'de', 'pt', 'ja', 'ar', 'zh', 'zh-tw', 'id', 'vi', 'ms', 'ru', 'hi'] as const
export type Locale = (typeof locales)[number]
export const defaultLocale: Locale = 'en'
export const localeNames: Record<Locale, string> = {
en: 'English',
es: 'Espanol',
fr: 'Francais',
de: 'Deutsch',
pt: 'Portugues',
ja: '日本語',
ar: 'العربية',
zh: '简体中文',
'zh-tw': '繁體中文',
id: 'Bahasa Indonesia',
vi: 'Tieng Viet',
ms: 'Bahasa Melayu',
ru: 'Русский',
hi: 'हिन्दी',
}
export const rtlLocales: Locale[] = ['ar']
src/i18n/routing.tsimport { defineRouting } from 'next-intl/routing'
import { defaultLocale, locales } from './config'
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: 'as-needed', // English URLs stay clean, other locales get /es/, /fr/, etc.
})
src/i18n/navigation.tsimport { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)
src/i18n/request.tsimport { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
}
})
Create src/middleware.ts:
import createMiddleware from 'next-intl/middleware'
import { routing } from '@/i18n/routing'
export default createMiddleware({
...routing,
localeDetection: false, // Don't auto-redirect based on Accept-Language
})
export const config = {
matcher: ['/((?!_next|api|images|fonts|favicon|sitemap|robots).*)'],
}
Key decision: localeDetection: false prevents auto-redirecting users based on browser language. This keeps English URLs stable for SEO. Users can manually switch languages via a language selector.
Wrap the existing config with createNextIntlPlugin:
import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
// ... existing config ...
export default withNextIntl(nextConfig)
[locale] Dynamic RouteMove all page content under src/app/[locale]/:
Create src/app/[locale]/layout.tsx with:
generateStaticParams() returning all localessetRequestLocale(locale) call<NextIntlClientProvider> wrapping children<html lang={locale} dir={rtlLocales.includes(locale) ? 'rtl' : 'ltr'}><link> tags in <head> for all locales + x-defaultMove existing pages into src/app/[locale]/
Each page should call setRequestLocale(locale) for static generation
Create src/messages/en.json with all user-facing strings organized by section:
{
"common": { "signIn": "Sign In", ... },
"tools": { "tool-slug": { "title": "...", "description": "..." } },
"faq": { "tool-slug": [{ "question": "...", "answer": "..." }] }
}
Replace all hardcoded strings in components with useTranslations():
const t = useTranslations('common')
return <button>{t('signIn')}</button>
For server components, use getTranslations():
const t = await getTranslations('common')
For each non-English locale, create src/messages/{locale}.json with the same structure as en.json.
Use parallel Codex agents via the codex-tasks skill to save Claude credits:
/codex-tasksen.json, translates all strings, writes {locale}.jsonen.json content (or path to read it){count}, {name} unchangedsrc/messages/{locale}.jsonAfter translation, run a verification script to catch issues:
import json
locales = ['es', 'fr', 'de', 'pt', 'ja', 'ar', 'zh', 'zh-tw', 'id', 'vi', 'ms', 'ru', 'hi']
english_words = ['the ', 'and ', 'you ', 'your ', 'our ', 'this ', 'that ', 'with ', 'from ', 'will ']
with open('src/messages/en.json') as f:
en = json.load(f)
for loc in locales:
with open(f'src/messages/{loc}.json') as f:
data = json.load(f)
# Check: missing sections
missing = [s for s in en if s not in data]
# Check: residual English content
eng_count = 0
def check(d):
nonlocal eng_count # won't work in inline script; use list trick
if isinstance(d, dict):
for v in d.values(): check(v)
elif isinstance(d, str):
if sum(1 for w in english_words if w in d.lower()) >= 3:
eng_count += 1
check(data)
status = 'OK' if not missing and eng_count == 0 else 'ISSUES'
print(f'{loc}: {status} (missing={len(missing)}, english={eng_count})')
Update src/app/sitemap.ts to include hreflang alternates:
import { MetadataRoute } from 'next'
import { locales } from '@/i18n/config'
const baseUrl = 'https://www.example.com'
function buildAlternates(path: string): Record<string, string> {
const alternates: Record<string, string> = {}
for (const locale of locales) {
const prefix = locale === 'en' ? '' : `/${locale}`
alternates[locale] = `${baseUrl}${prefix}${path}`
}
return alternates
}
export default function sitemap(): MetadataRoute.Sitemap {
return pages.map((path) => ({
url: `${baseUrl}${path}`,
lastModified: new Date(),
alternates: { languages: buildAlternates(path) },
}))
}
Important: Generate one canonical URL per page with hreflang alternates, NOT one URL per locale. This prevents duplicate content in search results.
Add a language switcher component that uses useRouter and usePathname from @/i18n/navigation to switch locales while preserving the current path.
npm run build — check that all static pages generate correctlyhttps://example.com/toolshttps://example.com/es/toolspublic/sitemap.xml conflicts with dynamic src/app/sitemap.ts in dev mode — delete the static one or rename it_next, api, sitemap, robots, and static asset pathslocalePrefix: 'as-needed' is critical — it keeps default locale URLs clean for SEO continuitylocaleDetection: false prevents unwanted redirects that break SEO and confuse usersgit config http.postBuffer 524288000