NocoBase 插件开发:客户端 I18n 国际化完整指南
2026/9/16 21:02:17 网站建设 项目流程

NocoBase 插件开发:客户端 I18n 国际化完整指南

【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase

在 NocoBase 插件开发中,国际化(i18n)是让插件同时服务中英文等多语言用户的基础能力。NocoBase 在前后端提供了统一的国际化机制:后端通过ctx.i18nctx.tplugin.t()完成服务端文案翻译,前端则基于 i18next 生态提供useTranslationtExpr等 API,让插件作者只需维护一份 JSON 语言文件,即可让界面、Schema 表达式和服务端返回的文案全部实现多语言。

本文将基于客户端 i18n 文档的骨架,结合@nocobase/client@nocobase/server@nocobase/flow-engine的真实源码,完整讲解语言文件的组织方式、JSON 词条格式、客户端与服务端翻译 API 的用法,以及app:getLang接口背后的运行时资源下发机制,帮助你写出的插件从第一天起就具备完整的多语言能力。

插件多语言文件管理

语言文件存放目录

插件的多语言文件统一存放在插件包的src/locale目录下,按语言(locale)命名文件,例如:

|- /plugin-hello |- /src |- /locale |- en-US.json # 英文语言 |- zh-CN.json # 中文语言

该约定与仓库内正式插件一致。以用户管理插件为例,其语言文件就位于 packages/plugins/@nocobase/plugin-users/src/locale 目录下,包含en-US.jsonzh-CN.jsonde-DE.jsones-ES.jsonfr-FR.jsonja-JP.json等多个语言文件,说明该目录机制是 NocoBase 所有插件通用的标准结构。

JSON 词条格式与插值

每个语言文件导出一个 JSON 对象,包含该语言的所有翻译词条。key 是翻译键,value 是翻译结果,例如zh-CN.json

{ "Hello": "你好", "World": "世界", "Enter your name": "请输入你的名字", "Your name is {{name}}": "你的名字是 {{name}}" }

对应的en-US.json

{ "Hello": "Hello", "World": "World", "Enter your name": "Enter your name", "Your name is {{name}}": "Your name is {{name}}" }

其中{{name}}是 i18next 标准的插值语法(interpolation),翻译函数在调用时会用传入的 options 替换占位符,例如t('Your name is {{name}}', { name: 'NocoBase' })会得到你的名字是 NocoBase

需要注意的是,由于客户端的 i18n 实例在初始化时关闭了 key 分隔符(详见下文"客户端 i18n 实例的初始化"),翻译键中允许直接包含空格、&等字符而不必担心被当作命名空间或层级分隔符解析。这一点在真实插件的语言文件中有大量体现,例如 plugin-users 的 en-US.json 中就有"Are you sure you want to delete selected users?""Users & Permissions"这样包含空格和&的完整句子作为键名。

新增语言文件后需要重启应用

初次添加语言文件,需要重启应用才能生效。可以通过接口校验翻译词条是否生效:http://localhost:13000/api/app:getLang?locale=zh-CN

之所以需要重启,是因为服务端对语言资源做了缓存。在 packages/core/server/src/locale/locale.ts 中,Locale管理器通过resourceCached集合记录已加载过的语言,并使用wrapCache将每个语言的资源包缓存起来(getCacheResourcesloadResourcesByLang方法);getBuiltInResources则遍历app.pm.getPlugins()中已加载的插件并读取其语言资源。因此新增加的语言文件在应用启动后才会被扫描并写入缓存,重启应用(或执行资源重载reload())是让新词条生效的最直接方式。

客户端 i18n 相关 API

客户端 i18n 实例的初始化

在深入各 API 之前,先看客户端 i18n 实例是如何创建的。packages/core/client/src/i18n/i18n.ts 中通过 i18next 创建了全局单例:

import i18next from 'i18next'; import { initReactI18next } from 'react-i18next'; import locale from '../locale'; export const i18n = i18next.createInstance(); const resources = {}; Object.keys(locale).forEach((lang) => { resources[lang] = locale[lang].resources; }); i18n .use(initReactI18next) .init({ lng: 'en-US', defaultNS: 'client', resources: {}, keySeparator: false, nsSeparator: false, });

几个关键点:

  • 基于i18next.createInstance()创建独立实例,避免污染全局 i18next;
  • 接入initReactI18next,使 React 组件可以通过useTranslation等 Hook 使用;
  • 默认语言为en-US,默认命名空间(namespace)为client
  • keySeparator: falsensSeparator: false表示翻译键不使用.:作为层级/命名空间分隔符,翻译键可以是任意完整句子。

另外,文件中导出的tval()函数已被标记为@deprecated,建议改用@nocobase/utils/client中的tval(见 packages/core/utils/src/i18n.ts),而后者同样被标记为 deprecated,建议改用@nocobase/flow-enginetExpr。下文会详细说明tExpr的用途。

useTranslation(ns)

useTranslation是客户端 React 组件内使用最频繁的翻译 Hook,它直接来自react-i18next。用法:

