多语言

NocoBase 3 支持多语言,本章节主要介绍如何编写可翻译的文案、组织翻译文件、覆盖插件文案等。如果你只想切换语言或者加一种新语言,看内置能力 / 多语言就够了。

switch language

多语言翻译的实现

在界面上,我们看到的文字、语句,如果要让它能够被翻译,就需要在翻译文件里写上它,并在组件里用 key 引用它。

可以直接告诉 Coding Agent:

订单列表页的标题和空状态文案改成走翻译,中英文都要有。

它会把文案加进 client/locales/en-US.ts 和 zh-CN.ts,再改组件。

比如文案如下:

// client/locales/en-US.ts
orders: {
  title: 'Orders',
  empty: 'No orders yet',
},
// client/locales/zh-CN.ts
orders: {
  title: '订单',
  empty: '还没有订单',
},

orders.title 这样的名字就是 key,两个文件里用同一个 key 对应同一句话的不同语言。

改完的组件是这样:

import { useTranslation } from '@nocobase/i18n/client';

export default function OrdersPage() {
  const { t } = useTranslation();

  return <h1>{t('orders.title')}</h1>;
}

需要拼接变量时,可以传递变量。文案里用 {{变量名}} 占位:

// client/locales/zh-CN.ts
orders: {
  count: '共 {{count}} 个订单',
},
t('orders.count', { count: orders.length }); // 共 3 个订单 (假设 orders.length 为 3)

翻译文件在哪

应用自己的翻译放在 client/locales/:

文件作用
en-US.ts定下有哪些 key,以及英文文案
zh-CN.ts中文文案,结构照着 en-US.ts
index.ts登记有哪些语言,每种语言一个动态导入

en-US.ts 是基准。它导出一个类型,其他语言用这个类型标注,所以漏翻一个 key 是编译错误,pnpm typecheck 会直接报出来:

// client/locales/en-US.ts
import type { LocaleResource } from '@nocobase/i18n';

const enUS = {
  orders: {
    title: 'Orders',
    empty: 'No orders yet',
  },
};

export type AppResource = LocaleResource<typeof enUS>;
export default enUS;
// client/locales/zh-CN.ts
import type { AppResource } from './en-US.js';

const zhCN: AppResource = {
  orders: {
    title: '订单',
    empty: '还没有订单',
  },
};

export default zhCN;

key 可以嵌套,用点号访问:t('orders.title')。

服务端也有一份 server/locales/,结构一样。接口报错、邮件正文这类由服务端产生的文字写在那里;纯界面文案不需要。

改插件里的文案

插件的措辞不一定符合你的业务习惯,此时可以在应用的翻译文件里覆盖它。

交给 Coding Agent 时,直接指界面上的那句话就行:

工作流插件在左侧菜单里显示为「工作流」,我们公司内部叫「审批流程」。请改成我们的叫法,中英文都要。

说清楚两件事:界面上的哪个位置、想改成什么。位置要说,因为同一个词在插件里可能对应好几个 key——菜单里的和页面标题里的是两处,不说明白容易改错或者漏改。

Agent 会在你的翻译文件里加一个 overrides 块,按插件包名分组:

const zhCN: AppResource = {
  // 你自己的文案……

  overrides: {
    '@nocobase/app-plugin-workflow': {
      nav: { title: '审批流程' },
    },
  },
};

覆盖在所有插件加载完之后才应用,所以不管加载顺序如何,你的措辞总是赢。

覆盖适合的是「叫法不一样」——同一个东西,插件叫「工作流」,你们公司叫「审批流程」。如果是插件压根没翻译某种语言,那应该去给插件提 issue,而不是在这里补齐。

服务端和后台任务的语言

界面的语言跟着浏览器走,服务端的不是。

接口报错跟着请求走。 在路由里拿翻译器就行,语言已经由中间件解析好了:

import { getRequestTranslator } from '@nocobase/i18n/server';

const t = getRequestTranslator(context);
t('errors.orderNotFound');

后台任务没有请求可依据,语言必须自己定。 定时任务、一次性后台任务、Webhook 都属于这种情况。这里有一条容易写错的规则:

注意

发出去的内容,语言跟着收件人走,不是跟着触发这件事的人走。

A 提交申请触发了给 B 的通知,这封通知该用 B 的语言。按 A 的语言发,或者按服务端默认语言发,都是错的。

// 先加载收件人的语言,再翻译
await i18n.ensureLocaleLoaded(recipient.locale);
const t = i18n.getFixedT(NS, recipient.locale);
t('notification.approved', { orderNo: order.no });

漏掉 ensureLocaleLoaded 不会报错,只会静默回落到默认语言——测试的时候不一定发现得了,所以写的时候就要记得。

一次给多个人发消息时,每个人的语言要分别处理,不能一批人共用一种语言。

加一种新语言

在 client/locales/ 加一个语言文件,并在 index.ts 里登记;服务端有自己的文案时,server/locales/ 也要加。应用支持哪些语言就是由这些文件决定的,没有别的清单需要同步。

完整步骤和给 Coding Agent 的描述见内置能力 / 多语言。

怎么确认改对了

pnpm typecheck                  # 漏翻的 key 会在这里失败
pnpm nocobase locales check     # 前后端声明的语言是否一致

常见问题

组件里的文案改了,页面上没变化

先确认改的是不是当前语言的文件。另外检查这段文字是不是属于某个插件——插件的文案要用 overrides 覆盖,改自己的 key 不会生效。

某个组件在测试里渲染时报错或显示 key

这个组件被单独渲染时没有翻译环境。给 t() 加一个 defaultValue 就能保证它始终可读:

t('orders.title', { defaultValue: 'Orders' });

插件导出的组件,翻译错了

比如插件 @acme/app-plugin-crm 导出的组件,有个翻译的 key 是 orders.title。而应用本身也有一个同名的 key。翻译的归属跟着渲染位置走,不跟着代码归属走。如果不做显式处理的话,插件的翻译可能会被应用的翻译覆盖。

这种组件需要显式指明自己的命名空间——也就是插件的包名:

// 写法一:取翻译器时指明
const { t } = useTranslation('@acme/app-plugin-crm');
// 写法二:把整个组件包起来,组件内部照常写 useTranslation()
import { withNamespace } from '@nocobase/i18n/client';

export default withNamespace('@acme/app-plugin-crm', CustomerCard);

判断方法:这个组件会不会在插件自己的页面之外被渲染?会的话就要指明。

相关链接