Zulip 翻译指南:从 Weblate 工作流到国际化工具链的完整实践
2026/9/13 5:10:16 网站建设 项目流程

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 七个标准步骤

官方为翻译贡献者定义了清晰的七步流程:

  1. 加入翻译频道:在 Zulip development community server 加入#translation频道(即 translation-channel)打个招呼。该频道同时是提问、汇报进度、报告问题字符串的官方渠道。

  2. 注册 Weblate 账号:前往 hosted.weblate.org 注册。

    • 重要提示:除非你计划贡献国家/地区特有的翻译,否则注册时不要在选择语言列表里勾选国家特定语言。例如:若你打算翻译英式英语,才选择English (United Kingdom) (en_GB);对于通用西班牙语,应选Spanish (es)而非Spanish (Colombia) (es_CO)。选择国家特定语言会显著缩小你能贡献的范围,且这些变体通常无需维护。
  3. 进入 Zulip 的 Weblate 项目页:打开 Zulip project on Weblate。

  4. 选择目标语言:你的偏好语言会排在列表顶部,直接点选即可。

  5. (可选)按组件细分翻译:点击顶部的 "Components" 标签,只翻译项目的一部分。Zulip 按使用位置将项目拆分为多个组件:

    • Flutter:移动端应用(Zulip Mobile)使用的字符串。
    • Desktop:Zulip 桌面端应用中不与 Web 应用共享的部分,字符串数量较少。
    • DjangoFrontend:对应 Zulip 服务器与 Web 应用的下一个大版本字符串(即 chat.zulip.org 和 Zulip Cloud 上正在运行的版本)。
    • 名称带版本号后缀的DjangoFrontend变体(如(10.x)):对应 Zulip 当前稳定发布系列的字符串。

    Weblate 足够智能:即使同一字符串出现在多个资源中,也只会要求你翻译一次。带(10.x)后缀的版本变体,则让译者可以把某种语言在当前发布版本中做到 100% 翻译完成。

  6. 点击 "Translate" 按钮开始翻译:具体操作方式参考 Weblate 官方翻译文档。

  7. 尽可能实测你的翻译(测试细节见下文第四节),然后在 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 提高效率。
  • 优先级排序:应优先翻译FrontendFlutter组件,因为最显眼的用户可见字符串在那里;但Django组件中的API 错误消息同样会呈现给用户,所以完整的翻译必须包含它们。

三、机器翻译(Machine Translation)

Weblate 内置了机器翻译能力。如果你的语言启用了该功能,可以在翻译框下方的Automatic suggestions标签页里一键生成机器翻译建议。

需要特别强调的是:Zulip 期望的是人类质量(human-quality)的翻译。机器翻译只能作为辅助工具,所有机器生成的字符串都必须逐条人工复核后才能提交。


四、在本机测试翻译(Testing Translations)

本节假设你已搭好 Zulip 开发环境。如果搭建环境有困难,也可以在 chat.zulip.org 上求助——社区通常可以直接把最新翻译部署上去供你验证。

4.1 拉取 Weblate 的翻译提交

  1. 把 Weblate 添加为 Git 远端:

    git remote add weblate https://hosted.weblate.org/git/zulip/django/
  2. 把 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 文档):

  1. URL 前缀中的语言代码(例如/de/login/)优先;
  2. 其次查找名为django_language的 Cookie(可通过LANGUAGE_COOKIE_NAME设置修改名称);
  3. 最后回退到 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 标记字符串时的三个陷阱

  1. 标点:不同语言标点习惯不同(例如日语句末不用.),因此.?等句末符号必须包含在待翻译字符串内
  2. 语序:拼接可翻译字符串必然产生烂翻译(如主语-动词顺序差异)。若句子含变量,绝不能把变量前后的部分拆开分别标记。
  3. 数字字符串:如 "5 bananas",不同语言处理方式差异很大,参见下方复数小节。

7.3 Web 应用翻译语法(FormatJS / ICU MessageFormat)

Web 应用使用 FormatJS(基于标准 ICU MessageFormat),覆盖 Handlebars 模板与 JavaScript。

JavaScript 中的$tintl.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)枚举的无属性简单标签(bcodeemikbdpstrong),以尽量避免把 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 端到端翻译流程与资源文件

完整工具链如下:

  1. 开发者标记字符串(见上文各小节);
  2. 运行./manage.py makemessages为每种语言生成资源文件:Web 应用为translations.json,服务端为django.po
  3. 资源文件提交后由 Weblate 自动扫描;
  4. 译者在 Weblate UI 中翻译;
  5. 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 extractweb/src/**/*.{js,ts}中提取$t/$t_htmldefaultMessageget_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_languagepage_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),仅供参考

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

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

立即咨询