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.i18n、ctx.t、plugin.t()完成服务端文案翻译,前端则基于 i18next 生态提供useTranslation、tExpr等 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.json、zh-CN.json、de-DE.json、es-ES.json、fr-FR.json、ja-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将每个语言的资源包缓存起来(getCacheResources、loadResourcesByLang方法);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: false与nsSeparator: false表示翻译键不使用.和:作为层级/命名空间分隔符,翻译键可以是任意完整句子。
另外,文件中导出的tval()函数已被标记为@deprecated,建议改用@nocobase/utils/client中的tval(见 packages/core/utils/src/i18n.ts),而后者同样被标记为 deprecated,建议改用@nocobase/flow-engine的tExpr。下文会详细说明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)
withTranslation是react-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);参数ns与useTranslation(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(); }可以看到语言解析的优先级依次为:
- 请求头
X-Locale; - URL 查询参数
locale; - 应用默认语言
app.i18n.language; Accept-Language请求头;- 兜底
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; }, }); }该组件完成了几件关键工作:
- 以当前登录用户的
auth.locale为参数请求app:getLang; - 若用户尚未显式设置语言,则把服务端返回的语言设为应用语言,并调用
i18n.changeLanguage()切换客户端语言; - 遍历服务端返回的
resources(按命名空间组织),逐个调用i18n.addResources(lang, ns, resources)注入客户端 i18n 实例——这就是你写的插件语言文件最终"到达"浏览器并可供useTranslation消费的完整链路; - 同步设置 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: English、zh-CN: 简体中文),供语言切换组件(见 packages/core/client/src/i18n/SwitchLanguage.tsx)与日期组件共用。
最佳实践小结
基于文档约定与源码实现,在 NocoBase 插件中落地国际化的推荐做法如下:
- 语言文件统一放
src/locale目录,按en-US.json、zh-CN.json等语言代码命名,键值对采用"完整句子作 key"的风格,并利用{{name}}插值处理动态内容; - React 组件内使用
useTranslation()获取t函数翻译界面文案;旧代码可用withTranslation(ns)HOC 或useT(); - Schema 中的文案使用
tExpr()生成{{t(...)}}模板字符串,实现 Schema 级延迟翻译; - 服务端文案使用
ctx.t()(请求上下文)或this.t()(插件类内,自动绑定包名命名空间); - 新增语言文件后重启应用(生产环境),再用
GET /api/app:getLang?locale=zh-CN校验词条是否已被服务端聚合、能够下发到客户端; - 熟悉 i18n.ts 中
keySeparator: false、nsSeparator: false的配置含义,避免在翻译键中使用会被误解析的分隔符。
只要遵循以上目录约定与 API 用法,你的插件即可无缝接入 NocoBase 的全局语言切换机制,让界面、Schema 与服务端返回的文案在en-US、zh-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),仅供参考