Internationalization and localization for web applications. Handles next-intl, react-intl, ICU MessageFormat, RTL layouts, and dynamic locale loading.
Internationalization (i18n) and localization (l10n) for web applications. Implements proper message formatting, locale routing, RTL support, and dynamic translation loading.
BAD: Flat structure, mixed namespaces
// messages/en.json
{
"welcomeMessage": "Welcome",
"homePageTitle": "Home",
"userProfileName": "Name",
"userProfileEmail": "Email",
"checkoutButtonSubmit": "Submit Order"
}
GOOD: Namespaced structure
// messages/en/common.json
{
"welcome": "Welcome",
"loading": "Loading...",
"error": "An error occurred"
}
// messages/en/home.json
{
"title": "Home",
"hero": {
"heading": "Welcome to our platform",
"subheading": "Build amazing things"
}
}
// messages/en/user.json
{
"profile": {
"name": "Name",
"email": "Email",
"updatedAt": "Last updated {date}"
}
}
BAD: String concatenation
// DON'T: Breaks in many languages
const message = count === 1
? `You have ${count} message`
: `You have ${count} messages`;
// DON'T: No gender agreement support
const greeting = gender === 'male' ? `Welcome, Mr. ${name}` : `Welcome, Ms. ${name}`;
GOOD: ICU MessageFormat
{
"messages": {
"count": "{count, plural, =0 {No messages} one {# message} other {# messages}}"
},
"greeting": {
"formal": "{gender, select, male {Welcome, Mr. {name}} female {Welcome, Ms. {name}} other {Welcome, {name}}}"
},
"cart": {
"items": "{itemCount, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}",
"total": "{total, number, ::currency/USD}"
}
}
// Usage with react-intl
import { useIntl } from 'react-intl';
function MessageCount({ count }: { count: number }) {
const intl = useIntl();
return <p>{intl.formatMessage({ id: 'messages.count' }, { count })}</p>;
}
BAD: Client-side only, no routing
// app/page.tsx - DON'T
'use client';
import { useState } from 'react';
export default function Page() {
const [locale, setLocale] = useState('en');
const messages = require(`@/messages/${locale}.json`);
return <div>{messages.welcome}</div>;
}
GOOD: Server-side with middleware routing
// i18n.ts
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';
export const locales = ['en', 'es', 'fr', 'ar'] as const;
export type Locale = (typeof locales)[number];
export default getRequestConfig(async ({ locale }) => {
if (!locales.includes(locale as Locale)) notFound();
return {
messages: (await import(`./messages/${locale}.json`)).default
};
});
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales } from './i18n';
export default createMiddleware({
locales,
defaultLocale: 'en',
localePrefix: 'always' // /en/about, /es/about
});
export const config = {
matcher: ['/', '/(en|es|fr|ar)/:path*']
};
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { locales } from '@/i18n';
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
if (!locales.includes(locale as any)) notFound();
const messages = await getMessages();
return (
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('home');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('hero.heading')}</p>
</div>
);
}
BAD: Hardcoded formats
// DON'T: Breaks internationalization
const date = new Date().toLocaleDateString('en-US');
const price = `$${amount.toFixed(2)}`;
const percent = `${(value * 100).toFixed(1)}%`;
GOOD: Intl API
import { useIntl } from 'react-intl';
function ProductCard({ price, date, discount }: Props) {
const intl = useIntl();
// Date formatting
const formattedDate = intl.formatDate(date, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Currency formatting
const formattedPrice = intl.formatNumber(price, {
style: 'currency',
currency: 'USD'
});
// Percentage formatting
const formattedDiscount = intl.formatNumber(discount, {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 1
});
return (
<div>
<p>{formattedPrice}</p>
<p>{formattedDiscount} off</p>
<time>{formattedDate}</time>
</div>
);
}
// Or use native Intl API directly
const dateFormatter = new Intl.DateTimeFormat('es-ES', {
dateStyle: 'full',
timeStyle: 'short'
});
const numberFormatter = new Intl.NumberFormat('de-DE', {
style: 'decimal',
minimumFractionDigits: 2
});
console.log(dateFormatter.format(new Date())); // "domingo, 2 de marzo de 2026, 14:30"
console.log(numberFormatter.format(1234.5)); // "1.234,50"
BAD: Hardcoded left/right
/* DON'T: Breaks in RTL languages */
.sidebar {
float: left;
margin-right: 20px;
text-align: left;
}
.arrow {
padding-left: 10px;
}
GOOD: Logical properties
/* Use logical properties */
.sidebar {
float: inline-start;
margin-inline-end: 20px;
text-align: start;
}
.arrow {
padding-inline-start: 10px;
}
/* Or use Tailwind RTL utilities */
.element {
@apply ms-4 me-2; /* margin-start, margin-end */
@apply ps-4 pe-2; /* padding-start, padding-end */
}
/* RTL-specific overrides */
[dir="rtl"] .custom-element {
transform: scaleX(-1); /* Flip icons */
}
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./app/**/*.{ts,tsx}'],
theme: {
extend: {}
},
plugins: [
require('tailwindcss-logical')
]
};
export default config;
BAD: Load all locales upfront
// DON'T: Bundles all translations
import en from './messages/en.json';
import es from './messages/es.json';
import fr from './messages/fr.json';
import ar from './messages/ar.json';
const messages = { en, es, fr, ar };
GOOD: Dynamic imports
// lib/i18n.ts
export async function loadMessages(locale: string) {
try {
const messages = await import(`@/messages/${locale}.json`);
return messages.default;
} catch (error) {
console.error(`Failed to load messages for locale: ${locale}`);
return import('@/messages/en.json').then(m => m.default);
}
}
// For large translation files, split by namespace
export async function loadNamespace(locale: string, namespace: string) {
const messages = await import(`@/messages/${locale}/${namespace}.json`);
return messages.default;
}
// app/[locale]/dashboard/page.tsx
import { loadNamespace } from '@/lib/i18n';
export default async function DashboardPage({ params }: Props) {
const messages = await loadNamespace(params.locale, 'dashboard');
return <NextIntlClientProvider messages={messages}>
{/* Dashboard content */}
</NextIntlClientProvider>;
}
BAD: Manual cookie management
// DON'T: Unreliable, no fallback
'use client';
import { useRouter } from 'next/navigation';
function LangSwitcher() {
const router = useRouter();
const changeLocale = (locale: string) => {
document.cookie = `locale=${locale}`;
router.refresh();
};
return <button onClick={() => changeLocale('es')}>Español</button>;
}
GOOD: next-intl locale switching
// components/LocaleSwitcher.tsx
'use client';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter, usePathname } from 'next/navigation';
import { locales } from '@/i18n';
export function LocaleSwitcher() {
const t = useTranslations('common');
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const changeLocale = (newLocale: string) => {
// Replace locale in pathname
const segments = pathname.split('/');
segments[1] = newLocale;
router.push(segments.join('/'));
};
return (
<select
value={locale}
onChange={(e) => changeLocale(e.target.value)}
aria-label={t('selectLanguage')}
>
{locales.map((loc) => (
<option key={loc} value={loc}>
{t(`locales.${loc}`)}
</option>
))}
</select>
);
}
// With content negotiation fallback
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales } from './i18n';
export default createMiddleware({
locales,
defaultLocale: 'en',
localeDetection: true, // Uses Accept-Language header
localePrefix: 'as-needed' // Only add prefix for non-default locales
});
BAD: String keys, runtime errors
// DON'T: Typos cause runtime errors
const title = t('home.titel'); // Typo: "titel" instead of "title"
const message = t('user.proifle.name'); // Typo: "proifle"
GOOD: Generated types
// scripts/generate-types.ts
import fs from 'fs';
import path from 'path';
function generateTypes() {
const enMessages = JSON.parse(
fs.readFileSync('./messages/en.json', 'utf-8')
);
const types = generateTypeFromObject(enMessages);
fs.writeFileSync(
'./types/messages.d.ts',
`export type Messages = ${types};`
);
}
// types/messages.d.ts (generated)
export type Messages = {
common: {
welcome: string;
loading: string;
};
home: {
title: string;
hero: {
heading: string;
subheading: string;
};
};
user: {
profile: {
name: string;
email: string;
};
};
};
// Usage with type safety
import { useTranslations } from 'next-intl';
function HomePage() {
const t = useTranslations('home');
// TypeScript autocomplete and validation
return <h1>{t('title')}</h1>; // ✓
// return <h1>{t('titel')}</h1>; // ✗ TypeScript error
}
{
"items": {
"selected": "{count, plural, =0 {No items selected} one {# item selected} other {# items selected}}",
"remaining": "{count, plural, =0 {All done!} one {# item left} other {# items left}}"
}
}
// messages/en.json
{
"terms": "By signing up, you agree to our <link>Terms of Service</link>"
}
// Component
import { useTranslations } from 'next-intl';
import Link from 'next/link';
function SignupForm() {
const t = useTranslations('auth');
return (
<p>
{t.rich('terms', {
link: (chunks) => <Link href="/terms">{chunks}</Link>
})}
</p>
);
}
// app/[locale]/about/page.tsx
import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ params }: Props) {
const t = await getTranslations({ locale: params.locale, namespace: 'about' });
return {
title: t('metaTitle'),
description: t('metaDescription')
};
}
export default async function AboutPage({ params }: Props) {
const t = await getTranslations({ locale: params.locale, namespace: 'about' });
return <h1>{t('title')}</h1>;
}
Apply these patterns to build robust, scalable internationalization in web applications.
npx skills add medy-gribkov/i18n-localization下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer