Metabase i18n 国际化管线全解析:从源码提取、Crowdin 同步到运行时产物与翻译违规报告
2026/9/10 16:03:36 网站建设 项目流程

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-formatskipped-arg-indexarg-count-mismatch三类问题时应该如何定位与修复。

一、管线总览:三个阶段、四种产物

Metabase 的翻译数据流可以概括为下面的 Mermaid 图(出自 bin/i18n/README.md):

三个阶段各自承担的任务是:

  1. Extraction(提取):扫描 Clojure 与 TypeScript 源码中的trs/tru/ ttagt标记调用点,生成 gettext 源模板到locales/metabase.po。其中 ttag 是前端使用的翻译运行时库(在 package.json 中以"ttag": "1.7.21"及配套的babel-plugin-ttageslint-plugin-ttag引入)。
  2. Crowdin sync(同步):翻译者(译者/社区)在 Crowdin 每周定时运行(cron: 30 5 * * 2,UTC 周二早上),调用 Crowdin GitHub Action 下载翻译并自动打开一个 PR。
  3. 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-fni18n.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!):

  1. 解析.poi18n.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。
  2. 自动修复i18n.autofix/autofix-po-contents先修正可无歧义修复的译者错误(详见下文)。
  3. 校验扫描i18n.validation/invalid-messages-in-po对修复后的内容产出违规 map 序列(详见第四、五节)。
  4. 推导丢弃集合并写产物:用i18n.validation/drop-from-build?决定哪些 msgid 要被剔除(见下节 drop policy),再把修复后的 po-contents 连同丢弃集合交给前后端 writer;被丢弃的 msgid 运行时回退为英文。
  5. 聚合报告:所有 locale 完成后,把全部违规聚合写入target/i18n-violations.csvtarget/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-contentslocales

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_PTpt_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,关键点有两处:

  1. 占位符风格转换:xgettext/MessageFormat 的{0}被替换成 ttag 的${ 0 }风格(->ttag-reference)。
  2. 以 msgctxt 为顶层 key 组织:ttag 按(context, msgid)二元组查表,因此顶层 map 以消息的msgctxt为 key(无 context 的归到"")。这样即便两条消息共享同一msgid,只要 context 不同就能各自保有自己的翻译——例如Year既作为ngettext("Year", "Years", n)的复数形式存在、又作为c("Date granularity option").tYear`` 的粒度选项出现;若仅按msgid归并,后写入的条目会覆盖前者导致翻译丢失。

