• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

国际化是前端应用的常见需求,比如一个应用要同时支持中文和英文用户访问。

如果你在外企工作,那基本要天天做这件事情,比如我待过韩企和日企,我们的应用要支持韩文和英文,或者日文和英文。

那如何实现这种国际化的需求呢?

用 react-intl 这个包。

这个包周下载量很高:

我们来用一下。

创建个项目:

npx create-vite

我们先安装 antd 来写个简单的页面:

npm install

npm install --save antd

去掉 main.tsx 里的 StrictMode 和 index.css

然后写下 App.tsx

import React from "react";
import type { FormProps } from "antd";
import { Button, Checkbox, Form, Input } from "antd";

type FieldType = {
    username?: string;
    password?: string;
    remember?: string;
};

const onFinish: FormProps<FieldType>["onFinish"] = (values) => {
    console.log("Success:", values);
};

const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (errorInfo) => {
    console.log("Failed:", errorInfo);
};

const App: React.FC = () => (
    <Form
        name="basic"
        labelCol={{ span: 8 }}
        wrapperCol={{ span: 16 }}
        style={{ maxWidth: 600 }}
        initialValues={{ remember: true }}
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
        autoComplete="off">
        <Form.Item<FieldType>
            label="Username"
            name="username"
            rules={[
                { required: true, message: "Please input your username!" },
            ]}>
            <Input />
        </Form.Item>

        <Form.Item<FieldType>
            label="Password"
            name="password"
            rules={[
                { required: true, message: "Please input your password!" },
            ]}>
            <Input.Password />
        </Form.Item>

        <Form.Item<FieldType>
            name="remember"
            valuePropName="checked"
            wrapperCol={{ offset: 8, span: 16 }}>
            <Checkbox>Remember me</Checkbox>
        </Form.Item>

        <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
            <Button type="primary" htmlType="submit">
                Submit
            </Button>
        </Form.Item>
    </Form>
);

export default App;

这里是直接从 antd 官网复制的代码。

把服务跑起来:

npm run dev

浏览器访问下:

那如果这个页面要同时支持中文、英文呢?

只要把需要国际化的文案转成一个 key,然后根据当前 locale 是中文还是英文来读取不同的资源包就好了:

locale 是“语言代码-国家代码”,可以从 navigator.language 拿到:

资源包就是一个 json 文件里面有各种 key 对应的不同语言的文案,比如 zh-CN.json、en-US.json 等。

我们用 react-intl 实现下:

在 main.tsx 引入下 IntlProvider,它是用来设置 locale 和 messsages 资源包的:

import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { IntlProvider } from "react-intl";
import enUS from "./en-US.json";
import zhCN from "./zh-CN.json";

const messages: Record<string, any> = {
    "en-US": enUS,
    "zh-CN": zhCN,
};
const locale = navigator.language;

ReactDOM.createRoot(document.getElementById("root")!).render(
    <IntlProvider
        messages={messages[locale]}
        locale={locale}
        defaultLocale="zh_CN">
        <App />
    </IntlProvider>
);

然后写一下 zh-CN.json 和 en-US.json

{
    "username": "Username",
    "password": "Password",
    "rememberMe": "Remember Me",
    "submit": "Submit",
    "inputYourUsername": "Please input your username!",
    "inputYourPassword": "Please input your password!"
}
{
    "username": "用户名",
    "password": "密码",
    "rememberMe": "记住我",
    "submit": "提交",
    "inputYourUsername": "请输入你的用户名!",
    "inputYourPassword": "请输入你的密码!"
}

把 App.tsx 里的文案换成从资源包取值的方式:

defineMessages 和 useIntl 都是 react-intl 的 api。

defineMessages 是定义 message,这里的 id 就是资源包里的 key,要对应才行。

此外还可以定义 defaultMessage,也就是资源包没有对应的 key 的时候的默认值:

useIntl 有很多 api,比如 formatMessage 的 api 就是根据 id 取不同 message 的。

import React from "react";
import type { FormProps } from "antd";
import { Button, Checkbox, Form, Input } from "antd";
import { useIntl, defineMessages } from "react-intl";

type FieldType = {
    username?: string;
    password?: string;
    remember?: string;
};

const onFinish: FormProps<FieldType>["onFinish"] = (values) => {
    console.log("Success:", values);
};

const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (errorInfo) => {
    console.log("Failed:", errorInfo);
};

