Gogs 本地化翻译同步:`gogs import locale` 命令的完整流程与源码解析
Gogs 的界面多语言翻译通过 Crowdin 平台协作完成,官方开发者文档 docs/dev/import_locale.md 定义了将 Crowdin 上翻译成果批量同步回仓库 conf/locale/ 目录的标准流程。本篇以该文档为主线,完整还原从上传源文件、构建下载、执行 import locale 子命令到提交 Pull Request 的全部操作步骤,并结合 cmd/gogs/import.go 的实现源码,深入讲解该命令如何跳过英语源文件、修复 Crowdin 引号转义问题以及纠正文件时间戳,帮助维护者理解并正确使用这套本地化导入机制。
一、翻译同步的背景:locale 文件与多语言配置
Gogs 自 v0.5.0 起支持多语言界面,每种语言对应 conf/locale/ 目录下的一份 INI 文件,如 locale_en-US.ini、locale_zh-CN.ini、locale_ja-JP.ini 等,目录中还包含一个 TRANSLATORS 文件用于维护译者信息。
可用的界面语言在 conf/app.ini 的 [i18n] 段中声明:
[i18n]
LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,gl-ES,uk-UA,en-GB,hu-HU,sk-SK,id-ID,fa-IR,vi-VN,pt-PT,mn-MN,ro-RO
NAMES = English,简体中文,繁體中文(香港),繁體中文(臺灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,galego,українська,English (United Kingdom),Magyar,Slovenčina,Indonesian,Persian,Vietnamese,Português,Монгол,Română
该配置在 internal/conf/conf.go 中被映射到 I18n 变量,其结构体定义在 internal/conf/static.go:
type i18nConf struct {
Langs []string `delim:","`
Names []string `delim:","`
dateLangs map[string]string `ini:"-"`
}
Langs 与 Names 以逗号为分隔符解析为两个等长、顺序一一对应的列表。理解这一点非常关键——后文的导入命令正是依赖 conf.I18n.Langs 来决定哪些语言文件需要被导入。
二、官方文档定义的完整同步流程
以下是 docs/dev/import_locale.md 给出的标准操作步骤,每一步都不可跳过:
第 1 步:上传最新英文源文件到 Crowdin
将仓库中最新的 locale_en-US.ini 上传到 Crowdin 项目的源文件区。英文文件是所有翻译的基准,模板更新后各语言翻译才能与之对齐。
第 2 步:在 Crowdin 上构建并下载
在 Crowdin 项目中执行构建(Build),下载包含全部语言翻译的 ZIP 压缩包并解压。
第 3 步:切换到仓库根目录
cd <gogs 仓库根目录>
第 4 步:执行 import 子命令
$ ./.bin/gogs import locale --source <path to the unzipped directory> --target ./conf/locale
Locale files has been successfully imported!
其中 ./.bin/gogs 是通过构建工具生成的本地可执行文件,--source 指向解压后的目录,--target 指向仓库内的 conf/locale 目录。执行成功时命令会打印 Locale files has been successfully imported!(对应 cmd/gogs/import.go 中的 fmt.Println)。
第 5 步:启动 Web 服务验证
按照文档要求,运行 moon run gogs:dev 启动开发用 Web 服务器,然后在浏览器中访问站点,确认界面没有因为 locale 文件变化而报错——这一步是防止翻译文件存在语法错误导致本地化初始化失败的兜底检查。
第 6~9 步:提交并发起 Pull Request
git checkout -b update-locales
# 暂存变更
git add -A
git commit -m "locale: sync from Crowdin"
git push origin update-locales
随后在上游仓库平台上基于 update-locales 分支创建 Pull Request,提交信息约定俗成地采用 locale: sync from Crowdin 格式,便于在历史中识别翻译同步类提交。
三、命令参数与前置校验
import locale 子命令的注册定义在 cmd/gogs/import.go。整个 import 命令组的定位是「将便携数据导入为本地 Gogs 数据」,目前仅挂载了 locale 一个子命令:
| 参数 | 别名 | 是否必填 | 说明 |
|---|---|---|---|
--source |
无 | 是 | 存放新 locale 文件的源目录(即 Crowdin ZIP 解压目录) |
--target |
无 | 是 | 存放旧 locale 文件的目标目录(通常为 ./conf/locale) |
--config |
-c |
否 | 自定义配置文件路径,用于覆盖默认的 conf/app.ini |
参数校验逻辑位于 cmd/gogs/import.go 的 runImportLocale 函数,包含三层检查,任何一层失败都会立即返回带上下文的错误:
--source未指定 →source directory is not specified;--target未指定 →target directory is not specified;- 两个路径必须真实存在且是目录(通过
osx.IsDir判断),否则返回source directory %q does not exist or is not a directory或对应的 target 错误。
校验通过后调用 conf.Init(configFromLineage(cmd)) 初始化全局配置。这里有一个值得注意的细节:configFromLineage(定义于 cmd/gogs/cmd.go)会沿着 CLI 命令的 Lineage() 逐级向上查找 --config 标志的值,因为子命令可能看不到父命令上设置的参数。因此 --config 既可以挂在 import locale 子命令上,也可以挂在父命令层,行为一致。
四、导入实现的三个关键细节
命令拿到源/目标目录后,真正的文件处理逻辑集中在 cmd/gogs/import.go 的循环中,源码揭示了三个容易被忽略但至关重要设计决策。
4.1 跳过英语:只同步 Langs[1:]
// Cut out en-US.
for _, lang := range conf.I18n.Langs[1:] {
name := fmt.Sprintf("locale_%s.ini", lang)
source := filepath.Join(cmd.String("source"), name)
target := filepath.Join(cmd.String("target"), name)
if !osx.IsFile(source) {
continue
}
...
}
循环遍历的是 conf.I18n.Langs[1:]——即从 [i18n] LANGS 列表中剔除第一项 en-US 之后的所有语言。原因很直接:英文源文件以仓库中的 locale_en-US.ini 为唯一事实来源,Crowdin 上的英文副本可能滞后或不一致,若覆盖回来会破坏基准。同时,每个语言的文件名严格按 locale_<lang>.ini 约定拼接,不在 LANGS 列表中的语言即使出现在解压目录里也不会被导入,这保证了导入范围永远受 app.ini 的 [i18n] 配置约束。
对于源目录中缺失的某语言文件,代码直接 continue 跳过而不报错——部分语言翻译进度落后时仍可以同步其他已完成的语言。
4.2 修复 Crowdin 的引号转义
badChars := []byte(`="`)
escapedQuotes := []byte(`\"`)
regularQuotes := []byte(`"`)
源码注释明确写道:Crowdin 会对「字符串内部含有引号」的值整体用双引号包裹,而这会破坏 Gogs 使用的 INI 解析器。因此命令逐行扫描源文件,当一行在 = 或 " 之后出现、且以 " 结尾时,视为 Crowdin 的包裹格式:
idx := bytes.Index(line, badChars)
if idx > -1 && line[len(line)-1] == '"' {
// We still want the "=" sign
line = append(line[:idx+1], line[idx+2:len(line)-1]...)
line = bytes.ReplaceAll(line, escapedQuotes, regularQuotes)
}
处理逻辑是:保留第一个 =/" 及其前面的部分(idx+1 保证等号不被误删),丢弃从第二个特殊字符到行尾引号之外的包裹层,再把内部的 \" 反转义回普通 ",最后把改写后的行写入目标文件。这是典型的「适配上游工具输出格式」的防御性代码,也是直接手工复制 Crowdin 导出文件而不经过此命令会导致界面显示异常(引号裸露、字符串截断)的根本原因。
4.3 回拨文件修改时间
// Modification time of files from Crowdin often ahead of current,
// so we need to set back to current.
_ = os.Chtimes(target, now, now)
Crowdin 导出的文件时间戳常常「超前于」当前时间(时区或服务器时间偏差所致)。如果不做处理,git status 的依赖工具、增量构建缓存等可能因异常 mtime 产生误判,因此命令在写完目标文件后立即把 mtime 重置为导入时刻 now。
五、导入后的配置联动与本地覆盖机制
导入完成后,新语言文件立即被 internal/conf/conf.go 中的配置加载流程消费:File.Section("i18n").MapTo(I18n) 将 [i18n] 段映射到 i18nConf,其中的 dateLangs 则来自 i18n.datelang 段,用于把标准 locale 名称映射为日期时间插件所需的语言代码(DateLang 方法在查不到映射时回退为 "en",见 internal/conf/static.go)。
除了走 Crowdin 的正式同步通道,docs/advancing/localization.mdx 还描述了两种用户侧的本地化覆盖方式,与导入流程互为补充:
- 本地测试翻译:将修改后的 locale 文件放到
custom/conf/locale/<file>后重启 Gogs 即可加载,无需改动 Git 历史; - 自定义覆盖:创建
custom/conf/locale/locale_<lang>.ini,文件中只需包含要覆盖的键,未指定的键自动回退到官方翻译。
这意味着维护者在验证某条翻译修复时,可以先用 custom 目录快速试错,确认无误后再走本文的 Crowdin 同步流程合并回主线。
六、小结:一条命令背后的工程约定
对照 docs/dev/import_locale.md 的流程与 cmd/gogs/import.go 的实现,可以归纳出 Gogs 本地化同步的几条核心约定:
- 英文是唯一基准:导入命令硬编码跳过
LANGS首位的en-US,英文更新只经由仓库提交生效; - 导入范围由
[i18n] LANGS决定:新增语言时须先在conf/app.ini中登记,否则新文件不会被同步; - 必须走
import locale而非手工拷贝:Crowdin 导出的引号格式需要命令内建的反转义处理; - 同步后必须启动服务验证:
moon run gogs:dev起服务、浏览器访问是提交前的强制检查项; - 提交遵循
locale: sync from Crowdin信息约定,并统一使用update-locales分支名,便于 Code Review 识别。
遵循上述流程,维护者即可安全、可追溯地把 Crowdin 上的多语言翻译成果合入仓库,且导入过程对缺失文件、格式异常、时间戳偏差都有明确的源码级兜底。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00