📚 现代前端开发 目录
阶段4:UI与体验
第 26 / 35 课

国际化(i18n)

让应用走向世界

📖 核心概念

💻 代码实现

next-intl国际化 ✅ typescript
// messages/zh.json
// {
//   "common": {
//     "hello": "你好,{name}!",
//     "items": "{count, plural, =0 {没有项目} =1 {1个项目} other {#个项目}}",
//     "currency": "{value, number, :: currency/CNY}"
//   },
//   "nav": { "home": "首页", "about": "关于", "contact": "联系" },
//   "blog": { "title": "博客", "publishedAt": "发布于 {date, date, long}" }
// }

// messages/en.json
// {
//   "common": {
//     "hello": "Hello, {name}!",
//     "items": "{count, plural, =0 {No items} =1 {1 item} other {# items}}",
//     "currency": "{value, number, :: currency/USD}"
//   },
//   "nav": { "home": "Home", "about": "About", "contact": "Contact" },
//   "blog": { "title": "Blog", "publishedAt": "Published on {date, date, long}" }
// }

// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';

export default getRequestConfig(async ({ locale }) => ({
  locale,
  messages: (await import(\`../messages/\${locale}.json\`)).default,
}));

// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
  locales: ['zh', 'en', 'ja'],
  defaultLocale: 'zh',
});
export const config = { matcher: ['/((?!api|_next|_vercel|.*\..*).*)'] };

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';

export default function LocaleLayout({ children, params: { locale } }) {
  return (
    <html lang={locale} dir={getDirection(locale)}>
      <body>
        <NextIntlClientProvider locale={locale}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

// 组件中使用
import { useTranslations } from 'next-intl';

function Greeting({ name }: { name: string }) {
  const t = useTranslations('common');
  return (
    <div>
      <p>{t('hello', { name })}</p>
      <p>{t('items', { count: 0 })}</p>  {/* 没有项目 */}
      <p>{t('items', { count: 5 })}</p>  {/* 5个项目 */}
    </div>
  );
}
格式化与RTL ✅ tsx
// 日期/数字/货币格式化
import { useFormatter, useTranslations } from 'next-intl';

function ProductInfo({ product }: { product: Product }) {
  const format = useFormatter();
  const t = useTranslations('product');

  return (
    <div>
      {/* 货币格式化 */}
      <p className="price">
        {format.number(product.price, { style: 'currency', currency: 'CNY' })}
      </p>

      {/* 日期格式化 */}
      <p className="date">
        {t('publishedAt', { date: new Date(product.createdAt) })}
      </p>

      {/* 相对时间 */}
      <p className="relative">
        {format.relativeTime(new Date(product.updatedAt))}
      </p>

      {/* 数字格式化 */}
      <p className="sales">
        已售 {format.number(product.sales, { notation: 'compact' })}
      </p>
    </div>
  );
}

// RTL支持
function getDirection(locale: string): 'ltr' | 'rtl' {
  const rtlLocales = ['ar', 'he', 'fa', 'ur'];
  return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
}

// RTL感知的CSS
// .flex-container {
//   /* 自动适配RTL */
//   display: flex;
//   gap: var(--space-2);
/*   /* LTR: margin-left: auto; RTL: margin-right: auto; */
/*   margin-inline-start: auto; */
/*   /* LTR: padding-left; RTL: padding-right */
/*   padding-inline-start: var(--space-4); */
/*   border-inline-start: 2px solid var(--color-accent); */
/* }

// 语言切换组件
import { useRouter, usePathname } from 'next/navigation';
import { useLocale } from 'next-intl';

function LanguageSwitcher() {
  const router = useRouter();
  const pathname = usePathname();
  const currentLocale = useLocale();

  const switchLocale = (newLocale: string) => {
    router.replace(pathname.replace(\`/\${currentLocale}\`, \`/\${newLocale}\`));
  };

  return (
    <select value={currentLocale} onChange={e => switchLocale(e.target.value)}>
      <option value="zh">中文</option>
      <option value="en">English</option>
      <option value="ja">日本語</option>
    </select>
  );
}

🎯 练习任务

🏆 成就解锁
国际化工程师 — 掌握i18n全链路,让应用服务全球用户