const messsages = defineMessages({
    username: {
        id: "username",
        defaultMessage: "用户名",
    },
    password: {
        id: "password",
    },
    rememberMe: {
        id: "rememberMe",
    },
    submit: {
        id: "submit",
    },
    inputYourUsername: {
        id: "inputYourUsername",
    },
    inputYourPassword: {
        id: "inputYourPassword",
    },
});

const App: React.FC = () => {
    const intl = useIntl();

    return (
        <Form
            name="basic"
            labelCol={{ span: 8 }}
            wrapperCol={{ span: 16 }}
            style={{ maxWidth: 600 }}
            initialValues={{ remember: true }}
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}
            autoComplete="off">
            <Form.Item<FieldType>
                label={intl.formatMessage(messsages.username)}
                name="username"
                rules={[
                    {
                        required: true,
                        message: intl.formatMessage(
                            messsages.inputYourUsername
                        ),
                    },
                ]}>
                <Input />
            </Form.Item>

            <Form.Item<FieldType>
                label={intl.formatMessage(messsages.password)}
                name="password"
                rules={[
                    {
                        required: true,
                        message: intl.formatMessage(
                            messsages.inputYourUsername
                        ),
                    },
                ]}>
                <Input.Password />
            </Form.Item>

            <Form.Item<FieldType>
                name="remember"
                valuePropName="checked"
                wrapperCol={{ offset: 8, span: 16 }}>
                <Checkbox>{intl.formatMessage(messsages.rememberMe)}</Checkbox>
            </Form.Item>

            <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                <Button type="primary" htmlType="submit">
                    {intl.formatMessage(messsages.submit)}
                </Button>
            </Form.Item>
        </Form>
    );
};

export default App;

试一下:

可以看到,现在文案就都变成中文了。

然后改下 locale:

现在界面又都是英文了:

其他语言也是同理。

但国际化可不只是替换下文案这么简单,日期、数字等的格式也都不一样。

react-intl 包也支持:

<div>
  日期:
  <div>{intl.formatDate(new Date(), { weekday: 'long' })}</div>
  <div>{intl.formatDate(new Date(), { weekday: 'short' })}</div>
  <div>{intl.formatDate(new Date(), { weekday: 'narrow' })}</div>
  <div>{intl.formatDate(new Date(), {  dateStyle: 'full' })}</div>
  <div>{intl.formatDate(new Date(), {  dateStyle: 'long' })}</div>
</div>
<div>
  相对时间:
  <div>{intl.formatRelativeTime(200, 'hour')}</div>
  <div>{intl.formatRelativeTime(-10, 'minute')}</div>
</div>
<div>
  数字:
  <div>{intl.formatNumber(200000, {
    style: 'currency',
    currency: 'USD'
  })}</div>
  <div>
    {
      intl.formatNumber(10000, {
        style: 'unit',
        unit: 'meter'
      })
    }
  </div>
</div>

然后换成 zh-CN 再看下:

可以看到,确实不同语言的表示方式不一样:

但这里金额没有切换过来,需要改一下:

<div>
    {intl.formatNumber(200000, {
        style: "currency",
        currency: intl.locale.includes("en") ? "USD" : "CNY",
    })}
</div>

根据 locale 来分别设置为美元符号 USD 或者人民币符号 CNY。

现在就都对了。

当然,可以国际化的东西还有很多,用到的时候查文档就行:

我们主要用的 useIntl 的 api,然后调用 formatXxx 方法。

其实这些 api 都有组件版本:

<div>
    <div>
        <FormattedDate value={new Date()} dateStyle="full"></FormattedDate>
    </div>
    <div>
        <FormattedMessage id={messsages.rememberMe.id}></FormattedMessage>
    </div>
    <div>
        <FormattedNumber
            style="unit"
            unit="meter"
            value={2000}></FormattedNumber>
    </div>
</div>

哪种方便用哪种。

回过头来再看下 message 的国际化。

message 支持占位符,比如这样:

用的时候传入具体的值:

<div>
    <div>{intl.formatMessage(messsages.username, { name: "光" })}</div>
    <div>
        <FormattedMessage
            id={messsages.username.id}
            values={{ name: "东" }}></FormattedMessage>
    </div>
</div>

此外,国际化的消息还可以用一些 html 标签,也就是支持富文本。

这样:

在 IntlProvider 的 defaultRichTextElements 这里定义所有的富文本标签:

