Rocket.Chat 国际化(i18n)工程实践指南:翻译键的存储、命名、插值与自动校验

发布时间:2026/9/9 13:44:20

Rocket.Chat 国际化(i18n)工程实践指南:翻译键的存储、命名、插值与自动校验 Rocket.Chat 国际化i18n工程实践指南翻译键的存储、命名、插值与自动校验【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.ChatRocket.Chat 的翻译体系以rocket.chat/i18n包为核心为 Web 客户端、Meteor 服务器以及omnichannel-transcript等微服务提供共享的语言资源。本文是仓库内 docs/i18n.md 的深度展开先梳理 68 个语言文件如何组织、为何en是唯一事实来源再逐一讲解翻译键的命名规范、五大命名空间、i18next 插值与复数规则随后结合源码剖析三个运行时如何初始化、服务端为何必须显式传lng最后介绍内置 i18n 代码检查器所强制的全部规则。读者读完既能写出一条符合规范的翻译键也能解释构建时类型生成、lint:fix自动排序等底层机制。客户端特有的Trans组件与转义约定不在本文范围详见 docs/frontend/i18n.md。翻译资源在哪里、如何组织所有翻译都是扁平 JSON 文件放在packages/i18n/src/locales/目录下一个语言一个文件按language.i18n.json命名。当前仓库内共有 68 个语言文件从af.i18n.json南非荷兰语到zh.i18n.json简体中文其间覆盖ar、de、fr、ja、ko、pt-BR、ru、tr等主流语言以及zh-HK、zh-TW等地区变体。这些文件描述的是键key与字符串value的映射而非文档中的占位符介绍因此任何消费rocket.chat/i18n的运行时——无论是浏览器还是 Node 服务——看到的都是同一份资源集合。en.i18n.json是唯一的基准语言en.i18n.json是base language也是新增功能时唯一允许手工编辑的文件。围绕它形成三条硬性约定缺键回退任何 locale 缺失的键都会回退到en每个运行时的初始化参数都带fallbackLng: en。多余键即陈旧某个 locale 中存在、但en中不存在的键会被判定为过时残留由检查器的wipe-extra-keys任务删除。顺序继承其他 67 个语言文件的键顺序完全由en推导而来所以重排en里的键就等于重排所有语言文件。这也意味着为新功能添加翻译时只需要在en.i18n.json里追加键。其他语言的翻译由外部流程单独提供不要为自己的功能手写其他语言的翻译。键的联合类型是从en构建时生成的仓库中packages/i18n/src/resources.ts是一个刻意保留的假文件dummy内容只是core.key1、onboarding.key1之类的占位联合类型。真正的键联合类型RocketchatI18nKeys是在构建阶段由脚本根据en.i18n.json实时生成到dist/resources.d.ts中的。在src/scripts/build.mts中可以看到这段逻辑遍历 base language 的每个键输出一个RocketchatI18n接口再取keyof得到RocketchatI18nKeys。需要特别注意的是拼错的键不是编译错误。虽然packages/i18n/src/index.ts通过模块增强给 i18next 的TFunction追加了以RocketchatI18nKeys为参数类型的重载但这是添加过载而非收窄签名——文档明确指出这是为类型检查性能而刻意避免的收窄。因此验证键名要靠 grep 基准语言文件而不是编译器。刚添加的键在重新构建包之前也不会进入生成的类型yarn workspace rocket.chat/i18n build该构建还会把每个 locale 逐一写入dist/resources/并生成dist/languages.js语言清单文件。翻译键的命名规范代码库主导的命名约定是大写下划线式Capitalized snake case即Sentence_case_with_underscores这也是Sentence_case_with_underscores被i18next识别为普通字符串的示例形式{ Cam_on: Camera on, Delete_room: Delete room, You_are_offline_please_reconnect: You are offline, please reconnect }用含义命名而不是用字面值或渲染位置命名命名的第一原则是让键描述语义而不是描述当前文案更不是描述它渲染在哪个按钮上。Delete_room在文案从 Delete room 改成 Remove channel 时依然成立而Delete_room_red_button这样的键会随着 UI 细节变化立刻失效。这类键的前三个示例在en.i18n.json中都能直接检索到如Cam_on: Camera on。第二原则是含义完全相同时复用已有键。但仅仅因为英文恰好相同就复用是危险的——按上下文变形的语言如需要性、数、格配合的语种会需要分开的键。更关键的是事后拆开一个被共享的键对所有语言文件都是一次破坏性变更代价极高。命名空间恰好五个键可以被最多五种命名空间之一作为前缀以点号分隔i18next 的nsSeparator: .core默认 ·onboarding·registration·cloud·subscription{ onboarding.component.form.action.next: Next, subscription.callout.title.limitsReached: Limits reached }无前缀的键自动落在core命名空间。这套集合定义在packages/i18n/src/index.tsnamespacesMap记录了这五个命名空间defaultTranslationNamespace为core。命名空间的目的是让客户端按需加载资源子集例如只用core和onboarding而不是充当一般性的分组工具——从源码extractTranslationNamespaces的实现看它只是按前缀把扁平键拆回五个对象。还要注意命名空间内部键的风格差异core里用大写下划线而命名空间内部如onboarding、subscription的键沿用现有条目使用小写点号路径onboarding.component.form.action.next。插值Interpolation运行时文案需要动态值时使用 i18next 的命名占位符{{likeThis}}占位符名称采用 camelCase{ Room_removed: Room {{roomName}} removed from ABAC management }三种占位符形态与三种废弃形态基础语言里至今还残存两类废弃写法新增键时严禁模仿形态状态{{name}}✅ 正确应使用__name__❌ 已废弃由检查器自动改写replace-2-underscores%s❌ 传统 sprintf基于位置传参属历史遗留sprintf形式目前在运行时仍然有效无论是客户端还是 Meteor 服务器都安装并启用了i18next-sprintf-postprocessor通过packages/i18n/src/index.ts导出的addSprinfToI18n把t包裹起来——当参数是一个数组时它会把t(key, replaces)转成t(key, { postProcess: sprintf, sprintf: replaces })。但它是位置式的翻译者一旦调整句子语序参数就会悄悄错位。因此不要新增任何%s键。当前 base locale 中仍可直接 grep 到 6 处%s由find-sprintf-params任务持续标记为 backlog。另外部分键名也内嵌了旧标记例如Added__username__to_team、__count__result_found两者在en.i18n.json中都能检索到。这仅是命名上的历史遗留其值使用的是{{...}}占位符语义正确。新键不要模仿这种命名。严禁用碎片拼接句子词序并不是普适的而翻译者只能看到你拼出来的碎片。下面这种写法是错误的${t(Deleted)} ${count} ${t(messages)};正确做法是让一个键承载整个句子t(Messages_deleted, { count });携带计数的键还需要配套复数形式因此Messages_deleted在语言文件里应定义为一个复数对象见下文复数化。需要区分的是用「标签键 运行时值」组合出Label: value这样的键值对是允许的把一段散文拆到多个键里才是不允许的。格式化器Formatters占位符后加逗号即可挂载 i18next 格式化器。所有运行时都内置基于Intl的内建格式化器{ Exceeded_limits: Your workspace exceeded the {{val, list}} license limits., Seats_used: {{count, number}} seats used }项目里还有一个自定义格式化器capitalize但只在客户端注册见apps/meteor/client/providers/TranslationProvider.tsx。它存在的意义是某些语言需要不同的词序翻译者可以在翻译文件内部把落在句首的那个词首字母大写而无需改代码。当前en中没有键使用它。注意不要在一个服务器也会渲染的键里用它——服务器没有注册该格式化器值会原样透传、不生效。复数化Pluralization需要随数量变化文案时把键定义成一个复数形式对象并在调用时传入count由 i18next 依据该语言在 CLDR 中的复数规则挑选形态{ message_counter: { one: {{count}} message, other: {{count}} messages } }对英语而言只有one和other两种其他语言则不同——例如阿拉伯语有六种复数形态。这正是不能手写判断的原因count 1 ? t(message_counter_one) : t(message_counter_other);上面是错误示范。正确写法是把决策交给 i18nextt(message_counter, { count });特殊形态zeroi18next 还支持一个特殊的zero形态用于空状态文案读起来比 0 items 更自然的场景{ Calls_in_queue: { zero: Queue is empty, one: {{count}} call in queue, other: {{count}} calls in queue } }但只有当措辞确实不同时才加zero——对英语而言 0 已经能被other覆盖没必要重复定义。复数形态是按语言逐一校验的不属于该语言 CLDR 形态集的形态会被wipe-invalid-plurals剥离合法集合是zero、one、two、few、many、other其中zero为 i18next 特例而某个 locale 缺少en已定义的形态则会被find-missing-plurals报告。相关实现可以分别在src/scripts/check.mts与src/scripts/common.mts后者通过 i18next 的pluralResolver取各语言复数后缀中看到。服务端使用三个运行时与必须传 lng客户端、Meteor 服务器与独立服务共享同一份资源但初始化方式不同运行时初始化客户端apps/meteor/client/providers/TranslationProvider.tsx——en随包静态内置非英语活动语言通过 HTTP 按需加载Meteor 服务器apps/meteor/server/lib/i18n.ts—— 启动即加载全部 68 个语言常驻内存omnichannel-transcript服务ee/apps/omnichannel-transcript/src/i18n.ts—— 与服务器相同的全量预载形态在服务器代码里应当导入共享实例而不是自己 new 一个import { i18n } from ../../app/utils/lib/i18n;服务端每次调用都要显式传lng文档直言这其实暴露了服务端 i18n 设计上的一个缺口。服务端实例以lng: en初始化且没有任何按请求取语言的上下文。漏传lng不会报错——它只是静默地返回英语。在约 200 个服务端调用点中只有大约三分之一传了lng所以周边代码不能作为可靠参照。错误示范——无论接收者是谁都返回英语i18n.t(Username_and_message_must_not_be_empty);正确示范i18n.t(Username_and_message_must_not_be_empty, { lng: user.language || settings.get(Language) || en });这条回退链——接收者的语言 → 工作区Language设置 →en——是既定的通行写法目前还没有共享的辅助函数所以每个调用点都是这么显式写出来的。选语言时遵循一条准则取阅读这段字符串的人的语言而不总是当前操作用户的语言。通知、邮件、导出文件都是渲染给接收者看的。不要在 API 边界翻译更优的做法是接口只返回键由客户端负责翻译——这也是绝大多数接口已经在做的。原因是客户端天然知道读者的语言而服务端必须被告知。因此新接口应优先返回翻译键而不是翻译后的字符串。独立的子系统packages/livechat要注意packages/livechat拥有自己的一套翻译在src/i18n/下与rocket.chat/i18n完全无关。这套体系有自己的特点语言文件是普通的language.json统一嵌套在单个translation根键下键采用lower_snake_case复数用_one/_other键后缀而非嵌套对象表达。本文描述的所有规则——包括代码检查器——对 livechat 都不适用。反过来也一样不要在这两套体系之间互相照搬约定。代码检查器linter强制了什么在packages/i18n目录下执行yarn workspace rocket.chat/i18n lint会运行 ESLint 加上src/scripts/check.mts中实现的自定义检查任务。绝大多数问题都可以用lint:fix自动修复yarn workspace rocket.chat/i18n lint:fix检查任务一览任务规则sort-base-keysen的键按字母序排序大小写不敏感sort-keys每个 locale 遵循en的键顺序wipe-extra-keys语言文件不得包含en中没有的键wipe-invalid-plurals复数形态对该语言必须合法外加zerofind-missing-plurals语言必须定义en定义的全部复数形态replace-2-underscores__name__→{{name}}missing-placeholders/extra-placeholders占位符必须与en完全一致find-duplicate-keysJSON 中不得出现重复键trim-eof文件末尾不得有尾随空白排序的两处细节与执行顺序sort-base-keys必须先于sort-keys运行因为其他所有语言文件的顺序都由en推导而来。新增的键放在en的任何位置都可以——lint:fix会自动把它挪到正确位置并同步重排其他 67 个文件。但有两处排序细节不是字母序而是 JavaScript 本身强制的对应实现见src/scripts/check.mts的isIntegerLikeKey与compareBaseKeys整数样式的键排最前如500因为JSON.parse无论文件里怎么写都会把这类键提升到对象最前面排序必须与实际 parse 结果一致才能让 lint 通过仅大小写不同的键如Private/private当前有 69 对在大小写不敏感比较下会打平需要再用纯码点比较打破平局保证顺序唯一且规范。find-sprintf-params定义了但不进默认运行有一个任务已定义却被排除在默认运行之外因此不会让构建失败——find-sprintf-params它负责标记en中残留的%s当前可实测为 6 处。它被排除是因为存在历史 backlog不应借功能 PR 顺手顺手清理它。想在不改动任何文件的前提下检查可以单跑cd packages/i18n node --experimental-transform-types ./src/scripts/check.mts -t find-sprintf-params-t参数支持传递任务名会清空默认任务集合、只执行指定的检查。提交规范仅含翻译改动的提交translation-only changes使用i18n:作为 commit 类型前缀遵循仓库 pull request 模板的约定。把 key 改动与功能逻辑改动分开提交能让翻译相关的审阅与后续语言同步都更清晰。小结一份可直接照做的检查清单最后把整篇指南浓缩成写新翻译键时的自检清单只编辑packages/i18n/src/locales/en.i18n.json追加的键用Sentence_case_with_underscores或对应命名空间内既有的小写点号路径风格语义相同就复用旧键语义不同绝不共用不要用渲染位置、颜色等 UI 特征命名动态值一律用{{camelCase}}绝不用%s、__name__或碎片拼接句子带计数的键定义成复数对象并传count把复数决策交给 i18next 的 CLDR 规则服务端渲染的文案务必按接收者语言 →Language设置 →en的链条显式传lng新接口优先返回键、在客户端翻译最后跑一次yarn workspace rocket.chat/i18n lint:fix让排序、占位符一致性、陈旧键清理等规则自动落地。【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/9 13:44:19

