Next.js 13 appDir 实战 i18n
创始人
2024-03-16 17:22:20
0

背景

官方目前未打算支持 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 ComponentClient 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 方法

首先你需要一个 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

注意 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});
}

Hook

不用说,也是个 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;
}

app 中创建

不要直接在 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};
}

最后

  • 附上示例项目代码: https://github.com/willin/beta.willin.wang/tree/87dce6e673659dbc8e5a3aac25e3cb6f6ea828bb
  • 示例项目演示地址: https://willin-wang-ndd35eg64-willin.vercel.app/

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...