skills/i18n/SKILL.md
Add full internationalization (i18n) to a Next.js project using next-intl. Supports 14+ languages, SEO-friendly locale routing, hreflang sitemaps, and bulk translation. Use when the user asks to "internationalize", "add i18n", "add translations", "multi-language", "localize", "add language support", or "translate my site".
npx skillsauth add OpenClaudia/openclaudia-skills i18nInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 524288000data-ai
Generate images using AI (OpenAI GPT Image or Stability AI). Use when the user asks to generate an image, create an AI image, make an illustration, or produce artwork from a text prompt.
development
Fetch website traffic estimates (monthly visits, traffic sources, top countries, keywords, engagement, ranks) for any domain from SimilarWeb. Use when the user asks about a domain's traffic, monthly visits, traffic sources, audience countries, or wants to compare/benchmark sites against competitors.
development
Find which ChatGPT search queries mention a given brand. Tests long-tail queries against ChatGPT's web-search-enabled model and reports which ones surface the brand. Use when the user asks to "find queries for [brand]", "check GEO visibility", "which queries mention [brand]", "geo query finder", "find AI mentions", or "test ChatGPT queries for [brand]".
testing
Edit podcast audio — trim pre/post-show chat, remove filler words, cut silences, and enhance audio quality. Use when the user asks to edit a podcast, clean up audio, remove fillers, trim a recording, or improve voice quality.