34 种语言、49 个 PO 文件:Penpot 多语言本地化工作流完整实战
【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot
设计团队打开 Penpot 想以中文评审界面,整站却是英文;有人补了一条翻译,界面上又冒出auth.check-mail这种带点号的文本——这大概率不是 bug,而是翻译缺失的显式信号。Penpot 作为开源设计与原型平台,其多语言能力由三层构成:frontend/src/app/util/i18n.cljs里的翻译引擎、frontend/translations/下的 49 个 PO 文件、以及 Weblate 平台上的社区协作流程。本文按实际操作路径走一遍:先把界面切到目标语言,再读懂 PO 文件结构,然后本地验证改动,最后把翻译推回主线。
如何把 Penpot 本地界面切到 34 种语言之一
先拉取代码并启动开发环境:
git clone https://gitcode.com/GitHub_Trending/pe/penpot cd penpot ./manage.sh run-devenv界面显示哪种语言,由i18n.cljs里的supported-locales表决定,共 34 个条目,覆盖zh_cn、zh_hant、ar、ja_jp、fr_ca等。语言确定分两条路:启动时autodetect读取navigator.language,经parse-locale归一化(zh-CN变成zh_cn,带国家码不支持时回退到纯语言码zh);也可以在设置里显式调用set-locale。两种路径都会校验白名单,查不到就落到cf/default-language(值为"en")。
locale 定下来后,load函数动态导入翻译表并挂到全局变量:
(let [path (str "./translation." locale ".js?version=" cf/version-tag)] (->> (mod/import path) (p/fmap (fn [result] (unchecked-get result "default"))) (p/fnly (fn [data cause] (if cause (js/console.error "unexpected error on fetching locale" cause) (set-translations locale data))))))而所有 UI 文案统一走tr查表:
(defn tr ([code] (t *current-locale* code)) ([code & args] (apply t *current-locale* code args)))用一句人话概括:组件里不硬编码任何字符串,文案全部由tr按当前 locale 查表返回。切语言本质上是换一张表,所以前端不存在"改代码换语言"这一步。
切完语言后你会发现一部分文案还是英文——这就是缺失回退到en的结果。要补上它,得先读懂 PO 文件。
如何读懂 frontend/translations 的 49 个 PO 翻译文件
frontend/translations/下有 49 个.po文件,按 ISO 语言码命名(zh_CN.po、ar.po、fr_CA.po等),比运行时支持的 34 个多出的部分是尚未接入切换器的储备语言。每条翻译的长这样(取自zh_CN.po的真实条目):
#: src/app/main/ui/auth/register.cljs:214 msgid "auth.already-have-account" msgstr "已经有账号了?"三个信息点都有用:msgid是键名,msgstr是译文,#:开头的注释指向该键在源码中的引用位置(文件加行号),排查上下文不用全局搜索。此外还有两个标志位:unused表示这个键已不再被任何源码引用,fuzzy表示译文待复核。文件头部的Plural-Forms: nplurals=2; plural=(n != 1)声明复数规则,中文和英文一样按"等于 1 / 不等于 1"分两支。
复数在代码侧的写法见frontend/src/app/main/ui/dashboard/fonts.cljs:
[:span (tr "dashboard.fonts.fonts-added" (i18n/c (count fonts)))](i18n/c ...)把参数标记为计数值;t函数检测到翻译值是指数组时,按计数等于 1 还是其他取msgstr[0]或msgstr[1]。这就是为什么.po里同一个键会同时出现msgid和msgid_plural两条。
回退链路值得记清楚:目标语言查不到该键,回落到en;en也查不到,t直接返回键名本身。界面上看到带点号的文本,就按这条链路反查是哪个环节漏了。
PO 文件改好了,下一步是本地验证和批量维护。
如何本地验证翻译改动:watch 热更新与 rehash、sync 命令
开发服务自带热更新。frontend/scripts/watch.js第 95 行起注册了对 translations 目录的监听:
log.info("watch: translations (~)"); h.watch("translations", null, async function (path) { ... })也就是说改完.po文件保存即可,前端会自动重新编译加载,不用手动重启。
批量维护用frontend/目录下的pnpm translations <子命令>,脚本是frontend/scripts/translations.js,提供四个子命令(以脚本自带的 help 输出为准):
rehash:用正则\(tr\s+"([\w\.\-]+)"扫描src/下所有.clj/.cljs/.cljc文件,把源码里出现但en.po里没有的键补录进去(标fuzzy并写入引用位置),同时把源码中已不引用的键标成unused,结束时打印Found N used translations和Found N unused strings两个数字;sync:以en为基准对齐其他语言文件的键集合,基准里不存在的键直接从各语言文件中删除;fuzzy <prefix>:把某前缀下的所有条目批量标记为 fuzzy,适合英文原文批量改动后要求各语言重新确认;delete <prefix>:按前缀删除条目。
日常节奏是:新增文案后先跑rehash补键,各语言翻译完成后再跑sync清掉孤儿键。本地验证通过后,改动要进入主线还得走审核流程。
如何把翻译推回主线:Weblate 协作与 waiting for review 状态机
Penpot 的官方翻译平台是 Weblate,流程定义在docs/contributing-guide/translations/index.njk。入场的门槛是:注册 Weblate 账号,然后在仓库提交 issue,写清三样东西——要翻译的语言、翻译类型(新语言 / 补字符串 / 改已有译文)、你的 Weblate 用户名,由团队授予对应权限。
上图为 Weblate 的语言列表页,每行显示一种语言的进度,铅笔图标用于进入该语言的字符串编辑。
三类操作的状态流转不同:
- 新增语言:在语言列表页点 "Start new translation",选择语言后开始逐条翻译;
- 补充缺失字符串:点语言行的编辑图标,找到未翻译条目填入译文并保存,状态自动变为 "waiting for review";
- 修改已批准的译文:进入该语言点 Browse,有权限直接 Save(同样进入待审核),没有权限则点 Suggest,条目状态标记为 "Approved strings with suggestions"。
上图是单个语言的字符串列表,未翻译条目一目了然,补漏时按这个清单逐条处理即可。
团队会定期检查待审核字符串,确认无误后批准,批准的结果随下个版本发布。界面文案这条链路到此闭环,但多语言产品还有另一半:设计稿本身要能承载不同语言的排版。
设计稿区域化:组件覆盖与灵活布局的多语言适配
i18n 体系只覆盖产品界面文案,用户设计稿里的多语言适配要靠设计手段。官方文档docs/user-guide/designing/text-typo.njk覆盖了 RTL(从右到左)文本的排版规则,阿拉伯语、希伯来语这类市场在设计阶段就要按 RTL 习惯安排对齐与留白。
组件层的做法是把语言作为变体维度:一个按钮组件拆出"中文-主要""英文-次要"等变体,实际使用时通过组件覆盖(component override)替换为对应语言的文本节点,而不是复制整份组件。
上图展示了在组件实例上覆盖属性的操作,语言变体切换走的正是这条机制。
文本长度差异用灵活布局吸收:容器不写死宽度,依赖 flex 的换行与伸缩规则(docs 中的 flexlayout 系列演示),德语比英语长 30% 左右、东亚文字密度更高时,布局自动重排而不会撑破容器。关键文案建议再收敛到设计标记(Design Tokens)统一管理,docs/user-guide/design-tokens/有完整说明。
设计和翻译两边都就绪后,交付前过一遍下面的清单。
交付前检查清单
- 在
i18n.cljs的supported-locales中确认目标语言已注册,否则界面切换器不会出现该语言; - 逐个切换目标语言,扫描界面有无带点号的键名文本,有则说明该键缺失或回退到
en; - 打开目标语言的
.po文件头,核对Plural-Forms与该语言的复数规则一致,并抽查一条msgid_plural的两支译文; - 在
frontend/下执行pnpm translations rehash,确认输出中没有新增fuzzy条目、没有遗留该清理的unused键; - 需要多语言键集合对齐时补跑
pnpm translations sync,确认没有其他语言文件残留孤儿键; - Weblate 上的目标条目状态为已批准(approved),而非停留在 "waiting for review";
- 设计稿侧:组件覆盖的语言变体文本已更新,承载多语言文本的容器使用灵活布局而非固定宽度。
七项全绿,这套从tr查表到 PO 文件、再到社区审核的 Penpot 多语言链路就算完整跑通了。
【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考