背景与需求
我们希望在以下约束条件下实现多语言支持:
- 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>
);
}
技术优势
- 类型安全:TypeScript + Context API 支持翻译键存在性检查
- 清晰的依赖关系:使用
useTranslations的组件必须置于 Provider 内,使设计明确 - 性能:通过静态生成实现快速页面交付
- SEO:每种语言页面有独立的 URL,针对搜索引擎进行了优化
- 用户体验:基于浏览器语言设置的自动重定向减少用户操作
总结
我们在 Next.js App Router 静态导出环境中使用 React Context API 实现了多语言支持。通过客户端逻辑补充服务端约束,实现了良好的用户体验。
更改默认语言只需编辑 config.ts 中的 defaultLocale,确保了可扩展性。
