Zulip 国际化i18n开发实战指南从字符串标记到翻译管线【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulipZulip 从设计之初就将国际化internationalization简称 i18n作为一等公民允许用户以自己偏好的语言查看整个 UI。本文基于仓库中的 docs/translating/internationalization.md系统讲解 Zulip 中哪些字符串需要翻译、如何在不同技术栈JavaScript / Handlebars / Jinja2 / Python中正确标记字符串、复数与列表如何处理、以及从makemessages到 Weblate 的完整翻译管线。读完本文你将掌握在 Zulip 中为 Web 前端与 Python 服务端添加可翻译字符串的完整实战方案并理解底层源码的实现机制。国际化如何影响 Zulip 的 UIZulip 要求开发者时刻牢记一个核心事实文本宽度不是常量。表达同一含义所需的字符串宽度在不同语言之间差异极大例如德语、俄语往往比英语更长日语则通常更短。因此绝不能把按钮或控件按英文宽度硬编码排版后就指望它在所有语言下都显示良好。在提交代码前建议按以下方法自测将字符串长度人为拉长 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 中的翻译语法先记住两条通用准则翻译函数必须接收待翻译的字符串字面量本身而不是存放字符串的变量。否则用于从项目中抽取字符串交给译者的解析器将找不到你的字符串。Zulip 服务端使用 Jinja2 模板系统Web 应用使用 HandlebarsHTML 模板文档中收录了这两个系统语法与行为的详细说明。Web 应用翻译FormatJS ICU MessageFormatWeb 应用的翻译统一基于 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_htmlhtml_content $t_html({defaultMessage: HTML with a {variable}}, {variable: Variable value}); $(#foo).html(html_content);翻译字符串内允许出现的 HTML 标签是受限的只有 web/src/i18n.ts 中default_html_elements枚举的无属性简单标签b、code、em、i、kbd、p、strong以避免向译者暴露 HTML 细节。若需要链接等更复杂的标记可以为该翻译局部定义自定义 HTML 标签或改用 Handlebars 模板$t_html( {defaultMessage: bHTML/b linking to the z-linklogin 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}} pBlock of English text./p {{/tr}} {{#tr}} pBlock 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}} bHTML/b linking to the z-linklogin 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 %}绝不能像下面这样拆句标记否则将无法正确翻译词序问题# Dont do this! {{ _(This string will have) }} {{ value }} {{ _(inside) }}PythonPython 代码中使用_()函数标记字符串按 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 的 lintertools/lint会尝试校验上述用法是否合规。翻译流程端到端工具链Zulip 的翻译工具链整体流程如下标记字符串按上文服务端与 Web 应用各自的方式完成标记。生成资源文件运行./manage.py makemessages。该命令会为每种语言生成两份资源文件——Web 应用字符串写入translations.json服务端字符串写入django.po。提交并扫描资源文件提交后会被 Weblate 自动扫描。译者翻译译者在 Weblate 界面中完成翻译。合并回代码库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_re、endblock_re、plural_re与constant_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_html从web/src/**/*.js与web/src/**/*.ts中抽取$t、$t_html的参数字符串见 makemessages.py。该命令还额外支持--frontend-source、--frontend-output、--frontend-namespace等自定义参数默认分别指向web/templates、locale与translations.json见 makemessages.py。提取时还会自动忽略docs/*、templates/zerver/emails/custom/*、var/*等目录见 makemessages.py。翻译资源文件所有翻译的魔法都发生在资源文件中服务端资源文件位于locale/lang_code/LC_MESSAGES/django.poWeb 应用资源文件位于locale/lang_code/translations.json。仓库中的locale/目录下即为各语言的实际资源文件例如中文对应locale/zh_Hans/俄语对应locale/ru/每个目录下均包含这两类文件。从 web/src/i18n.ts 的intl初始化可以看到前端资源文件的消费方式FormatJS 的createIntl以page_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),仅供参考