文件命名上把 locale 中的-替换为_(如pt-BRpt_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 的列定义

ColumnMeaning
locale发现违规的 locale(如frdept-BRar-SA
types该行的违规类型集合,以+连接(如skipped-arg-index+arg-count-mismatch),见后文
droppedtrue表示该翻译被排除.edn/.json产物之外(运行时会回退英文);false表示虽有问题仍保留在产物中(运行时会以可见问题渲染)
backendtrue表示 msgid 源自 Clojure 源码(.clj/.cljc),false表示源自前端源码(.ts/.tsx/.js/.jsx)。这决定应用哪套格式系统规则
plural_index非复数串为空;复数串为受影响形态在msgstr[N]数组中的下标(01、…)
msgid英文单数源串——翻译者在 Crowdin 里看到的 key
msgid_plural英文复数形式(如果有)。非复数为空
msgstr未通过校验的译串——翻译者实际产出的文本
errorinvalid-message-format行有值——java.text.MessageFormat的异常消息(如Unmatched braces in the pattern.),其余类型为空
expected_argsarg-count-mismatch行:可接受的占位符数量(由msgidmsgid_plural推导),逗号分隔(如0,1表示单数 0 个、复数 1 个占位符)
actual_argsarg-count-mismatch行:msgstr中实际用到的不同占位符下标个数
source_refs空格分隔的path:line指针,指向该 msgid 在源码中的使用位置

这些列在create_artifacts.cljviolations-csv-columnsviolation->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.getFormatsgetFormatsByArgumentIndex的数量是否一致。

arg-count-mismatch:占位符数量与源串不匹配

译文的占位符数量匹配不上英文源串的任一可接受数量。expected_args是「可接受数量」集合(含单数与复数的数量,若有复数),actual_args是译文实际数量。运行时缺失占位符会字面渲染({0}原样出现),或多出的调用方参数被静默丢弃。

这类行同样dropped=false。常见成因是翻译者自行加了个{0}(例如为了本地复数化)而英文源没有,或者漏掉了英文要求的{0}

译者行动:与英文源的占位符数量对齐。若源串复数形态占位符数不同,也要匹配复数形态。

注意(来自原文档的特别提示):罗曼语族的撇号错误(如加泰罗尼亚语d'aquí在 MessageFormat 转义语义下「吞掉」结尾的{0})本会触发该违规——autofix pass 会在上游处理,因此通常不应出现在报告中;但这些错误仍然存在于 Crowdin 中。

4.3 三类违规的构建期/运行时行为对照

Violation typeBuild-time actionRuntime behavior
invalid-message-formatDropped from artifactEnglish fallback
skipped-arg-indexKept in artifactLocalized text with literal{N}for missing indices
arg-count-mismatchKept in artifactLocalized text with literal{N}for missing placeholders, or extra args silently dropped

4.4 如何高效使用这份报告

  1. locale排序看每种语言的问题——译者常存在系统性毛病(如加泰罗尼亚语译者反复忘记双写撇号)。
  2. **筛选dropped=true**看哪些条目在静默回退英文——这是对 locale 用户最可见的体验回退。
  3. **筛选types=invalid-message-format**优先处理 Crowdin bug——这类修复优先级永远最高。
  4. **利用source_refs**判断违规影响哪个 UI、是高流量字符串还是边角用例。

每一行都足以在 Crowdin 中定位对应字符串:msgid是源 key,locale告诉你要打开哪种语言。

五、前端扫描与检查的差异:为什么前后端规则不同

前端字面量来自 ttag(t标签 /ngettext复数),其占位符风格在 pot 里被统一成{0},但运行时由 ttag 解析(且会再次转回${ 0 })。因此校验时前后端规则不同:后端字符串全部走java.text.MessageFormat三连检(格式合法性、跳号、数量匹配);前端字符串只用与格式系统无关的检查(跳号、数量匹配),借助基于正则的辅助函数(regex-arg-countregex-skipped-arg-index?),避免MessageFormat对撇号的误解析(见validation.cljcheck-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手动触发。流程要点:

  1. 安装 gettext,通过 Crowdin GitHub Action(读取 locales/crowdin.yml)download翻译,并排除一批如eletes-MX等未完成 locale;
  2. 依次运行./bin/i18n/update-translation-template(更新模板并提交)、./bin/i18n/build-translation-resources(重建产物并提交);
  3. if: always()上传target/i18n-violations.*作为i18n-violationsartifact;
  4. 运行 Crowdinupload同步新源字符串,最后通过 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.cljavailable-locale?en_ZZ加入可设置 locale 白名单(extra-available-locales),前端语言选择器仅在设置MB_ENABLE_TEST_LOCALES=true时才显示测试伪语言环境(test-only-localesshow-test-locales?),其显示名被覆盖为更清晰的 "English (ZZ)"。

八、实战清单:把一条坏翻译从发现到修复串起来

结合上述内容,一次完整的「翻译体检与修复」工作流建议如下:

  1. 本地执行./bin/i18n/build-translation-resources重建全部产物并生成最新报告;
  2. 打开target/i18n-violations.csv,按locale排序看系统性译员问题;
  3. 先处理dropped=truetypes=invalid-message-format的行——它们已在静默回退英文,是体验影响最大的问题;结合error列的MessageFormat异常消息(如Unmatched braces in the pattern.)判断病因;
  4. source_refs定位受影响源码(如 src/metabase/util.clj 中的tru/trs调用),评估该字符串是否高流量;
  5. 到 Crowdin 中按msgid+locale定位并修复;注意后端串撇号须双写、花括号须配对、占位符下标须连续且数量与英文源一致;
  6. 若嫌流程繁琐,可退而求其次直接用create-pot-file!在 REPL 中对单个文件做小范围抽取验证(参考 enumerate.clj 末尾的comment示例)。

最后再次强调核心机制:校验发生在 autofix 之后、产物写入之前;autofix 纠正「能无歧义修好的」,drop policy 剔除「运行时会抛异常的」,二者共同保证进入resources/i18n/*.ednresources/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),仅供参考

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

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

立即咨询