Zulip 翻译指南:从 Weblate 工作流到国际化工具链的完整实践
【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip
Zulip 是一款开源团队聊天服务器与 Web 应用,对 Unicode 拥有完整支持(并对 RTL 从右到左语言提供部分支持),UI 已被翻译为包括西班牙语、德语、印地语、法语、中文、俄语、日语在内的十余种主要语言。本文以官方翻译指南为主线,系统讲解贡献者如何通过 Weblate 参与翻译、如何在本机测试译文,并结合仓库源码剖析 Zulip 前端的$t/{{t}}、服务端_()/{% trans %}等国际化标记语法、复数(plural)处理、makemessages/compilemessages命令以及大小写规范 lint 的底层实现,帮助读者既能在贡献流程中即学即用,又能深入理解 Zulip 完整的国际化(i18n)工具链。
一、Zulip 的国际化概览:一套面向多语言的完整设计
Zulip 的国际化不是事后补丁,而是从 UI 渲染到资源文件管理的系统性设计。官方文档明确了两个层面的支持目标:
- 使用层面:由于完整支持 Unicode,用户可以在 Zulip 的任何地方使用自己的偏好语言;RTL 语言(如阿拉伯语、希伯来语)获得部分支持。
- 翻译层面:Zulip 官方 UI 已被翻译为十余种主要语言(西班牙语、德语、印地语、法语、中文、俄语、日语等),且持续欢迎新的语言贡献。
从仓库结构可以直观看到这套体系的落地形态:每种语言在 locale/ 目录下拥有独立的资源目录,例如locale/de/(德语)、locale/zh_Hans/(简体中文)、locale/ja/(日语)等,每个目录下同时包含两份核心资源文件:
LC_MESSAGES/django.po—— 服务端字符串(Portico 登录页、API 错误消息等)的 gettext 资源;translations.json—— Web 应用前端字符串的 FormatJS/ICU 资源。
实测locale/de/translations.json中形如"1 day": "1 Tag"、"1 hour": "1 Stunde"的键值对,正是"英文原文 → 目标语言译文"的标准映射结构。
对于想了解"字符串如何被打上翻译标记、翻译如何同步回仓库"这一整套技术流程的开发者,官方建议阅读姊妹篇 Internationalization for Developers;而本文接下来的主体,则聚焦翻译贡献者的实际工作流。
二、翻译者的工作流(Translators' Workflow)
2.1 七个标准步骤
官方为翻译贡献者定义了清晰的七步流程:
加入翻译频道:在 Zulip development community server 加入
#translation频道(即 translation-channel)打个招呼。该频道同时是提问、汇报进度、报告问题字符串的官方渠道。注册 Weblate 账号:前往 hosted.weblate.org 注册。
- 重要提示:除非你计划贡献国家/地区特有的翻译,否则注册时不要在选择语言列表里勾选国家特定语言。例如:若你打算翻译英式英语,才选择English (United Kingdom) (en_GB);对于通用西班牙语,应选Spanish (es)而非Spanish (Colombia) (es_CO)。选择国家特定语言会显著缩小你能贡献的范围,且这些变体通常无需维护。
进入 Zulip 的 Weblate 项目页:打开 Zulip project on Weblate。
选择目标语言:你的偏好语言会排在列表顶部,直接点选即可。
(可选)按组件细分翻译:点击顶部的 "Components" 标签,只翻译项目的一部分。Zulip 按使用位置将项目拆分为多个组件:
Flutter:移动端应用(Zulip Mobile)使用的字符串。Desktop:Zulip 桌面端应用中不与 Web 应用共享的部分,字符串数量较少。Django与Frontend:对应 Zulip 服务器与 Web 应用的下一个大版本字符串(即 chat.zulip.org 和 Zulip Cloud 上正在运行的版本)。- 名称带版本号后缀的
Django与Frontend变体(如(10.x)):对应 Zulip 当前稳定发布系列的字符串。
Weblate 足够智能:即使同一字符串出现在多个资源中,也只会要求你翻译一次。带
(10.x)后缀的版本变体,则让译者可以把某种语言在当前发布版本中做到 100% 翻译完成。点击 "Translate" 按钮开始翻译:具体操作方式参考 Weblate 官方翻译文档。
尽可能实测你的翻译(测试细节见下文第四节),然后在 Zulip 里请维护者把 Weblate 的字符串合并进代码库,并部署到 chat.zulip.org,以便你在真实环境中验证。
2.2 翻译过程中的实用技巧
官方给出了一批经社区验证的实战建议:
- 随身携带语言风格指南:始终跟随你的语言的 翻译风格指南,翻译时在标签页中保持打开。如果某语言还没有指南,边译边写一份是最好的时机——它最容易边翻译边记录,且能极大帮助未来的译者。提交方式参见 文档贡献说明。
- 使用并更新 Weblate 术语表(Glossary):Zulip glossary 会为全应用反复出现的术语(如 "channel")提供内联的、一致的翻译参考。
- 不要翻译变量与代码:通常以
%开头、位于 HTML 标签<...>内、或被花括号{variable}包裹的内容,一律原样保留(verbatim)。 - 善用 "Source string location":当上下文不清晰时,点击 Weblate 界面右侧栏的该链接,可跳转到源码出处查看语境。
- 存疑就问:不确定的字符串,到
#translation频道向社区求证。 - 留意大小写与标点:细节决定质量!Weblate 会捕获并警告部分标点不匹配的情况。
- 使用 Weblate 快捷键:参考 Weblate 文档中的 keyboard shortcuts 提高效率。
- 优先级排序:应优先翻译
Frontend与Flutter组件,因为最显眼的用户可见字符串在那里;但Django组件中的API 错误消息同样会呈现给用户,所以完整的翻译必须包含它们。
三、机器翻译(Machine Translation)
Weblate 内置了机器翻译能力。如果你的语言启用了该功能,可以在翻译框下方的Automatic suggestions标签页里一键生成机器翻译建议。
需要特别强调的是:Zulip 期望的是人类质量(human-quality)的翻译。机器翻译只能作为辅助工具,所有机器生成的字符串都必须逐条人工复核后才能提交。
四、在本机测试翻译(Testing Translations)
本节假设你已搭好 Zulip 开发环境。如果搭建环境有困难,也可以在 chat.zulip.org 上求助——社区通常可以直接把最新翻译部署上去供你验证。
4.1 拉取 Weblate 的翻译提交
把 Weblate 添加为 Git 远端:
git remote add weblate https://hosted.weblate.org/git/zulip/django/把 Weblate 的提交合入本地仓库:
git cherry-pick weblate/main ^upstream/main
4.2 在 UI 中查看翻译的四种途径
URL 前缀法:把语言代码作为 URL 前缀插入即可。例如用
http://localhost:9991/de/login/查看德语登录页。这适用于 Zulip UI 的任何部分,包括 Portico(未登录)页面。应用内切换:对于登录后的 Zulip 实际 Web 应用界面,可以在 Zulip UI 的偏好设置里选择语言。
系统语言自动检测:如果你的操作系统/浏览器配置了语言,Zulip 的 Portico(未登录)页面会自动使用该语言。注意:只有用户实际需要使用的页面(如
/login/、/register/等)才被标记为可翻译,/features/这类营销页面不在此列。HTTP 头手动指定:用
requests、cURL 或urllib等 HTTP 客户端库传递Accept-Language头。官方给出的 Python 示例:import requests headers = {"Accept-Language": "de"} response = requests.get("http://localhost:9991/login/", headers=headers) print(response.content)这在调试时偶尔会很有用。
4.3 浏览器语言选择优先级
需要厘清上述途径的交互关系时,官方明确给出了 Zulip 确定用户请求语言的优先级(基本沿袭 Django 文档):
- URL 前缀中的语言代码(例如
/de/login/)优先; - 其次查找名为
django_language的 Cookie(可通过LANGUAGE_COOKIE_NAME设置修改名称); - 最后回退到 HTTP 请求中的
Accept-Language头(浏览器借此把 OS/浏览器语言告诉 Zulip)。
五、翻译风格指南(Translation Style Guides)
Zulip 为各语言维护了官方翻译风格指南,给出特定语言的翻译决策(例如 "channel" 该译成什么词)及其推理过程,使后来的译者能理解并延续这些决策。当前仓库中已有的指南包括:
- Chinese
- Finnish
- French
- German
- Hindi
- Japanese
- Polish
- Russian
- Spanish
官方鼓励把指南中的信息同时沉淀到 Weblate glossary,翻译时会以内联建议的形式呈现。尚未编写指南的语言,官方强烈建议译者边翻译边写,因为"边译边记"最容易产出高质量、可持续维护的风格文档。
以 中文翻译指南 为例,可以看到典型的风格决策样本:
- 术语表:Message →消息("Stream Message"译作"频道消息","Direct Message"译作"直信","Starred Message"借鉴 QQ 邮箱译为"星标消息");Stream →频道(灵感来自游戏 Ingress 的聊天 Channel,比"群组/主题/版块/栏目"都更贴切);Topic →话题;Invite-Only/Public Stream →私有/公开频道;Bot →机器人;Integration →应用整合;Notification →通知;Alert Word →提示词。
- 习惯用语:Subscribe/Unsubscribe →订阅/退订;Narrow to →筛选(搜索语境下译"搜索");Mute/Unmute →开启/关闭免打扰(借自微信"消息免打扰",比"静音"更贴合 Zulip);Deactivate/Reactivate 分语境译为禁用/启用(帐户)、关闭/激活(社区);Invalid →不正确(如 "Invalid API key" → "API 码不正确");"I want" →开启(避免直译"我想"的口语化)。
- 其它细节:You/Your 用敬语您/您的;"We" 常省略不译或转换表达(如 "Still no email? We can resend it" → "仍然没有收到邮件?点击重新发送");感叹号与句号一般省略——中文感叹号语气比英文更重,句号(。)则影响页面排版,句末留空即可。
而 德语翻译指南 则展示了另一种语言策略:总体采用非正式语气(用 "Du" 而非 "Sie")、使用性别冒号(Gender-Doppelpunkt,如 "Nutzer:innen")、优先使用不定式命令式、尽量避免德语长复合词(词长控制在 20 字符内,例如不译 "Alert words" 为 "Benachrichtigungsstichwörter" 而是 "Stichwörter, die mich benachrichtigen")、对 "Bot" 等无准确德语等价物的外来词直接沿用,并提醒警惕false friends(如 actually/eigentlich、eventually/schließlich)。
这类风格指南的价值在于:统一性是翻译质量的基石,跨翻译者的用词一致性直接决定了用户界面的专业度。
六、英文翻译字符串的大小写规范(Capitalization)
Zulip 要求所有英文可翻译字符串的大小写必须与 Zulip 整体的大小写风格一致:
- 句子或短语的首字母大写,但遵循 sentence case(整句大小写风格),而非 Title Case:
- 正确:"Channel settings"
- 错误:"Channel Settings"
- 所有专有名词大写:
- 正确:"This is Zulip"
- 错误:"This is zulip"
- 所有通用词如 URL、HTTP 等使用标准写法:
- 正确:"URL"
- 错误:"Url"
该规范由 Zulip 测试套件强制实施(见 测试文档):通过./tools/check-capitalization检查所有标记为可翻译的字符串。其中 tools/lib/capitalization.py 维护了豁免列表IGNORED_PHRASES(如 AI、API、HTTP、JSON、Zulip、URL、UUID、GitHub 等专有名词与缩写,以及 "I'm"、"Topics I start" 等含 "I" 的表达),并采用"最长短语优先匹配"的策略防止短词先匹配而破坏长词。tools/check-capitalization会先自动执行./manage.py makemessages --locale en生成最新资源,再逐一校验英文消息的大小写。
七、开发者视角:Zulip 的国际化技术全景
翻译贡献者了解这套技术栈,有助于理解字符串的来龙去脉与上下文。以下内容提炼自 Internationalization for Developers 并对照源码验证。
7.1 设计原则:UI 必须"能翻译、能排版"
- 文本宽度不是常量:同一含义在不同语言中宽度差异巨大,不能按英文宽度硬编码按钮/组件。测试技巧:把字符串人为拉长 50%、缩短 50% 检查布局;对已有字符串,俄语是"比英文长"的典型测试用例,日语则通常更短。
- 所有用户可见字符串都应标记翻译:包括错误字符串、日期、邮件内容。例外仅有三类:落地页(Landing pages)、帮助中心页面、Zulip 更新公告。这些页面只需保证能被 Google Translate 等工具可用即可。
- 注意"用户可见"四个字:社区译者的时间是宝贵的,只标记真正会显示给用户的内容。
7.2 标记字符串时的三个陷阱
- 标点:不同语言标点习惯不同(例如日语句末不用
.),因此.、?等句末符号必须包含在待翻译字符串内。 - 语序:拼接可翻译字符串必然产生烂翻译(如主语-动词顺序差异)。若句子含变量,绝不能把变量前后的部分拆开分别标记。
- 数字字符串:如 "5 bananas",不同语言处理方式差异很大,参见下方复数小节。
7.3 Web 应用翻译语法(FormatJS / ICU MessageFormat)
Web 应用使用 FormatJS(基于标准 ICU MessageFormat),覆盖 Handlebars 模板与 JavaScript。
JavaScript 中的$t:intl.js中把intl.formatMessage化名为$t(实际定义于 web/src/i18n.ts)。待翻译字符串必须是常量字面量,变量用花括号插值并传上下文对象:
$t({defaultMessage: "English text with a {variable}"}, {variable: "Variable value"})$t不转义变量;若翻译结果最终作为 HTML 使用,请用$t_html:
html_content = $t_html({defaultMessage: "HTML with a {variable}"}, {variable: "Variable value"}); $("#foo").html(html_content);翻译字符串内只允许default_html_elements(定义于 web/src/i18n.ts)枚举的无属性简单标签(b、code、em、i、kbd、p、strong),以尽量避免把 HTML 细节暴露给译者。需要更复杂标记(如链接)时,可定义局部自定义 HTML 标签或改用 Handlebars 模板:
$t_html( {defaultMessage: "<b>HTML</b> linking to the <z-link>login page</z-link>"}, {"z-link": (content_html) => `<a href="/login/">${content_html.join("")}</a>`}, )Handlebars 模板:Zulip 注册了两个 FormatJS 辅助函数。简单字符串用{{t '...' }};传给 partial 时用variable_name=(t 'English text');HTML 字符串用块级{{#tr}}...{{/tr}}:
{{t 'English text' }} {{t 'Block of English text with a {variable}.' }} {{#tr}} <p>Block of English text with a {variable}.</p> {{/tr}}与 JavaScript 不同,Handlebars 里的变量会被辅助函数自动转义。注意{{#tr}}...{{/tr}}内禁止Handlebars 表达式/块(如{{variable}}、{{#if}}),因为它们会在字符串经 FormatJS 处理前被求值,导致待翻译字符串非常量——项目有专门的 linter 强制这一规则。复杂标记同样通过局部自定义标签实现:
{{#tr}} <b>HTML</b> linking to the <z-link>login page</z-link> {{#*inline "z-link"}}<a href="/login/">{{> @partial-block}}</a>{{/inline}} {{/tr}}7.4 复数与列表(Plurals and Lists)
英语只有单/复数两种形式("1 banana" vs "2 bananas"),而俄语等语言的名词变格甚至取决于数量的末位数字。Zulip 用 ICU MessageFormat 表达复数:
"{N, plural, one {Done! {N} message marked as read.} other {Done! {N} messages marked as read.}}"译者可用同一语法写出不同格范畴的译文(如俄语按 one/few/many 区分)。开发者只需写好英文的单/复数两个分支,其余交给译者;即便如此,设计 UI 时也应尽量避免非必要的复数字符串(例如"图标 + 数字"的呈现方式往往更优)。列表构造("foo, bar, and baz")在语言间差异巨大(有些语言甚至不用逗号),Web 应用提供了util.format_array_as_list(基于Intl模块)来正确完成,可用git grep查找使用示例。
7.5 服务端翻译语法
服务端字符串主要有两类:API 返回的错误字符串等值,以及不使用 JS/Handlebars 渲染的 Portico 页面(如登录流程)字符串。
Jinja2 模板:使用_()函数或{% trans %}块标记(HTML 模板文档有更多语法说明):
{{ _("English text") }} {% trans %}This string will have {{ value }} inside.{% endtrans %}切勿把一句话拆成三段拼接(会导致无法正确翻译):
# 不要这样做! {{ _("This string will have") }} {{ value }} {{ _("inside") }}Python:导入django.utils.translation.gettext为_。JsonableError的错误消息必须始终是_()包裹的字面量:
from django.utils.translation import gettext as _ JsonableError(_('English text'))在模块顶层或类中声明用户可见字符串时,必须改用gettext_lazy,确保翻译发生在请求处理期(此时 Django 才知道目标语言):
AVATAR_CHANGES_DISABLED_ERROR = gettext_lazy("Avatar changes are disabled in this organization.")tools/lint会对JsonableError等 JSON 错误消息的国际化用法做校验。
7.6 端到端翻译流程与资源文件
完整工具链如下:
- 开发者标记字符串(见上文各小节);
- 运行
./manage.py makemessages为每种语言生成资源文件:Web 应用为translations.json,服务端为django.po; - 资源文件提交后由 Weblate 自动扫描;
- 译者在 Weblate UI 中翻译;
- Weblate 生成 Git commit,由维护者合入代码库。
资源文件位置:locale/<lang_code>/LC_MESSAGES/django.po(服务端)与locale/<lang_code>/translations.json(Web 应用)。
makemessages是幂等的,具体行为可在 zerver/management/commands/makemessages.py 源码中验证:
- 仅当单数键在代码中不再使用时才删除;
- 仅当对应的单数键消失时才删除复数键;
- 不会覆盖已包含译文文本的单数键值。
源码还揭示了一个关键细节:Zulip 的makemessages命令继承自 Django,但通过**猴子补丁(monkey-patch)**扩展了 Django 的正则表达式,使 Jinja2 的{% trans %}、{% pluralize %}语法也能被提取(Django 原生只认识 DTL 语法);同时它额外注册了--frontend-source(默认web/templates)、--frontend-output(默认locale)、--frontend-namespace(默认translations.json)参数,用前端正则表达式从.hbs模板中提取{{#tr}}、{{t '...' }}等字符串,并调用formatjs extract从web/src/**/*.{js,ts}中提取$t/$t_html的defaultMessage。get_new_strings方法证实了幂等语义:英文 locale 的译文等于原文本身,其他语言的新键默认值为空字符串"",旧译文原样保留。
配套的 zerver/management/commands/compilemessages.py 则负责在构建期把.po编译为.mo,并通过统计django.po未翻译条目与translations.json空值,为每种语言计算出percent_translated(翻译完成百分比),汇总写入locale/language_options.json。
7.7 语言列表与翻译百分比的呈现
前端 web/src/i18n.ts 的initialize函数只把percent_translated >= 5%的语言纳入可选列表,语言选择器会以display_name (percent%)形式展示完成度(见get_language_list_columns);intl实例通过page_params.request_language与page_params.translation_data装载用户语言与翻译数据,对新增未翻译字符串(MISSING_TRANSLATION)则静默忽略。这套逻辑让"翻译完成度"直接成为用户体验的一部分——这也从侧面说明,每位译者的贡献都会即时反映在语言列表的百分比上。
八、结语
参与 Zulip 翻译是一条门槛低、回报高的贡献路径:无需精通服务器与前端架构,只需注册 Weblate、跟随语言风格指南、认真复核机器翻译建议,就能让成千上万用户用上高质量的母语界面。而当你希望更进一步,本文第七节梳理的$t/$t_html、{{t}}/{{#tr}}、_()/{% trans %}、ICU 复数、makemessages/compilemessages与大小写 lint,则构成了理解 Zulip 国际化全貌的完整地图——两者结合,正对应官方文档"翻译贡献者"与"国际化开发者"两条互补的学习路线。
【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考