---
title: 国际化
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

ice.js 官方提供 i18n 国际化插件，支持在应用快速开启国际化能力。核心特性包括：

1. 支持自动处理和生成国际化路由
2. 完美支持 SSR 和 SSG，以获得更好的 SEO 优化
3. 支持自动重定向到偏好语言对应的页面
4. 不耦合任何一个 i18n 库（流行的 React i18n 库有 [react-intl](https://formatjs.io/docs/getting-started/installation/)、[react-i18next](https://react.i18next.com/) 等），你可以选择任一国际化的库来为你的应用设置国际化

<details open>
  <summary>使用国际化插件的示例</summary>
  <ul>
    <li>
      <a href="https://github.com/alibaba/ice/tree/master/examples/with-i18n" target="_blank" rel="noopener noreferrer">
        with-i18n
      </a>
    </li>
  </ul>
</details>

:::tip

如果应用不需要使用国际化路由，你可以参考以下例子来让你的项目支持国际化：

- [with-antd5](https://github.com/alibaba/ice/tree/master/examples/with-antd5)
- [with-fusion](https://github.com/alibaba/ice/tree/master/examples/with-fusion)

:::

## 快速开始

首先，我们需要在终端执行以下命令安装插件：

```bash
$ npm i @ice/plugin-i18n -D
```

然后在 `ice.config.mts` 中添加插件和选项：

```ts
import { defineConfig } from '@ice/app';
import i18n from '@ice/plugin-i18n';

export default defineConfig({
  plugins: [
    i18n({
      locales: ['zh-CN', 'en-US', 'de'],
      defaultLocale: 'zh-CN',
    }),
  ],
});
```

上面的 `en-US` 和 `zh-CN` 是国际化语言的缩写，它们均遵循标准的 [UTS 语言标识符](https://www.unicode.org/reports/tr35/tr35-59/tr35.html#Identifiers)。比如：

- `zh-CN`：中文（中国）
- `zh-HK`：中文（香港）
- `en-US`：英文（美国）
- `de`: 德文

## 国际化路由

国际化路由是指在页面路由地址中包含了当前页面的语言，一个国际化路由对应一个语言。

假设现在插件的选项配置是：

```ts
import { defineConfig } from '@ice/app';
import i18n from '@ice/plugin-i18n';

export default defineConfig({
  plugins: [
    i18n({
      locales: ['zh-CN', 'en-US', 'nl-NL'],
      defaultLocale: 'zh-CN',
    }),
  ],
});
```

假设我们有一个页面 `src/pages/home.tsx`，那么将会一一对应自动生成以下的路由：

- `/home`：显示 `zh-CN` 语言，默认语言对应的路由不包含语言前缀
- `/en-US/home`：显示 `en-US` 语言
- `/nl-NL/home`：显示 `nl-NL` 语言

访问不同的路由，将会显示该语言对应页面内容。

## 获取语言信息

### `getLocales()`

`getAllLocales()` 用于获取当前应用支持的所有语言。

```ts
import { getAllLocales } from 'ice';

console.log(getAllLocales()); // ['zh-CN', 'en-US']
```

### `getDefaultLocale()`

`getDefaultLocale()` 用于获取应用配置的默认语言。

```ts
import { getDefaultLocale } from 'ice';

console.log(getDefaultLocale()); // 'zh-CN'
```

### `useLocale()`

在 Function 组件中使用 `useLocale()` Hook API，它的返回值是一个数组，包含两个值：

1. 当前页面的语言
2. 一个 set 函数用于更新当前页面的语言。注意，默认情况下调用此 set 函数时候，同时会更新 Cookie 中 `ice_locale` 的值为当前页面的语言。这样，再次访问该页面时，从服务端请求能得知当前用户的之前设置的偏好语言，以便返回对应语言的页面内容。


```tsx
import { useLocale } from 'ice';

export default function Home() {
  const [locale, setLocale] = useLocale();
  
  console.log('locale: ', locale);  // 'en-US'
  return (
    <>
      {/* 切换语言为 zh-CN */}
      <div onClick={() => setLocale('zh-CN')}>Set zh-CN</div>
    </>
  )
}
```

### `withLocale()`

使用 `withLocale()` 方法包裹的 Class 组件，组件的 Props 会包含 `locale` 和 `setLocale()` 函数，可以查看和修改当前页面的语言。注意，默认情况下调用 `setLocale()`，会更新 Cookie 中 `ice_locale` 的值为当前页面的语言。这样，再次访问该页面时，从服务端请求能得知当前用户的之前设置的偏好语言，以便返回对应语言的页面内容。

```tsx
import { withLocale } from 'ice';

function Home({ locale, setLocale }) {
  console.log('locale: ', locale);  // 'en-US'
  return (
    <>
      {/* 切换语言为 zh-CN */}
      <div onClick={() => setLocale('zh-CN')}>Set zh-CN</div>
    </>
  )
}

export default withLocale(Home);
```

## 切换语言

推荐使用 `setLocale()` 方法配合 `<Link>` 组件或者 `useNavigate()` 方法进行语言切换：

<Tabs>
<TabItem value="a" label="使用 <Link />">

```tsx
import { useLocale, getAllLocales, Link, useLocation } from 'ice';

export default function Layout() {
  const location = useLocation();
  const [activeLocale, setLocale] = useLocale();

  return (
    <main>
      <p><b>Current locale: </b>{activeLocale}</p>
  
      <b>Choose language: </b>
      <ul>
        {
          getAllLocales().map((locale: string) => {
            return (
              <li key={locale}>
                <Link 
                  to={location.pathname} 
                  onClick={() => setLocale(locale)}
                >
                  {locale}
                </Link>
              </li>
            );
          })
        }
      </ul>
    </main>
  );
}
```

</TabItem>
<TabItem value="b" label="使用 useNavigate()">

```tsx
import { useLocale, useNavigate, useLocation } from 'ice';

export default function Layout() {
  const [, setLocale] = useLocale();
  const location = useLocation();
  const navigate = useNavigate();
  const switchToZHCN = () => {
    setLocale('zh-CN'); 
    navigate(location.pathname);
  }
  return (
    <main>
      <div onClick={switchToZHCN}>
        点我切换到中文
      </div>
    </main>
  );
}
```

</TabItem>
</Tabs>

## 路由自动重定向

路由自动重定向是指，如果当前访问的页面是根路由（`/`），将会根据当前语言环境自动跳转到对应的国际化路由。

默认情况下，路由自动重定向的功能是关闭的。如果需要开启，则需要加入以下内容：

```diff
import { defineConfig } from '@ice/app';
import i18n from '@ice/plugin-i18n';

export default defineConfig({
  plugins: [
    i18n({
      locales: ['zh-CN', 'en-US', 'de'],
      defaultLocale: 'zh-CN',
+     autoRedirect: true,
    }),
  ],
});
```
其中，语言环境的识别顺序如下：

- `CSR`：cookie 中 `ice_locale` 的值 > `window.navigator.language` > `defaultLocale`
- `SSR`：cookie 中 `ice_locale` 的值 > `Request Header` 中的 `Accept-Language` > `defaultLocale`

在部署阶段，路由自动重定向的功能需要配合 Node 中间件使用才能生效。比如：

```ts
import express from 'express';
import { renderToHTML } from './build/server/index.mjs';

const app = express();

app.use(express.static('build', {}));

app.use(async (req, res) => {
  const { statusCode, statusText, headers, value: body } = await renderToHTML({ req, res });
  res.statusCode = statusCode;
  res.statusMessage = statusText;
  Object.entries((headers || {}) as Record<string, string>).forEach(([name, value]) => {
    res.setHeader(name, value);
  });
  if (body && req.method !== 'HEAD') {
    res.end(body);
  } else {
    res.end();
  }
});
```

##  禁用 Cookie

在上面的章节中提到，用户设置的偏好语言是存放在 Cookie 中的 `ice_locale`，调用 `setLocale()` 时会更新到 Cookie 中，并且路由重定向和路由跳转的时候依赖 `ice_locale` 的值。

假设有这么一个场景，用户拒绝接受 Cookie，为了保护隐私，这样就不能把偏好语言写到 Cookie 中了。因此需要做以下的配置来禁用 Cookie：

```ts title="src/app.ts"
import { defineI18nConfig } from '@ice/plugin-i18n/types';

export const i18nConfig = defineI18nConfig(() => ({
  // 可以是一个 function
  disabledCookie: () => {
    if (import.meta.renderer === 'client') {
      return window.localStorage.getItem('acceptCookie') === 'yes';
    }
    return false;
  },
  // 也可以是 boolean 值
  // disabledCookie: true,
}));
```

这样，就禁用掉了 Cookie 的写入了。在切换语言的时候需要在 `state` 对象中显式传入即将要切换的新语言的值：

```tsx
import { Link, useLocale } from 'ice';

export default function Home() {
  const [, setLocale] = useLocale();
  return (
    <>
      <Link 
        to="/"
        onClick={() => setLocale('zh-CN')}
        state={{ locale: 'zh-CN' }}
      >
        切换到 zh-CN
      </Link>
    </>
  )
}
```

## SSG

在开启 SSG 功能后，将根据配置的 `locales` 的值，在 `build` 阶段会生成不同语言对应的 HTML。

比如我们有以下的目录结构，包含 `about` 和 `index` 两个页面：

```md
├── src/pages
|  ├── about.tsx
|  └── index.tsx
```

假如插件的配置是：

```ts
import { defineConfig } from '@ice/app';
import i18n from '@ice/plugin-i18n';

export default defineConfig({
  plugins: [
    i18n({
      locales: ['zh-CN', 'en-US'],
      defaultLocale: 'zh-CN',
    }),
  ],
});
```

那么将会生成 4 个 HTML 文件：

```md
├── build
|  ├── about
|  |  └── index.html
|  ├── en-US
|  |  ├── about
|  |  |  └── index.html
|  |  └── index.html
|  ├── index.html
```

## 插件选项

### `locales`

- **类型：**`string[]`

用于声明该应用支持的语言。

### `defaultLocale`

- **类型：**`string`

声明该应用默认的语言。需要注意的是， `locales` 数组必须包含 `defaultLocale` 的值。

### `autoRedirect`

- **类型：**`boolean`
- **默认值：**`false`

默认不会自动重定向到用户偏好语言对应的页面。如果设置为 `true`，在生产环境下，一般需要配合 Node 中间件一起使用才能生效。[详见](#路由自动重定向)