牛客Java刷题21-26:继承、多态、接口与抽象类深度解析

从“照着敲都会,一运行就报错”到真正理解面向对象,是每个Java零基础学习者必须跨过的一道坎。这个系列的牛客刷题指南走到第21~26题,正好切入Java里最核心也最容易让人绕晕的一组概念:继承、多态、接口和抽象类。这篇文章就把这6…

2026/9/9 13:44:19

割草机器人RTK天线:决定厘米级定位的最后一厘米

割草这件事,听起来简单,真正做起来才发现坑全在地图之外。我见过不少割草机器人的DEMO,空地上跑得规规矩矩,一旦贴到墙边、树根、围栏,就开始画蛇型,留下一道道"阴阳头"。绝大多数人第一反应是算…

2026/9/9 14:34:30

RESTler+Jenkins:API模糊测试流水线实践与踩坑指南

我先把话放这儿:不要指望手工测试能把一个API服务所有角落都测干净。绝大多数团队在CI阶段跑的是Postman集合和单元测试,但等接口文档膨胀到几十个资源、几百个参数组合的时候,人肉构造用例的天花板就到了。我经历过一次线上事故,…

2026/9/9 14:34:30

大模型API线上稳定性排查指南:从超时到流式输出的完整方案

上个月我负责的服务接入了大模型 API,上线前所有测试都过了,我甚至用脚本压了 50 个并发,本地表现一直很理想。结果真正放量之后,用户反馈接踵而至:回答到一半突然断了、页面转圈十几秒、多问几轮就开始报错。后台看监…

