官方目前未打算支持 i18n 国际化路由支持,且尚未提供解决方案,但是我们可以通过实验特性 appDir 来实现。
Not Planned Features
We are currently not planning to include the following features in app:
- Internationalization (i18n) - we will be providing a guide on how to implement internationalization in app.
- AMP Support
If you need any of these features, we will continue to support
pages
, including bug fixes and feature additions, for multiple major versions.
文档地址: https://beta.nextjs.org/docs/app-directory-roadmap#not-planned-features
首先需要了解一下 Server and Client Components
服务器组件和客户端组件。
What do you need to do? | Server Component | Client Component |
---|---|---|
Fetch data. Learn more. | ✅ | ⚠️ |
Access backend resources (directly) | ✅ | ❌ |
Keep sensitive information on the server (access tokens, API keys, etc) | ✅ | ❌ |
Keep large dependencies on the server / Reduce client-side JavaScript | ✅ | ❌ |
Add interactivity and event listeners (onClick() , onChange() , etc) | ❌ | ✅ |
Use State and Lifecycle Effects (useState() , useReducer() , useEffect() , etc) | ❌ | ✅ |
Use browser-only APIs | ❌ | ✅ |
Use custom hooks that depend on state, effects, or browser-only APIs | ❌ | ✅ |
Use React Class components | ❌ | ✅ |
简单来说,服务器端组件不支持事件侦听、不支持生命周期状态,这导致了原有的好多组件不可以直接拿来就用了。比如 next-i18next, next-themes 等等好多,直接引入使用会报各种莫名其妙的错误。
废话不多说,直接进入正题。
首先你需要一个 i18n 的实现,可以用 i18next,也可以用 rosetta 之类的。我这里实现了一个简化版的。
// 参考示例项目的 /i18n/next-i18n.ts
import dlv from 'dlv';
import tmpl from 'templite';// eslint-disable-next-line no-unused-vars
type Fn = (...args: any[]) => string;
export interface I18nDict {[key: string]: string | number | Fn | I18nDict;
}export interface NextI18nOptions {/*** Define the list of supported languages, this is used to determine if one of* the languages requested by the user is supported by the application.* This should be be same as the supportedLngs in the i18next options.*/supportedLanguages: string[];/*** Define the fallback language that it's going to be used in the case user* expected language is not supported.* This should be be same as the fallbackLng in the i18next options.*/fallbackLng: string;
}export class NextI18n {private currentLocale: string;public fallbackLng: string;public supportedLanguages: string[];private dict: I18nDict = {};constructor(options: NextI18nOptions) {this.currentLocale = options.fallbackLng;this.supportedLanguages = options.supportedLanguages;this.fallbackLng = options.fallbackLng;}public locale = (lang?: string) => {if (lang !== undefined && this.currentLocale !== lang) {this.currentLocale = lang;this.onChangeLanguage?.(lang);}return this.currentLocale;};public set = (lang: string, dict: I18nDict) => {this.dict[lang] = Object.assign(this.dict[lang] || {}, dict);};public t = (key: string, params?: any, lang?: string): string => {// eslint-disable-next-lineconst val = dlv(this.dict[lang || this.currentLocale] as any, key, key);// eslint-disable-next-lineif (typeof val === 'function') return val(params) as string;// eslint-disable-next-lineif (typeof val === 'string') return tmpl(val, params);return val as string;};/* PROTECTED */// eslint-disable-next-line no-unused-varsprivate onChangeLanguage?: (locale: string) => void;// eslint-disable-next-line no-unused-varspublic setOnChange = (fn: (locale: string) => void) => {this.onChangeLanguage = fn;};
}
然后是初始化这个 i18n 实例:
// 参考示例项目的 /i18n/index.ts
import { NextI18n } from './next-i18n';export const languages = {'zh-CN': { name: '简体中文', flag: '🇨🇳', unicode: '1f1e8-1f1f3' },'zh-TW': { name: '正體中文', flag: '🇹🇼', unicode: '1f1f9-1f1fc' },en: { name: 'English', flag: '🇺🇸', unicode: '1f1fa-1f1f8' },ko: { name: '한국어', flag: '🇰🇷', unicode: '1f1f0-1f1f7' },ja: { name: '日本語', flag: '🇯🇵', unicode: '1f1ef-1f1f5' }
};export const supportedLanguages = Object.keys(languages);
export const fallbackLng = 'zh-CN';const i18n = new NextI18n({supportedLanguages,fallbackLng
});supportedLanguages.forEach((locale) => {// eslint-disable-next-line @typescript-eslint/no-unsafe-argumenti18n.set(locale, require(`./${locale}/common.json`));
});export default i18n;
注意 Provider 中用到了 useState
来强制刷新,所以必须是个 Client Component
。
// 参考示例项目的 /i18n/provider.ts
'use client';import {createContext,createElement,ReactNode,useMemo,useState
} from 'react';
import { NextI18n } from './next-i18n';export const context = createContext<{ i18n: NextI18n } | null>(null);interface I18nProviderProps {children: ReactNode;i18n: NextI18n;
}export function I18nProvider({ i18n, children }: I18nProviderProps) {const [, setTick] = useState(0);const value = useMemo(() => {// eslint-disable-next-linei18n.setOnChange(() => {setTick((s) => s + 1);});return { i18n };}, [i18n]);// eslint-disable-next-line react/no-children-propreturn createElement(context.Provider, {value: { ...value },children});
}
不用说,也是个 Client Component
。
// 参考示例项目的 /i18n/hook.ts
'use client';import { useContext } from 'react';
import { context } from './provider';export function useI18n() {const content = useContext(context);if (!content) {throw new Error('Unable to get instance of i18n');}return content.i18n;
}
不要直接在 layout、page 中使用 Client Component
,所以我又在 Provider 上套了一层。
// laoyout.tsx
// 手动套一层 provider
import { I18nClientProvider } from './providers';export default function RootLayout({children,params
}: {children: React.ReactNode;params: { locale: string };
}) {const { locale = 'zh-CN' } = params || {};return ( { minHeight: 'calc(100vh - 75px)' }}>{children});
}
这个 Provider 的代码为:
'use client';
import { I18nProvider, i18n } from '@/i18n';
import { useEffect } from 'react';export function I18nClientProvider({children,locale
}: {children: React.ReactNode;locale: string;
}) {useEffect(() => {if (i18n.locale() !== locale) {i18n.locale(locale);}}, [locale]);return {children} ;
}