<IntlProvider
    messages={messages[locale]}
    locale={locale}
    defaultLocale="zh_CN"
    defaultRichTextElements={{
        bbb: (str) => <b>{str}</b>,
        strong: (str) => <strong>{str}</strong>,
    }}>
    <App />
</IntlProvider>

这样,运行时就会把他们替换成具体的标签:

掌握这些功能,国际化需求就足够用了。

此外,还要注意下兼容性问题:

react-intl 的很多 api 都是对浏览器原生的 Intl api 的封装:

而 Intl 的 api 在一些老的浏览器不支持,这时候引入下 polyfill 包就好了:

那如果我想在组件外用呢?

也可以,用 createIntl 的 api:

src/getMessage.ts

import { createIntl, defineMessages } from "react-intl";
import enUS from "./en-US.json";
import zhCN from "./zh-CN.json";

const messages: Record<string, any> = {
    "en-US": enUS,
    "zh-CN": zhCN,
};

const locale = "zh-CN";
const intl = createIntl({
    locale: locale,
    messages: messages[locale],
});

const defines = defineMessages({
    inputYourUsername: {
        id: "inputYourUsername",
        defaultMessage: "",
    },
});

export default function () {
    return intl.formatMessage(defines.inputYourUsername);
}

在 App.tsx 里引入下:

useEffect(() => {
    setTimeout(() => {
        alert(getMessage());
    }, 2000);
}, []);

可以看到,在非组件里也可以做文案的国际化。

还有一个问题,不知道大家有没有觉得把所有需要国际化的地方找出来,然后在资源包里定义一遍很麻烦?

确实,react-intl 提供了一个工具来自动生成资源包。

我们用一下:

npm i -save-dev @formatjs/cli

用这个工具需要所有 message 都有默认值,前面我们省略了,这里改一下:

const messsages = defineMessages({
    username: {
        id: "username",
        defaultMessage: "用户名",
    },
    password: {
        id: "password",
        defaultMessage: "密码",
    },
    rememberMe: {
        id: "rememberMe",
        defaultMessage: "记住我",
    },
    submit: {
        id: "submit",
        defaultMessage: "提交",
    },
    inputYourUsername: {
        id: "inputYourUsername",
        defaultMessage: "请输入用户名!",
    },
    inputYourPassword: {
        id: "inputYourPassword",
        defaultMessage: "请输入密码!",
    },
});

然后执行 extract 命令从 ts、vue 等文件里提所有 defineMessage 定义的消息:

npx formatjs extract "src/**/*.tsx" --out-file temp.json

然后可以看到我们 defineMessage 定义的所有 message 都提取了出来,key 是 id:

接下来再执行 compile 命令生成资源包 json:

npx formatjs compile 'temp.json' --out-file src/ja-JP.json

可以看到它用所有的 message 的 id 和默认值生成了新的资源包。

这样,只要把这个资源包交给产品经理或者设计师去翻译就好了。

最后把刚才的临时文件删除:

rm ./temp.json

这个 cli 工具对于项目中 defineMessage 定义了很多国际化消息,想要全部提取出来生成一个资源包的场景还是很有用的。

案例代码上传了小册仓库

总结

很多应用都要求支持多语言,也就是国际化,如果你在外企,那几乎天天都在做这个。

我们用 react-intl 包实现了国际化。

它支持在 IntlProvider 里传入 locale 和 messages,然后在组件里用 useIntl 的 formatMessage 的 api 或者用 FormatMessage 组件来取资源包中的消息。

定义消息用 defineMessages,指定不同的 id。

在 en-US.json、zh-CN.json 资源包里定义 message id 的不同值。

这样,就实现了文案的国际化。

此外,message 支持占位符和富文本,资源包用 {name}、<xxx></xxx>的方式来写,然后用的时候传入对应的文本、替换富文本标签就好了。

如果是在非组件里用,要用 createIntl 的 api。

当然,日期、数字等在不同语言环境会有不同的格式,react-intl 对原生 Intl 的 api 做了封装,可以用 formatNumber、formatDate 等 api 来做相应的国际化。

如果应用中有很多 defineMessage 的国际化消息,想要批量提取出来生成资源包,可以用 @formatjs/cli 的 extract、compile 命令来做。

掌握了这些功能,就足够实现前端应用中各种国际化的需求了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
48.原子化状态管理库Jotai
Next
50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理