import { useTranslation } from 'react-i18next'; export const MyComponent = () => { const { t } = useTranslation(); // 可传入命名空间,如 useTranslation('plugin-hello') return <div>{t('Hello')}</div>; };

useTranslation接受可选的命名空间参数ns。不传时使用defaultNS: 'client';如果插件使用了独立的命名空间(通常与插件包名一致),则应显式传入。

仓库内的正式插件大量使用该模式。以 plugin-users 的 ChangePassword.tsx 为例:

import { useTranslation } from 'react-i18next'; export const ChangePassword = () => { const { t } = useTranslation(); // ... return <div>{t('Allow change password')}</div>; };

在 block-provider/hooks/index.ts 等核心模块中,同样通过const { t } = useTranslation()获取翻译函数后包装按钮文案、提示信息等。

需要特别强调的是:useTranslation返回的t函数在组件首次渲染时消费的词条来自客户端初始 resources(当前默认是空的),真正多语言资源是应用启动后由服务端通过app:getLang接口下发、再通过i18n.addResources()注入客户端 i18n 实例的(详见下文"app:getLang 与运行时资源下发")。因此,只要资源加载完成,t()就能正确返回对应语言文案,并随i18n.changeLanguage()的调用自动更新。

tExpr(text)

tExpr是 NocoBase 用于Schema 表达式翻译的函数,它的作用不是立即翻译,而是生成一段{{t(...)}}模板字符串,交给 Schema 渲染引擎在渲染时延迟求值。其实现位于 packages/core/flow-engine/src/utils/translation.ts:

export function tExpr(text: TFuncKey | TFuncKey[], options?: TOptions) { if (options) { return `{{t(${JSON.stringify(text)}, ${JSON.stringify(options)})}}`; } return `{{t(${JSON.stringify(text)})}}`; }

例如:

import { tExpr } from '@nocobase/flow-engine'; // 无 options tExpr('Hello'); // => "{{t(\"Hello\")}}" // 带插值 options tExpr('Your name is {{name}}', { name: 'NocoBase' }); // => "{{t(\"Your name is {{name}}\", {\"name\":\"NocoBase\"})}}"

返回的模板字符串可以直接写入 UI Schema(如按钮标题、区块标题、字段 label 等),当 Schema 被渲染时,其中的{{t(...)}}表达式会由翻译上下文求值,从而实现"Schema 中的文案也支持多语言"。

同文件中还导出了两个辅助函数:

  • getT(model):从FlowModel实例获取翻译函数,自动使用flow-engine命名空间(ns: ['flow-engine', 'client']nsMode: 'fallback'),在流程引擎相关场景中优先使用;
  • escapeT(text, options):已被标记为@deprecated,等价于tExpr

useT()

文档中列出的useT()是客户端获取翻译函数的另一种方式(当前仓库中部分模块仍在使用的快捷封装)。如果你的插件需要统一入口获取t函数,可以使用useT()代替直接解构useTranslation(),它与useTranslation共享同一个 i18n 实例与命名空间配置,行为一致,可根据团队代码风格二选一。

withTranslation(ns)

withTranslationreact-i18next提供的高阶组件(HOC)形式,适用于函数组件之外或需要把t注入 props 的场景:

import { withTranslation } from 'react-i18next'; class LegacyComponent extends React.Component { render() { const { t } = this.props; return <div>{t('Hello')}</div>; } } export default withTranslation()(LegacyComponent);

参数nsuseTranslation(ns)相同,用于指定命名空间。在新代码中更推荐使用useTranslationHook,HOC 主要用于兼容旧组件。

服务端 i18n API(配套)

虽然本文聚焦客户端国际化,但客户端多语言资源恰恰由服务端下发,因此服务端的三个 API 是完整国际化链路中不可或缺的一环。

ctx.i18n 与 ctx.t(text, options)

服务端中间件 packages/core/server/src/middlewares/i18n.ts 在每个请求进入时根据请求上下文确定语言,并把翻译能力挂载到 Koa 的ctx上:

export async function i18n(ctx, next) { ctx.getCurrentLocale = () => { const lng = ctx.get('X-Locale') || (ctx.request.query.locale as string) || ctx.app.i18n.language || ctx.acceptsLanguages().shift() || 'en-US'; return lng; }; const lng = ctx.getCurrentLocale(); const localeManager = ctx.app.localeManager as Locale; const i18n = await localeManager.getI18nInstance(lng); ctx.i18n = i18n; ctx.t = i18n.t.bind(i18n); if (lng !== '*' && lng) { await i18n.changeLanguage(lng); await localeManager.loadResourcesByLang(lng); } await next(); }

可以看到语言解析的优先级依次为:

  1. 请求头X-Locale
  2. URL 查询参数locale
  3. 应用默认语言app.i18n.language
  4. Accept-Language请求头;
  5. 兜底en-US

在请求处理函数中即可使用:

// 某个 action 或中间件内 ctx.body = { message: ctx.t('Hello'), // 当前请求语言下的翻译 hello: ctx.i18n.t('Hello'), // 等价写法 };

plugin.t()

插件类内部提供了实例方法plugin.t(),自动把命名空间绑定为插件自身的packageName,实现见 packages/core/server/src/plugin.ts:

t(text: TFuncKey | TFuncKey[], options: TOptions = {}) { return this.app.i18n.t(text, { ns: this.options['packageName'], ...(options as any) }); }

也就是说,在插件的服务端代码中调用this.t('Hello')时,会自动从该插件包名对应的命名空间查找词条,无需手动传入ns。这要求插件的语言文件 key 与该插件包名命名空间对应。

app:getLang 接口与运行时资源下发

接口定义

app:getLang是 NocoBase 提供的语言资源查询接口,也是文档中校验翻译词条是否生效的入口。接口定义见 packages/core/server/src/swagger/app.ts:

  • 请求方式:GET /api/app:getLang
  • 查询参数:
    • locale(可选):请求的语言,服务端会校验其是否为已启用语言;
    • ns(可选):逗号分隔的资源命名空间列表,用于只返回指定命名空间的资源;
  • 响应:返回服务端当前语言及对应的多语言资源。

文档给出的校验方式即:

http://localhost:13000/api/app:getLang?locale=zh-CN

请求后返回的resources中应包含已加载插件(含你新增词条的插件)在该语言下的翻译资源。

客户端如何消费接口返回的资源

客户端在应用启动时会主动请求该接口,把服务端聚合后的语言资源注入本地 i18n 实例。实现位于 packages/core/client/src/antd-config-provider/index.tsx:

export function AntdConfigProvider(props) { const api = useAPIClient(); const { i18n } = useTranslation(); const { data, loading } = useRequest({ url: 'app:getLang', params: { locale: api.auth.locale }, }, { onSuccess(data) { const locale = api.auth.locale; if (data?.data?.lang && !locale) { api.auth.setLocale(data?.data?.lang); i18n.changeLanguage(data?.data?.lang); } Object.keys(data?.data?.resources || {}).forEach((key) => { i18n.addResources(data?.data?.lang, key, data?.data?.resources[key] || {}); }); loadConstrueLocale(data?.data); dayjs.locale(data?.data?.moment); window['cronLocale'] = data?.data?.cron; }, }); }

该组件完成了几件关键工作:

  1. 以当前登录用户的auth.locale为参数请求app:getLang
  2. 若用户尚未显式设置语言,则把服务端返回的语言设为应用语言,并调用i18n.changeLanguage()切换客户端语言;
  3. 遍历服务端返回的resources(按命名空间组织),逐个调用i18n.addResources(lang, ns, resources)注入客户端 i18n 实例——这就是你写的插件语言文件最终"到达"浏览器并可供useTranslation消费的完整链路;
  4. 同步设置 dayjs 与 cron 组件的本地化配置。

语言切换的底层支持

客户端 i18n 的语言切换能力来自 packages/core/client/src/i18n/i18n.ts 中创建的 i18next 实例;而服务端负责聚合各插件资源的是 packages/core/server/src/locale/locale.ts 中的Locale管理器。它提供:

  • loadResourcesByLang(lang):按需加载某个语言的资源并写入缓存;
  • getBuiltInResources(lang):遍历已注册插件读取内置资源;
  • reload()/reset():清空缓存并广播localeManagerreload 事件,用于语言文件变更后的资源刷新;
  • syncSources(ctx, types):合并各LocaleSource的同步资源。

此外,packages/core/client/src/locale/index.ts 维护了语言代码与 dayjs locale 的映射表(dayjsLocale)以及各语言的展示名(如en-US: Englishzh-CN: 简体中文),供语言切换组件(见 packages/core/client/src/i18n/SwitchLanguage.tsx)与日期组件共用。

最佳实践小结

基于文档约定与源码实现,在 NocoBase 插件中落地国际化的推荐做法如下:

  1. 语言文件统一放src/locale目录,按en-US.jsonzh-CN.json等语言代码命名,键值对采用"完整句子作 key"的风格,并利用{{name}}插值处理动态内容;
  2. React 组件内使用useTranslation()获取t函数翻译界面文案;旧代码可用withTranslation(ns)HOC 或useT()
  3. Schema 中的文案使用tExpr()生成{{t(...)}}模板字符串,实现 Schema 级延迟翻译;
  4. 服务端文案使用ctx.t()(请求上下文)或this.t()(插件类内,自动绑定包名命名空间);
  5. 新增语言文件后重启应用(生产环境),再用GET /api/app:getLang?locale=zh-CN校验词条是否已被服务端聚合、能够下发到客户端;
  6. 熟悉 i18n.ts 中keySeparator: falsensSeparator: false的配置含义,避免在翻译键中使用会被误解析的分隔符。

只要遵循以上目录约定与 API 用法,你的插件即可无缝接入 NocoBase 的全局语言切换机制,让界面、Schema 与服务端返回的文案在en-USzh-CN等语言之间自由切换。

【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询