Zulip 国际化(i18n)开发实战指南:从字符串标记到翻译管线
2026/9/13 9:00:33 网站建设 项目流程

Zulip 国际化(i18n)开发实战指南:从字符串标记到翻译管线

【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip

Zulip 从设计之初就将国际化(internationalization,简称 i18n)作为一等公民,允许用户以自己偏好的语言查看整个 UI。本文基于仓库中的 docs/translating/internationalization.md,系统讲解 Zulip 中"哪些字符串需要翻译、如何在不同技术栈(JavaScript / Handlebars / Jinja2 / Python)中正确标记字符串、复数与列表如何处理、以及从makemessages到 Weblate 的完整翻译管线"。读完本文,你将掌握在 Zulip 中为 Web 前端与 Python 服务端添加可翻译字符串的完整实战方案,并理解底层源码的实现机制。

国际化如何影响 Zulip 的 UI

Zulip 要求开发者时刻牢记一个核心事实:文本宽度不是常量。表达同一含义所需的字符串宽度在不同语言之间差异极大,例如德语、俄语往往比英语更长,日语则通常更短。因此,绝不能把按钮或控件按英文宽度硬编码排版后,就指望它在所有语言下都显示良好。

在提交代码前,建议按以下方法自测:

  • 将字符串长度人为拉长 50% 和缩短 50%,检查 UI 是否会溢出、折行或裁切;
  • 对已在 Zulip UI 中存在的字符串,可以用俄语作为"普遍比英文长"的测试用例,用日语作为"普遍比英文短"的测试用例,直观验证布局是否健壮。

哪些内容应该被标记翻译

Zulip 的目标是:所有面向用户的字符串——包括错误提示、日期、邮件正文等任何用户可能看到的内容——都必须在 HTML 模板和代码中标记翻译,并由 linter 强制约束。

需要排除在"全部标记"规则之外的特例只有三类:

  • 落地页(Landing pages,如产品功能页);
  • 帮助中心页面;
  • Zulip 更新公告(Zulip update announcements)。

这三类页面只要求能做到"可通过 Google Translate 之类工具阅读"即可。

同时,"面向用户"这个限定也很关键:为了让社区译者宝贵的翻译时间花在刀刃上,Zulip 只为真正会展示给用户的内容打标记,内部实现细节、日志等一律不标记。

如何正确标记一个字符串

不同语言的差异决定了标记字符串时标记什么、如何切分必须非常谨慎:

  • 标点符号因语言而异(例如日语常在句末省略.)。因此句末符号如.?必须包含在待翻译字符串内部,译者才能正确地翻译整句内容。
  • 词序因语言而异(有的语言主谓在前,有的相反)。因此拼接多个可翻译字符串会产出错误结果:如果句子中包含变量,绝不能把变量前、后两部分分开标记(这正是 Jinja2 部分会强调"不要拆句"的原因)。
  • 带数字的字符串(如 "5 bananas")在不同语言中的表述差异极大(俄语名词单复数的形式取决于数量的最后一位数字),标记含数字的字符串时要格外小心,详见下文 复数与列表。

此外,Zulip 还有一条"句首大写(sentence case)"大小写规范,并通过 linter 检查所有被标记翻译的字符串来强制执行:句子/短语首字母大写("Channel settings" 而非 "Channel Settings")、专有名词大写("This is Zulip")、常用词如 URL/HTTP 使用标准写法("URL" 而非 "Url")。该检查由 tools/check-capitalization 实现,排除词表位于 tools/lib/capitalization.py(如IGNORED_PHRASES)。

Zulip 中的翻译语法

先记住两条通用准则:

  1. 翻译函数必须接收"待翻译的字符串字面量"本身,而不是存放字符串的变量。否则,用于从项目中抽取字符串交给译者的解析器将找不到你的字符串。
  2. Zulip 服务端使用 Jinja2 模板系统,Web 应用使用 Handlebars;HTML 模板文档中收录了这两个系统语法与行为的详细说明。

Web 应用翻译:FormatJS + ICU MessageFormat

Web 应用的翻译统一基于 FormatJS 库,覆盖 Handlebars 模板和 JavaScript/TypeScript 两种场景。FormatJS 采用标准的 ICU MessageFormat 语法,天然支持下文所述的复数翻译。

JavaScript / TypeScript

在 JS 文件中标记可翻译字符串,需将其传给intl.formatMessage函数——Zulip 在 web/src/i18n.ts 中将其别名定义为$t

$t({defaultMessage: "English text"})

待翻译的字符串必须是常量字面量;变量通过花括号{variable}插值,并传入上下文对象:

$t({defaultMessage: "English text with a {variable}"}, {variable: "Variable value"})

