国际化 (i18n) 是现代 Web 开发的一个重要方面。它使我们能够创建可供世界各地用户访问的网站,无论他们的语言或位置如何。在本文中,我们将探讨如何使用 Next.js 和 react-intl 使 i18n 变得简单。
Installation
要在 Next.js 和 react-intl 中开始使用 i18n,我们需要安装一些依赖项。首先,我们需要安装 react-intl 库:
sh
npm install react-intl
NextJS Config
这就是我们的 next.config.js
文件所需要的全部内容。
javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
// instead of wrapping our Component inside <StricMode/> we set this to true.
reactStrictMode: true,
i18n:{
locales: ['en', 'de'],
defaultLocale: 'en',
}
}
module.exports = nextConfig
Folder structure
这是我们的文件夹结构
lang
文件夹包含我们的json
文件以及flattenMessages.ts
,我们将在后面解释。
pages
文件夹是 NextJS 文件夹,我们将把我们的路由放进去
React Itl Config
react-intl 是一个很棒的库,它使内化工作变得非常容易!它提供了超酷的功能(我们今天不会介绍其中的大部分!我们将只关注如何处理基本的内化)
typeScript
import type { AppProps } from "next/app";
import { IntlProvider } from "react-intl";
import { useLocale } from "../hooks/useLocale";
export default function App({ Component, pageProps }: AppProps) {
const { locale, messages } = useLocale();
console.log(locale);
return (
<IntlProvider locale={locale as string} messages={messages}>
<Component {...pageProps} />
</IntlProvider>
);
}
在我们的_app.tsx
文件中,我们只是导入IntlProvider
并包装我们的Component.
IntlProvider:
需要 2 个 props。locale
在我们的例子中是 en
或 de
&messages:
具有key``value
对的对象,因为我们在这里不能有嵌套对象!但是,如果我们看一下我们的[locale].json
文件,我们会发现我们的文件包含嵌套的 JSON!
为什么?因为嵌套的 JSON 可以很容易地按屏幕或主题拆分我们的键!因此,它们可以有一个清晰的结构,而简单的 key value
JSON 则不会如此。
因此,为了解决这个问题,我们有这个flattenMessages
方法,它只接受任何 json 并对其进行flattens
以使其成为key value
对!
typeScript
// This interface is using Indexed signature and Recursive type utility in TS.
// It just means our json might have key and value of type string or nested json.
export interface INestedMessages {
[key: string]: string | INestedMessages;
}
export const flattenMessages = (
nestedMessages: INestedMessages,
prefix = ""
): Record<string, string> => {
return Object.keys(nestedMessages).reduce(
(messages: Record<string, string>, key) => {
const value = nestedMessages[key];
const prefixedKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "string") {
messages[prefixedKey] = value;
} else {
Object.assign(messages, flattenMessages(value, prefixedKey));
}
return messages;
},
{}
);
};
flattenMessages(en)
// output: key value pair
{
"app.title": "NextJS internalisation",
"app.locale_switcher.en": "English",
"app.locale_switcher.de": "German",
"app.main.description": "This article is trying to explain how we can use react-intl with NextJS and use also Typescript to make our life easy!"
}
Custom hooks: useLocale
typeScript
import { useRouter } from "next/router";
import { useCallback, useMemo } from "react";
import en from "@/../lang/en.json";
import de from "@/../lang/de.json";
import { flattenMessages, INestedMessages } from "../../lang/flattenMessages";
// Union type
export type Locale = "en" | "de";
// a Record is an object wich we can pass union types to it as key.
const messages: Record<Locale, INestedMessages> = {
en,
de,
};
export const useLocale = () => {
const router = useRouter();
const flattenedMessages = useMemo(
() => flattenMessages(messages[router.locale as keyof typeof messages]),
[router]
);
const switchLocale = useCallback(
(locale: Locale) => {
// if we already have /en and we choose english for example we just return!
if (locale === router.locale) {
return;
}
// This is how we change locale in NextJS.
const path = router.asPath;
return router.push(path, path, { locale });
},
[router]
);
return { locale: router.locale, switchLocale, messages: flattenedMessages };
};
这个定制钩子非常简单!它只是使用 nextjs
中的 useRouter
钩子来获取语言环境并切换语言环境。它还会返回翻译消息的扁平化版本。 它使用起来非常简单,通过这种方式,我们抽象了locale
管理的复杂性。
typeScript
cosnt {switchLocale, locale, messages} = useLocale()
Custom Hooks: useTranslate
typeScript
import type { PrimitiveType } from "react-intl";
import { useIntl } from "react-intl";
import type { FormatXMLElementFn } from "intl-messageformat";
import { useCallback } from "react";
import { TranslationKey } from "../../lang/flattenMessages";
export const useTranslate = () => {
const { formatMessage } = useIntl();
// Define a function called t that takes in a key of type TranslationKey
(key: TranslationKey ) =>
// Call formatMessage with an object that has an id property set to the given key
[formatMessage]
);
return { t };
};
现在这是我们useTranslate
自定义钩子!我们使用的是 react-intl
formatMessage
方法中的useIntl
。 此方法具有必需的参数MessageDescriptor
它只是一个具有id:string
的对象。它还接受本文范围之外的其他参数。
TranslationKey
是我们构建的用于检索翻译键的 TS 类型。转换键只是以字符串结尾的所有路径。在我们的例子中,我们的翻译键是app.title | app.locale_switcher.en | app.locale.de | app.main.description.
我们需要提取这种类型,这样我们就不会在尝试翻译字符串时出现错别字。
typeScript
// Define a TypeScript type called KeyPaths that takes in an object type T as its generic type parameter.
// KeyPaths is defined as a mapped type, which produces a new type by iterating over the keys of T.
type KeyPaths<T extends INestedMessages> = {
// For each key K in T, create a mapped type that checks whether the value of that key T[K] extends INestedMessages,
// i.e., whether the value is an object that has string keys and unknown values.
[K in keyof T]: T[K] extends INestedMessages
? // If the value of the key is an object, create a string literal type that represents the path to that object.
// The path is constructed by combining the current key K with a dot separator and the key paths of the nested object T[K].
// The & string is used to ensure that TypeScript recognizes this string literal as a string.
`${K & string}.${KeyPaths<T[K]> & string}`
: // If the value of the key is not an object, simply return the key name K.
K;
// Finally, index the KeyPaths type by keyof T, which returns a union of all the key paths in the object type T.
// This means that KeyPaths<T> is a union of all the possible key paths in the object type T.
}[keyof T];
export type TranslationKey = KeyPaths<typeof en>;
这就是结果!我们的 IDE 将帮助我们避免任何typos
,我们不需要记住所有这些路径!如果我们更改翻译文件中的路径,我们也会收到错误,并且在我们使用它的任何地方修复它之前,构建不会通过。
Translated Page
最后,这是我们的页面。
typeScript
import { Inter } from "next/font/google";
import { Locale, useLocale } from "@/hooks/useLocale";
import { useTranslate } from "@/hooks/useTranslate";
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
const { switchLocale, locale } = useLocale();
const { t } = useTranslate();
return (
<main
className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
>
<div className="flex items-center justify-between w-full">
<h1 className="text-3xl">{t("app.title")}</h1>
<select
onChange={(e) => {
switchLocale(e.target.value as Locale);
}}
className="outline-none rounded-xl text-black px-2 w-32"
placeholder="Language"
>
<option value="en">{t("app.locale_switcher.en")}</option>
<option value="de">{t("app.locale_switcher.de")}</option>
</select>
</div>
</main>
);
}
Conclusion
在本文中,我们讨论了将 react-intl 与 NextJs 一起使用来构建一个内部化的 Web 应用程序。
我们使用 TypeScript 为我们的locales
和translationKeys.
构建类型
我们还使用customHooks
来抽象locale
和translation
管理的所有复杂性。
github链接: github.com/wugaoliang1...