---
url: https://nsh.dev/zh-CN/docs/i18n
description: "把应用翻译成用户使用的每一种语言，在运行时切换语言，并按每种语言格式化数字和日期。"
---

# I18n

NSH 应用用两种方式讲用户的语言：**翻译**，即应用自己的文本在每种语言下的写法；**格式化**，即按各语言和地区的习惯书写数字、货币和日期。两者都跟随应用当前显示的语言：默认是用户自己的语言，直到应用选择另一种。

翻译使用 [rust-i18n](https://github.com/longbridge/rust-i18n) 的文件格式和 API，TSX 和 Rust 都一样：把文件放进 `locales/`，调用 `t("hello")`，文本就以用户的语言显示。

```tsx
import { Text } from "nsh";
import { t } from "nsh/i18n";

export default function App() {
  return <Text>{t("messages.hello", { name: "Jason" })}</Text>;
}
```

```rust
use nsh_core::prelude::*;

fn greeting() -> impl IntoElement {
    text(move || t!("messages.hello", name = "Jason"))
}
```

## 翻译文件

翻译放在应用 `nsh.json` 旁边的 `locales/` 目录里（Rust 应用放在 `Cargo.toml` 旁边）。不需要任何配置：构建时会读取这里的每个文件，并随应用一起打包。文件就是 rust-i18n 的文件，所以同一份文件也能给应用的 Rust crate、服务端或其他工具使用。

```
my-app/
  nsh.json
  App.tsx
  locales/
    app.yml
```

一个文件可以包含所有语言（`_version: 2`），每个 key 下面是它在各语言中的文本：

```yaml
# locales/app.yml
_version: 2

hello:
  en: Hello
  zh-CN: 你好
  ja: こんにちは

messages.hello:
  en: Hello, %{name}!
  zh-CN: 你好，%{name}！
  ja: こんにちは、%{name}！
```

也可以每种语言一个文件（`_version: 1`，默认格式），文件名以语言结尾（`en.yml`、`app.zh-CN.yml`），key 以对象形式嵌套：

```yaml
# locales/zh-CN.yml
hello: 你好
messages:
  hello: 你好，%{name}！
```

文件可以是 YAML（`.yml`、`.yaml`）、JSON（`.json`）或 TOML（`.toml`），也可以放在 `locales/` 的子目录里。嵌套的 key 用点连接，例如上面的 `messages.hello`。大型应用可以按功能拆分文件（`locales/settings.yml`、`locales/checkout.yml`）；同一语言在多个文件中的 key 会合并。

在 `nsh run` 和 `nsh dev` 中，保存 `locales/` 里的文件和保存代码一样会重新构建应用：正在运行的应用会显示新的文本。

## t

`t(key, options?)` 返回 `key` 在应用当前显示语言下的文本。

```tsx
import { t } from "nsh/i18n";

t("hello");                                  // "Hello"、"你好"……
t("messages.hello", { name: "Jason" });      // "Hello, Jason!"：填入 %{name}
t("hello", { locale: "ja" });                // "こんにちは"：指定另一种语言
```

```rust
t!("hello");
t!("messages.hello", name = "Jason");
t!("hello", locale = "ja");
```

- **参数**会填入文本中的 `%{name}`。值按文本写出（TSX 中是 `String(value)`，Rust 中是 `Display`）；没有对应参数的占位符保持原样。
- **`locale`** 用于取另一种语言的文本，而不是应用当前显示的语言。
- **回退**与 rust-i18n 相同：某个语言缺少的 key，会依次从去掉最后一个子标签的语言中查找（`zh-Hant-TW`，然后 `zh-Hant`，然后 `zh`），再从英语（`en`）中查找。所有语言都没有这个 key 时，`t` 返回 `<locale>.<key>`（`"fr.messages.hello"`），这样缺失的翻译会显示在屏幕上，而不是一片空白。
- **响应式**：`t` 会读取当前语言，所以调用它的文本、样式或计算值会在语言变化时更新，开发中翻译文件变化时也会更新。请在显示文本的地方调用它（`<Text>{t("hello")}</Text>`、`text(move || t!("hello"))`），不要只调用一次存进常量。

翻译在应用的任何模块运行之前就已就绪，所以在模块顶层也可以调用 `t`（此时结果只计算一次）。

## 应用显示的语言

默认情况下，应用的文本以用户的语言显示：在用户的偏好语言（`localeSettings().languages`，按系统列出的顺序，最偏好的在前）中，取第一个应用有翻译的语言（它本身或去掉子标签后的较短标签）。偏好瑞士德语、然后中文、然后英语的用户，在应用没有德语时看到 `zh` 文本，有德语时看到 `de` 文本。应用一个都没有时，文本以 `locale()`（设备的地区格式）的语言显示，并按上面的规则回退到英语。

`setLocale` 让应用显示用户在应用内选择的语言，而不管设备的语言是什么：

```tsx
import { Button } from "nsh/base";
import { Text, View } from "nsh";
import { availableLocales, setLocale, t } from "nsh/i18n";

export function LanguagePicker() {
  return (
    <View>
      <Each of={availableLocales()}>
        {(locale) => (
          <Button onPress={() => setLocale(locale())}>
            <Text>{t("language.name", { locale: locale() })}</Text>
          </Button>
        )}
      </Each>
    </View>
  );
}
```

```rust
use nsh_core::{platform::Locale, prelude::*};

fn use_chinese() {
    set_locale(Locale::parse("zh-CN"));
}

fn follow_the_device() {
    set_locale(None);
}
```

- `setLocale("zh-CN")` 会同时改变所有跟随语言的东西：`t`、`locale()`、`localeSettings().languages` 的第一项，以及 `Intl` 的默认 locale。不是 BCP 47 的标签会抛出 `RangeError`。
- `setLocale(null)`（Rust：`set_locale(None)`）重新跟随设备语言。
- `availableLocales()`（Rust：`available_locales()`）按顺序列出应用翻译所包含的语言。

这个选择在应用运行期间有效；需要记住它的应用自己保存（[`Storage`](https://nsh.dev/zh-CN/modules/nsh.md#storage)），并在启动时调用 `setLocale`。每个 surface 有自己的语言：同一个应用被嵌入两次时，可以显示两种语言。

## 数字和日期

`Intl` 和 `toLocaleString` 默认按应用显示的语言格式化。把 `locale()` 作为它们的 locale 传入，这样写数字的地方会读取语言，并在 `setLocale` 改变它时更新：

```tsx
import { locale } from "nsh";

const price = (amount: number) =>
  amount.toLocaleString(locale(), { style: "currency", currency: "EUR" });
// en-US: "€1,234.50"   de: "1.234,50 €"   zh-CN: "€1,234.50"
```

Native Shell 的运行时自带英语（`en-US`）及其地区变体的格式化数据。其他每种语言的数据（分隔符、紧凑单位、日期格式、月份和星期名称）都随应用一起提供：默认是应用翻译所包含的语言，或者 `nsh.json` 中 `locales` 列出的语言。

```json
{
  "id": "com.example.shop",
  "locales": ["de", "fr", "ja", "zh-CN"]
}
```

- **没有 `locales`** 时，应用按它有翻译的语言（上面的 `locales/`）和英语格式化。
- **有 `locales`** 时，应用正好按列出的语言和英语格式化：当用户格式化所用的语言与翻译的语言不同时列出它们（例如只有英文界面、但要显示德国价格的应用），或者用来去掉某些语言。
- 没有打包的语言按英语（`en-US`）格式化，与没有数据的 locale 一向的行为相同。列出 Native Shell 没有数据的语言时，构建会给出警告。

每种语言只给 bundle 增加几 KB，而且只有列出它的应用才会打包它：应用只包含用户使用的语言，而不是 Native Shell 支持的所有语言。

有数据的语言：德语（`de`）、西班牙语（`es`）、法语（`fr`）、印地语（`hi`）、印尼语（`id`）、意大利语（`it`）、日语（`ja`）、韩语（`ko`）、荷兰语（`nl`）、波兰语（`pl`）、葡萄牙语（`pt`）、俄语（`ru`）、瑞典语（`sv`）、泰语（`th`）、土耳其语（`tr`）、越南语（`vi`）和中文（`zh`，简体和繁体），以及它们的地区变体（`de-CH`、`es-MX`、`pt-PT`、`zh-TW`……）。新增一种语言只需要在 Native Shell 的 `nsh-i18n` crate 中加一个数据文件（`crates/nsh-i18n/data/`），和其他改动一样提交贡献即可。

在 Web 上，浏览器自己的 `Intl` 可以格式化所有语言，与 `locales` 列出什么无关。

`nsh package` 还会把这些语言声明给 iOS 和 macOS（bundle 的 `CFBundleLocalizations`），让应用里的系统 UI（例如选中文字后的编辑菜单）和应用显示同一种语言：系统语言为中文的用户，在带有中文的应用里看到 `全选`，在没有中文的应用里看到英文。

`Intl` 能格式化什么，见[数字与日期格式化](https://nsh.dev/zh-CN/modules/globals.md#数字与日期格式化)；用户的其他设置（时区、12 或 24 小时制、每周第一天和单位）见[设备与环境](https://nsh.dev/zh-CN/docs/device-and-environment.md#语言与地区设置)。

## Rust 应用

Rust 应用的 `locales/` 放在 `Cargo.toml` 旁边；`nsh run`、`nsh test` 和 `nsh package` 会读取它，并把翻译交给 root（`Root::translations`），所以 `t!` 无需任何设置即可使用。只用 Cargo 构建的 Rust 应用（例如嵌入这个 crate 的宿主）自己传入翻译：

```rust
use nsh_core::{element::Root, i18n::Translations};

let translations = Translations::from_json(include_str!("../translations.json")).unwrap();
let root = Root::of::<App>().translations(translations);
```

这里的 `t!` 是 Native Shell 的，不是 rust-i18n 的：它读取所在 surface 的语言（每个 surface 各有一个），显示它的视图会跟随 `set_locale`。它的写法与 rust-i18n 相同：`t!("key")`、`t!("key", name = value)`、`t!("key", locale = "ja")`。

TSX 应用中的 Rust 组件也用同一个 `t!` 翻译：它们共享应用的 `locales/`。

## 测试

`nsh test` 默认以 `en-US` 打开应用，除非 `open` 的 `locale` 另行指定；测试其他语言时用那种语言打开，或者像应用一样调用 `setLocale`：

```tsx
import { open, test } from "nsh/test";

test("the greeting is translated", async () => {
  const app = await open("./App.tsx", { locale: "zh-CN" });
  await app.waitFor("text:你好");
});
```

## 检查清单

- 用户读到的每个字符串都来自 `t`（标签、占位符、无障碍标签、提示框的按钮），组件里不写字面量。
- key 按文本的用途命名（`checkout.pay`、`errors.network`），而不是按英文文本命名，这样修改英文时无需重命名 key。
- 完整的句子放在一个 key 中，用 `%{name}` 传参数，不要用几个 key 拼接：不同语言的语序不同。
- 数字、价格和日期都通过 `Intl` / `toLocaleString` 写出，不要用 `+` 和 `toFixed` 拼接。
- 布局要留出余量：德语和芬兰语文本比英语长得多，中文和日文则更短。
- 当用户格式化所用的语言与翻译的语言不同时，在 `nsh.json` 的 `locales` 中列出它们。