$t不会对任何变量做 HTML 转义。因此,如果翻译结果最终会被当作 HTML 使用,必须改用$t_html

html_content = $t_html({defaultMessage: "HTML with a {variable}"}, {variable: "Variable value"}); $("#foo").html(html_content);

翻译字符串内允许出现的 HTML 标签是受限的:只有 web/src/i18n.ts 中default_html_elements枚举的无属性简单标签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>`}, )
复数与列表

复数是人类语言中最复杂的细节之一:英语只有 "1 banana" / "2 bananas" 两种形式,而俄语名词形式部分取决于数量个位数,需要 one/few/many 等多套规则。

Zulip 用标准 ICU MessageFormat 语法表达复数串。开发者只需为英语写出正确的一对单/复数变体:

"{N, plural, one {Done! {N} message marked as read.} other {Done! {N} messages marked as read.}}"

译者则可以用同样的语法,按目标语言写出任意多套变体,例如这段俄语翻译根据数量是 1、少数还是多数分别处理:

"{N, plural, one {Готово! {N} сообщение помечено как прочитанное.} few {Готово! {N} сообщений помечены как прочитанные.} many {Готово! {N} сообщений помечены как прочитанные.} other {Готово! {N} сообщений помечены как прочитанные.}}"

你不需要会写俄语复数——作为开发者,只需为英语写出正确的 ICU 复数(恒为单数/复数两套),其余交给译者。即便如此,带复数的英语字符串也不易阅读,因此在设计 UI 时,Zulip 通常尽量避免无谓地使用需要复数的字符串,转而用"图标 + 数字"等方式呈现信息。

列表的构造方式在不同语言间差异也很大(有些语言根本不用逗号)。Web 应用提供了util.format_array_as_list函数,利用浏览器原生Intl.ListFormat正确处理国际化列表——其实现位于 web/src/util.ts,内部通过new Intl.ListFormat(user_settings.default_language, {style, type})完成格式化,并在不支持Intl.ListFormat时退化为array.join(", ")。在仓库中git grep可找到大量使用范例,例如 web/src/compose_ui.ts 中格式化收件人名单、web/src/people.ts 中格式化 @提及列表等。

Handlebars 模板

Handlebars 模板同样使用 FormatJS,通过 Zulip 注册的两个 Handlebars helpers)完成翻译。简单字符串的语法为:

{{t 'English text' }} {{t 'Block of English text with a {variable}.' }}

将翻译后的字符串传给 Handlebars partial 时:

{{> template_name variable_name=(t 'English text') }}

HTML 字符串使用块级{{#tr}}...{{/tr}}

{{#tr}} <p>Block of English text.</p> {{/tr}} {{#tr}} <p>Block of English text with a {variable}.</p> {{/tr}}

与 JavaScript 一样,变量用单花括号{variable}(而非 Handlebars 惯用的双花括号)包裹;与 JavaScript 不同的是,Handlebars helper 会自动转义变量(见 web/src/templates.ts 中Handlebars.Utils.escapeExpression的调用)。

注意:{{#tr}}...{{/tr}}翻译块内不允许出现 Handlebars 表达式(如{{variable}})或块语句(如{{#if}}...{{/if}})。因为 Handlebars 表达式会在字符串交给 FormatJS 处理之前就被求值,导致待翻译字符串不再是常量。Zulip 配有专门的 linter 强制执行这一约束。

翻译串内 HTML 标签的限制与 JavaScript 相同;需要更复杂标记时同样用局部自定义标签:

{{#tr}} <b>HTML</b> linking to the <z-link>login page</z-link> {{#*inline "z-link"}}<a href="/login/">{{> @partial-block}}</a>{{/inline}} {{/tr}}

服务端翻译

服务端字符串主要分布在两个领域:

  • API 返回的错误字符串及其他值;
  • 未用 JavaScript 或 Handlebars 渲染的 portico 页面(如登录流程)。
Jinja2 模板

Zulip UI 中所有面向用户的文本都应经由 Jinja2 HTML 模板生成,以便翻译。标记方式是在模板中使用_()函数:

{{ _("English text") }}

如果一段文本同时包含字面量与变量,应使用块级翻译{% trans %}...{% endtrans %}(Jinja2 的 i18n 扩展提供的 trans 标签),为变量放置占位符,从而允许译者翻译整个句子:

{% trans %}This string will have {{ value }} inside.{% endtrans %}

绝不能像下面这样拆句标记,否则将无法正确翻译(词序问题):

# Don't do this! {{ _("This string will have") }} {{ value }} {{ _("inside") }}
Python

Python 代码中使用_()函数标记字符串,按 Django 惯例导入:

from django.utils.translation import gettext as _

Zulip 要求所有错误信息都可翻译。为此,传给JsonableError的错误消息必须是_()包裹的字符串字面量:

JsonableError(_('English text'))

如果在模块顶层或类内声明面向用户的字符串,则必须改用gettext_lazy,以保证翻译发生在请求处理时——此时 Django 才知道应使用哪种语言。例如 zerver/models 中:

from zproject.backends import check_password_strength, email_belongs_to_ldap AVATAR_CHANGES_DISABLED_ERROR = gettext_lazy("Avatar changes are disabled in this organization.") def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpResponse: ...
class Realm(models.Model): MAX_REALM_NAME_LENGTH = 40 MAX_REALM_SUBDOMAIN_LENGTH = 40 ... ... STREAM_EVENTS_NOTIFICATION_TOPIC = gettext_lazy("channel events")

为确保 JSON 错误信息始终被国际化,Zulip 的 linter(tools/lint)会尝试校验上述用法是否合规。

翻译流程(端到端工具链)

Zulip 的翻译工具链整体流程如下:

  1. 标记字符串:按上文服务端与 Web 应用各自的方式完成标记。
  2. 生成资源文件:运行./manage.py makemessages。该命令会为每种语言生成两份资源文件——Web 应用字符串写入translations.json,服务端字符串写入django.po
  3. 提交并扫描:资源文件提交后,会被 Weblate 自动扫描。
  4. 译者翻译:译者在 Weblate 界面中完成翻译。
  5. 合并回代码库:Weblate 将翻译结果提交为 Git commit,由维护者合并进主仓库。

makemessages命令是幂等的,具体表现为:

  • 只有当某个单数键在 Zulip 代码中不再使用时,才会将其从资源文件中删除;
  • 只有当对应的单数键消失时,才会删除复数键;
  • 不会覆盖已包含翻译文本的单数键的值。

这一行为可以在 zerver/management/commands/makemessages.py 的get_new_strings中看到实现:对英文 locale,翻译等于键本身;对其他语言,保留旧翻译(old_strings.get(k, "")),缺失的新字符串以空串填充。

makemessages 的源码级实现

Zulip 的makemessages命令(位于 zerver/management/commands/makemessages.py)并非 Django 自带命令的简单复用,而是通过扩展正则表达式 + monkey-patch Django 模板解析的方式同时处理两套模板系统:

  • 对于服务端,它扩展了 Djangomakemessages内部的template.block_reendblock_replural_reconstant_re,使 Django 的提取逻辑能够识别 Jinja2 的{% trans %}块语法与_()调用(见 makemessages.py);
  • 对于前端,它先扫描web/templates目录下所有.hbs文件,用正则提取{{t ...}}{{#tr}}...{{/tr}}等语法中的字符串(frontend_compiled_regexes,见 makemessages.py),再调用node_modules/.bin/formatjs extract --additional-function-names=$t,$t_htmlweb/src/**/*.jsweb/src/**/*.ts中抽取$t$t_html的参数字符串(见 makemessages.py)。

该命令还额外支持--frontend-source--frontend-output--frontend-namespace等自定义参数,默认分别指向web/templateslocaletranslations.json(见 makemessages.py)。提取时还会自动忽略docs/*templates/zerver/emails/custom/*var/*等目录(见 makemessages.py)。

翻译资源文件

所有翻译的"魔法"都发生在资源文件中:服务端资源文件位于locale/<lang_code>/LC_MESSAGES/django.po,Web 应用资源文件位于locale/<lang_code>/translations.json。仓库中的locale/目录下即为各语言的实际资源文件,例如中文对应locale/zh_Hans/,俄语对应locale/ru/,每个目录下均包含这两类文件。

从 web/src/i18n.ts 的intl初始化可以看到前端资源文件的消费方式:FormatJS 的createIntlpage_params.request_language作为 locale、en作为默认 locale,并从page_params.translation_data加载翻译字典;对"自上次同步翻译后新增、尚未翻译"的字符串,通过忽略IntlErrorCode.MISSING_TRANSLATION错误来保持静默(见 i18n.ts)。此外,i18n.ts 的initialize只把翻译完成度达到 5% 以上的语言列入语言选择列表,避免用户选到几乎未翻译的语言。

延伸阅读

如果你对译者侧的工作流感兴趣——包括如何加入翻译社区、注册 Weblate、按组件翻译、测试翻译、使用机器翻译辅助以及各语言翻译风格指南——请参阅 译者工作流 与 翻译风格指南。此外,HTML 模板文档系统介绍了 Zulip 中 Jinja2 与 Handlebars 模板的语法细节,是理解本指南中各类模板语法的基础。关于国际化的通用知识,社区普遍推荐 EdX 的 i18n 指南作为补充学习资料。

【免费下载链接】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),仅供参考

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

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

立即咨询