Next.jsReactTypeScripti18n

Next.js App Router + 静态导出的多语言架构

Sloth255
Sloth255
·2 min read·241 words

背景与需求

我们希望在以下约束条件下实现多语言支持:

  • Next.js 15 App Router with TypeScript
  • 静态导出output: 'export')——通过 S3 + CloudFront 部署
  • 无服务端渲染——无法使用动态 API 和中间件
  • 需要 SEO——必须向搜索引擎提供适当的语言内容

架构概述

1. 路由设计

/                    # 根页面(默认:en)
/en/                 # 英语页面
/ja/                 # 日语页面

使用动态段 [locale] 在静态生成期间为每种语言预渲染页面。

2. 翻译数据管理

翻译数据以 JSON 格式管理:

src/locales/en.json
{
  "title": "title"
}
src/locales/ja.json
{
  "title": "タイトル"
}

3. 使用 React Context API 管理状态

src/i18n/context.tsx
interface I18nContextType {
  messages: Record<string, any>;
  locale: string;
}

const I18nContext = createContext<I18nContextType | undefined>(undefined);

export function I18nProvider({ 
  children, 
  messages, 
  locale 
}: I18nProviderProps) {
  return (
    <I18nContext.Provider value={{ messages, locale }}>
      {children}
    </I18nContext.Provider>
  );
}

export function useTranslations() {
  const context = useContext(I18nContext);
  if (!context) {
    throw new Error('useTranslations must be used within I18nProvider');
  }
  
  const t = (key: string): string => {
    // 支持嵌套键
    const keys = key.split('.');
    let value: any = context.messages;
    for (const k of keys) {
      value = value?.[k];
    }
    return typeof value === 'string' ? value : key;
  };
  
  return { 
    t, 
    locale: context.locale,
    messages: context.messages,
    isLoaded: true 
  };
}

4. 在布局层级提供 Context

src/app/layout.tsx
export default async function RootLayout({ children }: Props) {
  const locale = defaultLocale;
  const messages = await getMessages(locale);
  
  return (
    <html lang={locale}>
      <body>
        <I18nProvider messages={messages} locale={locale}>
          <LanguageSwitcherClient />
          {children}
        </I18nProvider>
      </body>
    </html>
  );
}
src/app/[locale]/layout.tsx
export default async function LocaleLayout({ children, params }: Props) {
  const { locale } = await params;
  const messages = await getMessages(locale);
  
  return (
    <div lang={locale}>
      <I18nProvider messages={messages} locale={locale}>
        {children}
      </I18nProvider>
    </div>
  );
}

默认语言与客户端重定向

服务端(静态生成期间)

根页面(/)始终以**英语(en)**生成:

src/app/page.tsx
export default async function RootPage() {
  const locale = defaultLocale; // 'en'
  const messages = await getMessages(locale);
  
  return (
    <I18nProvider messages={messages} locale={locale}>
      <ClientLocaleRedirect />
      <PageContent />
    </I18nProvider>
  );
}

原因

  • SEO 策略:为搜索引擎爬虫提供稳定内容
  • 静态导出约束:无法在服务端确定用户语言

客户端(浏览器)

首次访问时,检测浏览器语言设置后自动重定向:

src/components/ClientLocaleRedirect.tsx
'use client';

export default function ClientLocaleRedirect() {
  useEffect(() => {
    // 检查是否已经执行过重定向
    if (typeof window === 'undefined' || 
        sessionStorage.getItem('localeRedirectDone')) {
      return;
    }

    const supportedLocales = ['en', 'ja'];
    const pathname = window.location.pathname;
    
    // 仅对根路径执行重定向
    if (pathname === '/') {
      // 获取浏览器语言设置
      const browserLocales = navigator.languages || [navigator.language];
      
      // 查找支持的语言
      const preferredLocale = browserLocales
        .map(lang => lang.split('-')[0]) // 'ja-JP' -> 'ja'
        .find(lang => supportedLocales.includes(lang));
      
      // 如果与默认语言不同则重定向
      if (preferredLocale && preferredLocale !== defaultLocale) {
        sessionStorage.setItem('localeRedirectDone', 'true');
        window.location.replace(`/${preferredLocale}`);
      }
    }
  }, []);

  return null;
}

行为场景

情况 SSG 期间 客户端行为
搜索引擎爬虫 以英语生成 / 不重定向
日语浏览器用户 以英语生成 / 重定向到 /ja
英语浏览器用户 以英语生成 / 不重定向(原样显示)
不支持语言的用户 以英语生成 / 不重定向(以英语显示)

在组件中的使用

'use client';

function NetworkInfoClient() {
  const { t } = useTranslations();
  
  return (
    <div>
      <h2>{t('title')}</h2>
    </div>
  );
}

技术优势

  1. 类型安全:TypeScript + Context API 支持翻译键存在性检查
  2. 清晰的依赖关系:使用 useTranslations 的组件必须置于 Provider 内,使设计明确
  3. 性能:通过静态生成实现快速页面交付
  4. SEO:每种语言页面有独立的 URL,针对搜索引擎进行了优化
  5. 用户体验:基于浏览器语言设置的自动重定向减少用户操作

总结

我们在 Next.js App Router 静态导出环境中使用 React Context API 实现了多语言支持。通过客户端逻辑补充服务端约束,实现了良好的用户体验。

更改默认语言只需编辑 config.ts 中的 defaultLocale,确保了可扩展性。