Metabase i18n 国际化管线全解析:从源码提取、Crowdin 同步到运行时产物与翻译违规报告
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
Metabase 的前后端跨 Clojure 与 TypeScript 两大技术栈,其国际化(i18n)由 bin/i18n/README.md 描述的一条「提取 → Crowdin 同步 → 产物生成」三段式流水线支撑:先从.clj/.cljc/.ts/.tsx源码中扫描翻译调用点生成 gettext 模板locales/metabase.po,翻译者在 Crowdin 上补全各语种的msgstr后,构建阶段再把这些.po文件加工成后端与前端运行时真正消费的翻译资源,并用一份自动化违规报告兜底质量问题。读完本文,你将掌握这条管线的每个环节、其背后的 Clojure 实现位置(主要在 bin/build/src/i18n/),以及当翻译出现invalid-message-format、skipped-arg-index、arg-count-mismatch三类问题时应该如何定位与修复。
一、管线总览:三个阶段、四种产物
Metabase 的翻译数据流可以概括为下面的 Mermaid 图(出自 bin/i18n/README.md):
三个阶段各自承担的任务是:
- Extraction(提取):扫描 Clojure 与 TypeScript 源码中的
trs/tru/ ttagt标记调用点,生成 gettext 源模板到locales/metabase.po。其中 ttag 是前端使用的翻译运行时库(在 package.json 中以"ttag": "1.7.21"及配套的babel-plugin-ttag、eslint-plugin-ttag引入)。 - Crowdin sync(同步):翻译者(译者/社区)在 Crowdin 每周定时运行(
cron: 30 5 * * 2,UTC 周二早上),调用 Crowdin GitHub Action 下载翻译并自动打开一个 PR。 - Artifact generation(运行时产物生成):读回已翻译的各语种
locales/*.po,先应用自动修复(autofix pass)修正翻译者可以无歧义修复的错误,再做校验扫描,最终输出:- 后端产物
resources/i18n/*.edn(由metabase.util.i18n.impl/translate通过java.text.MessageFormat消费); - 前端产物
resources/frontend_client/app/locales/*.json(由 ttag 在浏览器中消费); - 以及
target/i18n-violations.csv违规报告。
- 后端产物
bin/i18n/目录下正是编排这些环节的三个脚本:update-translation-template(提取并合并模板)、build-translation-resources(生成产物)、merge-translations(分支间合并翻译)。
二、Extraction:如何把源码中的翻译字符串变成.po模板
2.1 后端 pot 文件的手动构建
在项目根目录执行(clojure -X:build:build/i18n):
❯ clojure -X:build:build/i18n Created pot file at cli.pot Found 1393 forms for translations Grouped into 1313 distinct pot entries输出含义:共找到 1393 处翻译调用点(forms),由于 pot 文件中每条字符串只能有一个条目(grouped by message),归并成 1313 个不同的 pot 条目。
实际上,这条命令背后是 deps.edn 中的:build/i18nalias,其:exec-fn是i18n.create-artifacts/create-all-artifacts!(产物生成入口,见后文)。而模板提取则由 update-translation-template 脚本统一完成,它会分别构建三份 pot 文件再用 gettext 的msgcat合并成单一产物:
# 1) 前端 pot —— 用 babel 的 BABEL_ENV=extract 模式跑一遍 ttag 插件 BABEL_ENV=extract ./node_modules/.bin/babel --quiet -x .ts,.tsx -o /dev/null {enterprise/,}frontend/src # 随后把 ttag 的 "${ 0 }" 风格占位符替换成 xgettext 的 "{0}" 风格 sed -i".bak" -E 's/\$\{ *([0-9]+) *\}/{1}/g' "$POT_FRONTEND_NAME" # 2) 后端 pot —— 调用 i18n.enumerate/enumerate clojure -X:build i18n.enumerate/enumerate :filename "\"$POT_BACKEND_NAME\"" # 3) 自动仪表盘 pot clojure -M:generate-automagic-dashboards-pot # 4) 合并三份 pot msgcat --add-location=file -F "$POT_FRONTEND_NAME" "$POT_BACKEND_NAME" "$POT_AUTODASH_NAME" > "$POT_NAME"合并后的模板即locales/metabase.po,仓库中真实存在,例如其中的条目形如:
#: enterprise/frontend/src/metabase-enterprise/whitelabel/lib/loading-message.ts #: frontend/src/metabase/plugins/oss/core.ts msgid "Doing science..." msgstr ""2.2 源码扫描的原理:Grasp + 自定义 spec
后端提取不依赖 clang 之类的通用工具,而是使用 Grasp。
翻译相关的 var 集合定义在translation-vars中:
#{'metabase.util.i18n/trs 'metabase.util.i18n/tru 'metabase.util.i18n/deferred-trs 'metabase.util.i18n/deferred-tru 'metabase.util.i18n/trsn 'metabase.util.i18n/trun 'metabase.util.i18n/deferred-trsn 'metabase.util.i18n/deferred-trun}匹配用 spec::translate则要求一个非 vector 的 form,首符号能解析为上述 var,且带有参数:
(s/def ::translate (s/and (complement vector?) (s/cat :translate-symbol (fn [x] (and (symbol? x) (translation-vars (g/resolve-symbol x)))) :args (s/+ any?))))这些宏定义于 src/metabase/util/i18n.clj,例如:
tru/trs:分别翻译为「用户 locale」与「站点 locale」字符串;deferred-tru/deferred-trs:生成UserLocalizedString/SiteLocalizedString记录,把翻译动作推迟到str展开时,适用于编译期执行的代码;trun/trsn及 deferred 版本:处理复数的翻译,如(trun "{0} table" "{0} tables" n)。
enumerate 的扫描把调用点的文件名和行列号作为 metadata 一并返回,通过 REPL 可以直观看到:
enumerate=> (def single-file (str u/project-root-directory "/src/metabase/util.clj")) #'i18n.enumerate/single-file enumerate=> (map (juxt meta identity) (g/grasp single-file ::translate)) ([{:line 81, :column 13, :uri "file:/.../src/metabase/util.clj"} (trs "Maximum memory available to JVM: {0}" (format-bytes (.maxMemory (Runtime/getRuntime))))] [{:line 313, :column 33, :uri "file:/.../src/metabase/util.clj"} (tru "Timed out after {0}" (format-milliseconds timeout-ms))])2.3 从 form 到 pot 条目
拿到 form 后,form->messages负责抽取字符串字面量:既支持直接的字符串,也支持(str "Foo {0} " "Bar")这种字符串拼接(多个字符串字面量会被连接成一个完整 message),复数宏则从第三个参数取message-pl。随后group-results-by-string以 message 为 key 做归并,把同一字符串的所有源文件引用(#: path:line注释)折叠进一个条目——因为 pot 文件中每条字符串只能出现一次。最终processed->catalog用 jgettext 的Catalog/Message写回 pot。
create-pot-file!是整个流程的便捷入口,返回统计信息:
enumerate=> (create-pot-file! single-file "pot.pot") Created pot file at pot.pot {:valid-usages 3, :entry-count 4, :bad-forms ()}生成的 pot 内容如下(注意#:源引用、msgid/msgstr结构,以及java.text.MessageFormat风格的{0}占位符):
# Copyright (C) 2022 Metabase <docs@metabase.com> # This file is distributed under the same license as the Metabase package #, fuzzy msgid "" msgstr "" "Project-Id-Version: 1.0\n" "Report-Msgid-Bugs-To: docs@metabase.com\n" "POT-Creation-Date: 2022-07-22 14:03-0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: /.../src/metabase/sync/analyze/fingerprint/fingerprinters.clj msgid "Error generating fingerprint for {0}" msgstr "" #: metabase/util.clj:81 msgid "Maximum memory available to JVM: {0}" msgstr ""2.4 Overrides:处理宏遮蔽导致扫不到的调用点
这里有一个值得注意的坑:指定单个文件src/metabase/util.clj扫描,结果却出现了来自fingerprinters.clj的条目。原因在于那条trs位于一个会展开为defrecord的宏内部,宏展开时的 quote 改变了 form 的结构——'(trs "foobar") 实际是(clojure.core/seq (clojure.core/concat ...))形态的嵌套 seq,无法匹配自定义 spec(参见 Grasp 的 issue #28)。
对策是维护一个手工 override 列表(overrides定义于 enumerate.clj),把这类 "grasp 找不到" 的形式显式补充进去:
;; 找不到 fingerprinters 里的用法 —— 宏展开成 defmethod,quoting 改变了 seq 形态导致 spec 不匹配 [{:file "/src/metabase/analyze/fingerprint/fingerprinters.clj" :message "Error generating fingerprint for {0}"}]这些 override 会在group-results-by-string阶段与扫描结果合并((concat overrides)),确保不会漏掉任何需要翻译的字符串。
2.5 源码侧占位符校验
值得顺带一提的是,翻译字符串的占位符一致性不仅在构建期被校验,在宏展开期也有防线:metabase.util.i18n/validate-number-of-args(见 src/metabase/util/i18n.clj)会在编译 Clojure 源码时就检查(deferred-)trs/tru的格式串里{N}占位符数量与实参数量是否一致,例如「missing some {} placeholders」或「expects %d args, got %d」这类断言。纯校验函数集中在 src/metabase/util/i18n/validation.clj,且刻意不依赖 jgettext 等构建期依赖,因此可以被「宏展开期检查、构建期.po扫描器、产物回归测试」三层共享复用。复数宏trsn/trun还额外要求只允许单个{0}占位符(用于数量 n)。
三、Artifact generation:把翻译变成后端与前端各自消费的产物
3.1 一键运行整条产物流水线
翻译者在 Crowdin 上补全locales/<locale>.po之后,在项目根目录执行:
❯ ./bin/i18n/build-translation-resources该脚本本质就是对clojure -X:build:build/i18n的封装(见 bin/i18n/build-translation-resources),最终调用i18n.create-artifacts/create-all-artifacts!(bin/build/src/i18n/create_artifacts.clj)。
每个 locale 的处理步骤(create-artifacts-for-locale!):
- 解析
.po:i18n.common/po-contents用 jgettext 的PoParser把文件解析为{:headers {...} :messages [...]},其中每条 message 含:id、:id-plural、:str、:str-plural、:fuzzy?、:plural?、:source-references、:context等字段。解析函数位于 bin/build/src/i18n/common.clj。 - 自动修复:
i18n.autofix/autofix-po-contents先修正可无歧义修复的译者错误(详见下文)。 - 校验扫描:
i18n.validation/invalid-messages-in-po对修复后的内容产出违规 map 序列(详见第四、五节)。 - 推导丢弃集合并写产物:用
i18n.validation/drop-from-build?决定哪些 msgid 要被剔除(见下节 drop policy),再把修复后的 po-contents 连同丢弃集合交给前后端 writer;被丢弃的 msgid 运行时回退为英文。 - 聚合报告:所有 locale 完成后,把全部违规聚合写入
target/i18n-violations.csv与target/i18n-violations.edn。
create-all-artifacts!还会先调用i18n.pseudo-locale/generate-pseudo-locale-po!生成一个en-ZZ伪语言环境(见第七节),并生成resources/locales.clj(声明可用 locale 列表的自动生成文件,标注「DO NOT EDIT」)。
3.2 源码所在地
产物生成管线的「单一事实来源」位于bin/build/src/i18n/:
| 文件 | 职责 |
|---|---|
| autofix.clj | 校验前修复(如撇号转义) |
| validation.clj | 扫描器 + drop policy |
| create_artifacts.clj | 编排器 + 报告写入 |
| create_artifacts/backend.clj | 后端.edn写入器 |
| create_artifacts/frontend.clj | 前端.json写入器 |
| common.clj | .po解析器与共享工具(backend-message?、po-contents、locales) |
3.3 后端.edn产物与运行时消费
后端 writer 只保留backend-message?为真的消息——判定规则是 message 的任一:source-references(按空格和冒号拆分后)以.clj或.cljc结尾,即字符串来自 Clojure 源码。随后剔除属于 drop-msgids 的条目,把:headers与:messages以 UTF-8 写入resources/i18n/<locale>.edn。复数条目以向量形式存储({msgid ["单数" "复数"]}结构)。
运行时消费逻辑在 src/metabase/util/i18n/impl.clj:
translate先按用户 locale / 站点 locale 读取对应的.edn资源(io/resource "i18n/<locale>.edn"),用get-in translations [:messages format-string]查表;- 查不到时先尝试 fallback locale(如
pt_PT→pt_BR,见fallback-locale),最终回退为英文原串; - 最终用
java.text.MessageFormat做占位符插值; - 整体包在 try/catch 中:一旦翻译串本身格式非法(如未转义撇号),会记录错误并回退英文——这正是构建期要预筛
invalid-message-format的根本原因。
3.4 前端.json产物与 ttag
前端 writer(frontend.clj)则相反:frontend-message?要求 source-references 匹配frontend|cljs|cljc。它的输出是 ttag 运行时能直接消费的 JSON,关键点有两处:
- 占位符风格转换:xgettext/MessageFormat 的
{0}被替换成 ttag 的${ 0 }风格(->ttag-reference)。 - 以 msgctxt 为顶层 key 组织:ttag 按
(context, msgid)二元组查表,因此顶层 map 以消息的msgctxt为 key(无 context 的归到"")。这样即便两条消息共享同一msgid,只要 context 不同就能各自保有自己的翻译——例如Year既作为ngettext("Year", "Years", n)的复数形式存在、又作为c("Date granularity option").tYear`` 的粒度选项出现;若仅按msgid归并,后写入的条目会覆盖前者导致翻译丢失。
文件命名上把 locale 中的-替换为_(如pt-BR→pt_BR.json)写入resources/frontend_client/app/locales/。前端源码侧则通过import { t } from "ttag"使用模板标签(例如 UserPasswordForm.tsx 中的t`Current password`)。
3.5 Autofix pass:先修再验,保证「修复与产物一致」
autofix 在每个 locale 上、在校验之前对解析出的 po-contents 运行一次。它默默纠正「无歧义可修复」的译者错误,使本会失败的翻译能够正确发布。由于校验器和产物 writer 都消费修复后的内容,三者对「最终上线的到底是什么」保持一致。
当前唯一自动修复(且仅针对后端字符串——前端msgstr原样通过,因为 ttag 的转义规则不同):
- 撇号转义:与单词相邻的单个撇号(如加泰罗尼亚语
d'aquí、英语It's)会被翻倍成'',这样java.text.MessageFormat会将其视为字面字符而非转义界定符。已经成对的撇号,或紧挨{/}的撇号(即刻意构造的 MessageFormat 转义,如'{0}')则原样通过。
实现的正则(autofix.clj)会避开非单词字符(包括{/})邻接的撇号:
(def ^:private apostrophe-regex #"(?<![^a-zA-Z0-9\s\u00C0-\u017F])'(?![^a-zA-Z0-9\s\u00C0-\u017F])")正因为 autofix 在扫描器之前执行,仅犯撇号拼写错误的翻译永远不会出现在违规报告里。如果你在报告中看到d'aquí风格的条目,说明 autofix 够不到它(大概率是非后端上下文),需要直接去 Crowdin 修复。
扩展 autofix 层的原则(文档中明确约定):只有当修复 (a) 确定无歧义、(b) 语义中立(渲染输出不变)、(c) 覆盖常见译者错误时,才应加入i18n.autofix/autofix-po-contents;其余情况应让其在违规报告中浮现。
3.6 Drop policy:什么才值得从产物中剔除
drop-from-build?只对「运行时会抛异常、无论如何都会触发英文回退」的违规返回true——即:invalid-message-format。这是因为MessageFormat构造器对非法 pattern 会抛IllegalArgumentException,运行时必然落入 try/catch 的英文回退分支;既然如此,构建期直接剔除反而能保证.edn/.json产物干净。
反之:skipped-arg-index与:arg-count-mismatch虽然渲染不完美但不会抛异常,因此保留在产物中——宁可让用户看到「大部分本地化 + 个别字面{N}」的文本,也不整体回退成纯英文。用msgids-to-drop汇总时若某复数消息的任一形态命中可丢弃违规,则整条消息被剔除。
四、Violations report:构建产出的翻译体检表
每次产物生成都会输出一份违规报告,两种形态并存:
target/i18n-violations.csv——每条违规一行,符合 RFC 4180,可用任意表格软件打开;这是 CI 中作为i18n-violationsartifact 上传的文件(见 .github/workflows/translation-update.yml,path: target/i18n-violations.*,保留 30 天)。target/i18n-violations.edn——同一批数据以 Clojure map 向量形式存在,供程序化消费。
自动生成的 translation-update PR 还会在摘要评论中给出按类型统计的条数以及 artifact 下载直链(workflow 里用awk解析 CSV 汇总各类型计数)。
4.1 CSV 的列定义
| Column | Meaning |
|---|---|
locale | 发现违规的 locale(如fr、de、pt-BR、ar-SA) |
types | 该行的违规类型集合,以+连接(如skipped-arg-index+arg-count-mismatch),见后文 |
dropped | true表示该翻译被排除在.edn/.json产物之外(运行时会回退英文);false表示虽有问题仍保留在产物中(运行时会以可见问题渲染) |
backend | true表示 msgid 源自 Clojure 源码(.clj/.cljc),false表示源自前端源码(.ts/.tsx/.js/.jsx)。这决定应用哪套格式系统规则 |
plural_index | 非复数串为空;复数串为受影响形态在msgstr[N]数组中的下标(0、1、…) |
msgid | 英文单数源串——翻译者在 Crowdin 里看到的 key |
msgid_plural | 英文复数形式(如果有)。非复数为空 |
msgstr | 未通过校验的译串——翻译者实际产出的文本 |
error | 仅invalid-message-format行有值——java.text.MessageFormat的异常消息(如Unmatched braces in the pattern.),其余类型为空 |
expected_args | arg-count-mismatch行:可接受的占位符数量(由msgid与msgid_plural推导),逗号分隔(如0,1表示单数 0 个、复数 1 个占位符) |
actual_args | arg-count-mismatch行:msgstr中实际用到的不同占位符下标个数 |
source_refs | 空格分隔的path:line指针,指向该 msgid 在源码中的使用位置 |
这些列在create_artifacts.clj的violations-csv-columns与violation->row中定义,写入前全部用(fnil str "")把 nil 归一为空字符串。
4.2 三类违规类型详解
invalid-message-format:译文根本无法作为 MessageFormat 解析
msgstr不是合法的java.text.MessageFormatpattern——构造器会抛IllegalArgumentException。典型诱因:
- 花括号不配对(
{0或缺少正确转义的{{...}}); - 使用
{login}这类命名占位符——Java MessageFormat 只认数字下标。
这类行恒为dropped=true。因为运行时MessageFormat构造器就会抛异常、触发 try/catch 英文回退,构建期提前剔除可保证.edn产物干净无雷。
译者行动:去 Crowdin 修复
msgstr,通常意味着对齐英文字符串的转义约定(字面'双写、花括号配对等)。
skipped-arg-index:占位符下标不连续
译文的{N}占位符引用了不连续下标(如只出现{0}和{2}而没有{1}),或同一下标被多次使用造成缺口。运行时缺失下标会字面渲染({2}作为四个字符{2}原样出现在输出里)。
这类行dropped=false——翻译保留在产物中,用户看到的是「绝大部分本地化文本 + 个别{N}瑕疵」,而非整体英文回退。
译者行动:让占位符连续——
{0}、{1}、{2}、……逐一排列。
底层判定逻辑(src/metabase/util/i18n/validation.clj 的skipped-arg-index?)正是比对MessageFormat.getFormats与getFormatsByArgumentIndex的数量是否一致。
arg-count-mismatch:占位符数量与源串不匹配
译文的占位符数量匹配不上英文源串的任一可接受数量。expected_args是「可接受数量」集合(含单数与复数的数量,若有复数),actual_args是译文实际数量。运行时缺失占位符会字面渲染({0}原样出现),或多出的调用方参数被静默丢弃。
这类行同样dropped=false。常见成因是翻译者自行加了个{0}(例如为了本地复数化)而英文源没有,或者漏掉了英文要求的{0}。
译者行动:与英文源的占位符数量对齐。若源串复数形态占位符数不同,也要匹配复数形态。
注意(来自原文档的特别提示):罗曼语族的撇号错误(如加泰罗尼亚语
d'aquí在 MessageFormat 转义语义下「吞掉」结尾的{0})本会触发该违规——autofix pass 会在上游处理,因此通常不应出现在报告中;但这些错误仍然存在于 Crowdin 中。
4.3 三类违规的构建期/运行时行为对照
| Violation type | Build-time action | Runtime behavior |
|---|---|---|
invalid-message-format | Dropped from artifact | English fallback |
skipped-arg-index | Kept in artifact | Localized text with literal{N}for missing indices |
arg-count-mismatch | Kept in artifact | Localized text with literal{N}for missing placeholders, or extra args silently dropped |
4.4 如何高效使用这份报告
- 按
locale排序看每种语言的问题——译者常存在系统性毛病(如加泰罗尼亚语译者反复忘记双写撇号)。 - **筛选
dropped=true**看哪些条目在静默回退英文——这是对 locale 用户最可见的体验回退。 - **筛选
types=invalid-message-format**优先处理 Crowdin bug——这类修复优先级永远最高。 - **利用
source_refs**判断违规影响哪个 UI、是高流量字符串还是边角用例。
每一行都足以在 Crowdin 中定位对应字符串:msgid是源 key,locale告诉你要打开哪种语言。
五、前端扫描与检查的差异:为什么前后端规则不同
前端字面量来自 ttag(t标签 /ngettext复数),其占位符风格在 pot 里被统一成{0},但运行时由 ttag 解析(且会再次转回${ 0 })。因此校验时前后端规则不同:后端字符串全部走java.text.MessageFormat三连检(格式合法性、跳号、数量匹配);前端字符串只用与格式系统无关的检查(跳号、数量匹配),借助基于正则的辅助函数(regex-arg-count、regex-skipped-arg-index?),避免MessageFormat对撇号的误解析(见validation.clj的check-violations:(if backend? validation/message-format-arg-count validation/regex-arg-count))。
同时,为后端字符串做三类检查时会复用一次性的MessageFormat解析结果:只有格式合法才继续测跳号与数量匹配(format-error存在时跳过后续检查)。所有命中类型会一并收集、不做短路,因此单行可能出现多个 type(如skipped-arg-index+arg-count-mismatch同时触发)。
六、日常维护工具:merge-translations 与 CI 每周同步
6.1 分支间合并翻译
merge-translations 用于把指定分支的翻译合并进当前分支。典型场景:把翻译 PR 反向移植(backport)到发布分支后,该 PR 可能删掉了当前分支仍需要的翻译;运行该脚本可以把发布分支的翻译合并回 backport 分支。冲突时默认以当前分支的翻译为准,如需给另一分支更高优先级则加-o参数:
./bin/i18n/merge-translations <branch-name> [-o]注意需在仓库根目录运行,因为脚本假设.po文件位于locales/。
6.2 CI 的每周自动化
.github/workflows/translation-update.yml(name: "Synchronize Translations")每周二 05:30 UTC 通过schedule触发,也支持workflow_dispatch手动触发。流程要点:
- 安装 gettext,通过 Crowdin GitHub Action(读取 locales/crowdin.yml)
download翻译,并排除一批如el、et、es-MX等未完成 locale; - 依次运行
./bin/i18n/update-translation-template(更新模板并提交)、./bin/i18n/build-translation-resources(重建产物并提交); if: always()上传target/i18n-violations.*作为i18n-violationsartifact;- 运行 Crowdin
upload同步新源字符串,最后通过 GitHub CLI 创建 PR。
七、进阶补充:en-ZZ伪语言环境与测试
产物构建前还会生成en-ZZ伪语言环境(见 bin/build/src/i18n/pseudo_locale.clj)。它把locales/metabase.po里的每条英文字符串转换为[zz] {English},产出locales/en-ZZ.po,再走正常构建管线。E2E 测试可把 UI 切到en-ZZ并断言可预测的[zz] ...前缀,而不依赖 Crowdin 随时可能更新的真实语言字符串。该文件被.gitignore忽略、每次构建重新生成,Crowdin 亦不感知它(不在crowdin.yml中)。
后端通过src/metabase/util/i18n/impl.clj的available-locale?把en_ZZ加入可设置 locale 白名单(extra-available-locales),前端语言选择器仅在设置MB_ENABLE_TEST_LOCALES=true时才显示测试伪语言环境(test-only-locales、show-test-locales?),其显示名被覆盖为更清晰的 "English (ZZ)"。
八、实战清单:把一条坏翻译从发现到修复串起来
结合上述内容,一次完整的「翻译体检与修复」工作流建议如下:
- 本地执行
./bin/i18n/build-translation-resources重建全部产物并生成最新报告; - 打开
target/i18n-violations.csv,按locale排序看系统性译员问题; - 先处理
dropped=true且types=invalid-message-format的行——它们已在静默回退英文,是体验影响最大的问题;结合error列的MessageFormat异常消息(如Unmatched braces in the pattern.)判断病因; - 用
source_refs定位受影响源码(如 src/metabase/util.clj 中的tru/trs调用),评估该字符串是否高流量; - 到 Crowdin 中按
msgid+locale定位并修复;注意后端串撇号须双写、花括号须配对、占位符下标须连续且数量与英文源一致; - 若嫌流程繁琐,可退而求其次直接用
create-pot-file!在 REPL 中对单个文件做小范围抽取验证(参考 enumerate.clj 末尾的comment示例)。
最后再次强调核心机制:校验发生在 autofix 之后、产物写入之前;autofix 纠正「能无歧义修好的」,drop policy 剔除「运行时会抛异常的」,二者共同保证进入resources/i18n/*.edn与resources/frontend_client/app/locales/*.json的每一条翻译都至少「不会让运行时崩溃、不会整段回退英文」。理解了这条流水线与三类违规的取舍逻辑,你就能既当好翻译质量守门人,也能在需要时安全地扩展 bin/build/src/i18n/ 下的构建工具链。
【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考