2026/9/9 14:34:30

Selenium自动化测试框架从零搭建:Python+pytest+PO模式实战

做Web自动化测试的,谁没在Selenium上栽过几个跟头?最简单的脚本能跑通,一到了实际项目里,脚本堆成山、用例跑两天就开始红,定位器改一处崩一片,执行到一半浏览器就无响应。老实说,Selenium本身不…

2026/9/9 14:34:30

单片机驱动LED矩阵像素屏:从扫描原理到动画实现

简介:这是一份围绕像素艺术与发光二极管矩阵结合的入门资源,面向对复古8位图像风格感兴趣、希望用硬件显示静态图案或动画的电子爱好者与开发者。内容系统梳理了像素艺术依靠色块拼合构图的核心理念,并说明发光二极管矩阵中每颗灯珠即一个像素…

2026/9/9 14:29:29

爆炸建筑毁伤估算方法详解:从冲击波荷载到整体毁伤定级

做过几次工业爆炸事故后的建筑损伤评估,也帮一些单位做过危险源周边的建筑抗爆预评估,我对这门“估算”的体会是:它既是科学,也是手艺活。所谓科学,是因为背后有冲击波力学、结构动力学、材料损伤累积这些硬核理论撑着…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/8 7:15:10

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/9 0:00:48

MHS模型硬件标准:让大模型像调用软件一样控制物理设备

让Claude真正看着显微镜说“这个细胞形态不太对”,或者让大模型自己调一版机械臂的运动轨迹,这事儿听上去已经很接近科幻片了。但你真上手试一次就会发现,模型不缺智商,缺的是一个能插进显微镜、机械臂、激光控制器里的“通用插座…

2026/9/9 0:00:48

AI五大核心方向详解:从机器学习到大模型,零基础转行选哪条?

会有人告诉我,他想转行学AI,但打开招聘网站一看直接傻眼:机器学习、深度学习、自然语言处理、计算机视觉、大模型应用……满屏都是这些词,好像每个都会一点,又好像每个都离自己很远。还有人上来就问“学Python还是学Ja…

2026/9/9 0:00:49

从50行最小循环到生产级AI引擎:工程化改造全解析

直接说干货。这一章我写的不是那种"hello world跑通某个模型"的教程,而是把AI引擎当做一个真正要上线、要被人调用、要扛流量的系统来聊。从最初只有50行的最小循环,到能够承载生产流量的AI引擎,中间差的不是代码量,而是…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码