GraphHopper 路线转向提示多语言翻译机制与本地化贡献指南
【免费下载链接】graphhopperOpen source routing engine for OpenStreetMap. Use it as Java library or standalone web server.项目地址: https://gitcode.com/GitHub_Trending/gr/graphhopper
导读
GraphHopper 是面向 OpenStreetMap 的开源路由引擎,其服务器端会为每次导航请求生成"向左转""进入环岛并驶出第 N 个出口"等转向提示(turn instructions)。为了让全球用户以母语接收这些指令,GraphHopper 内置了一套由 Google Sheets 驱动的多语言翻译工作流:翻译条目集中托管在电子表格中,通过脚本自动导出为各语言资源文件,由 TranslationMap 在运行时加载并提供带占位符参数、带英语回退的翻译查询。本文以 docs/core/translations.md 为骨架,结合源码与脚本,完整讲解这套机制的原理、参与流程与工程细节,帮助你为 GraphHopper 新增或修正一种语言。
一、翻译系统的整体架构
GraphHopper 的翻译体系分为两层:
- 服务器端(核心路由引擎):只翻译转向指令(turn instructions),即由路由算法产出的"转弯/环岛/上桥/上轮渡"等每一步操作描述。
- 客户端(GraphHopper Maps 前端):除转向指令外的其余界面文案(按钮、标签、设置项等)均由前端项目维护,走独立的翻译渠道(见下文"客户端翻译"小节)。
整个服务端翻译链路可概括为:
Google Sheets(翻译总表) │ curl 导出 TSV ▼ core/files/update-translations.sh 脚本 │ cut 按列拆分 ▼ core/src/main/resources/com/graphhopper/util/<locale>.txt 资源文件 │ TranslationMap.doImport() 类路径加载 ▼ TranslationMap 内存映射(locale → Translation) │ getWithFallBack() / get() ▼ Router 组装 ResponsePath 时按请求 locale 取出译文 │ tr(key, params) → String.format ▼ 导航转向指令文本(如 "at roundabout, take exit 2")关键源码入口:
- 翻译管理器:TranslationMap.java;
- 路由服务中的调用点:Router.java(
translationMap.getWithFallBack(request.getLocale())); - 请求端的语言参数:GHRequest.java 的
setLocale(Locale)/setLocale(String)。
二、locale 语言代码:ISO 639-1 双字母码
翻译表中每个语言列的名称(如 "Spanish: es")中,冒号后的字符串就是该语言的ISO 639-1 双字符代码,例如:
| 语言 | 代码 |
|---|---|
| German / 德语 | de |
| English / 英语 | en |
| Simplified Chinese / 简体中文 | zh |
| French / 法语 | fr |
| Spanish / 西班牙语 | es |
| Italian / 意大利语 | it |
| Russian / 俄语 | ru |
| Japanese / 日语 | ja |
如果你不确定自己语言的代码,可在维基百科查询该语言词条中给出的 ISO 639-1 代码。实际资源文件中,locale 往往带国家和地区后缀(如de_DE、zh_CN),这是为了支持方言变体;但在 URL 查询参数与前端展示中,通常会退化为双字母形式。
三、%1$s占位符:为什么绝不能被省略
翻译条目中常出现%1$s、%2$s这类字符,它们是JavaString.format位置参数占位符:%1$s表示"第 1 个字符串参数",%2$s表示"第 2 个字符串参数",由 GraphHopper 在运行时填入路名、出口编号等动态值。
保留占位符至关重要,原因有两点:
- 语序差异:不同语言中参数出现的位置可能完全不同。例如英语是
"Enter roundabout and use exit %1$s"(参数在句尾),而德语必须把"出口编号"参数放在句中并搭配动词:"In den Kreisverkehr einfahren und Ausfahrt %1$s nehmen"(“…并取第 N 个出口”)。 - 句式重构:即使含义相同,译文也可能整体重组,必须把参数嵌入新句式的正确位置。
因此翻译时不要忘记这些占位符,也不得擅自增删个数。如果不确定某个条目的参数含义,应先在 GraphHopper Maps 上观察对应指令的英文原文,或到社区论坛询问。源码层面,TranslationHashMap.tr() 最终通过String.format(Locale.ROOT, val, params)完成渲染——占位符个数不匹配会直接导致格式化异常。
四、翻译资源文件格式与内容构成
每种语言对应一个 UTF-8 编码的.txt文件,位于 core/src/main/resources/com/graphhopper/util/,当前仓库共提供约 50 种语言,包括en_US.txt、de_DE.txt、fr_FR.txt、zh_CN.txt、zh_TW.txt、ja.txt、ru.txt、es.txt等。
文件格式为简单的key=value行:
# do not edit manually, instead use spreadsheet from translations.md and script ./core/files/update-translations.sh continue=continue continue_onto=continue onto %1$s turn_left=turn left roundabout_exit=at roundabout, take exit %1$s board_ferry=Attention, take ferry (%1$s) web.total_ascend=%1$s total ascent- 以
//或#开头的行是注释,会被忽略; - 每行第一个
=左侧为 key,右侧为译文;value 为空的行会被跳过; - key 会被统一转为小写后存储(见
put()中toLowerCase(key)),重复 key 会抛出IllegalStateException,防止覆盖; - 条目大致分两类:转向指令类(
continue、turn_left、roundabout_exit、leave_ferry、board_ferry、pt_transfer_to等)与Web 展示类(以web.为前缀,如web.total_ascend、web.way_contains_toll、web.start_label等,供服务端组装路线汇总信息使用)。
以de_DE.txt为例,同一 key 的德语译文为:
continue=dem Straßenverlauf folgen continue_onto=dem Straßenverlauf von %1$s folgen roundabout_exit=im Kreisverkehr Ausfahrt %1$s nehmen roundabout_exit_onto=im Kreisverkehr Ausfahrt %1$s auf %2$s nehmen board_ferry=Achtung, auf Fähre umsteigen (%1$s)五、源码解析:TranslationMap 如何加载与回退
TranslationMap.java 是服务端翻译的内存管理器,核心逻辑如下:
1. 语言清单LOCALES
类顶部的静态常量LOCALES列出全部受支持语言的 locale,按字典序排列,且英语(en_US)必须在列表最前,因为它充当所有其他语言的参考基准。该清单必须与脚本core/files/update-translations.sh中的语言列表保持一致(新增语言时两处都要改)。
2. 加载入口doImport()
提供两个重载:
doImport():从classpath加载core/src/main/resources/com/graphhopper/util/下的<locale>.txt;doImport(File folder):从外部目录加载同构文件。
加载完成后调用postImportHook()做一致性校验。GraphHopper 在 GraphHopper.java 启动时即通过new TranslationMap().doImport()初始化并暴露给路由组件。
3. 兼容性别名add()
add()在注册翻译对象的同时处理了新旧 JDK 的 locale 命名差异:
iw(旧 JDK 希伯来语)与he互相注册别名;in(旧 JDK 印度尼西亚语)与id互相注册别名;- 无国家后缀的语言代码会回填为同语言翻译,保证
get("de")也能命中de_DE的翻译。
4. 回退机制getWithFallBack()
public Translation getWithFallBack(Locale locale) { Translation tr = get(locale.toString()); if (tr == null) { tr = get(locale.getLanguage()); if (tr == null) tr = get("en"); } return tr; }查找顺序为:完整 locale(如zh_CN)→ 语言代码(如zh)→ 英语en。get()内部还会将连字符-归一化为下划线_。这意味着任何未覆盖的语言最终都会安全回退到英语,不会产生空译文。路由服务在 Router.java 中正是用getWithFallBack(request.getLocale())取得译文对象。
5. 导入校验postImportHook()
doImport完成后会对每种语言执行两项自动检查,任一失败都会抛出IllegalStateException并打印错误清单(这正是下文"运行mvn clean test验证"能拦住低级错误的原因):
- 缺失补全:某语言缺失的条目自动用英语值补齐;
- 占位符校验:比较译文与英文条目中
%占位符个数是否一致,并用占位符占位值实际执行一次String.format(Locale.ROOT, value, strs),捕获如%1$(缺少结尾s)之类的格式错误。
六、翻译参与全流程:从电子表格到合入
第一步:查看现有翻译并找到你的语言
翻译条目托管在共享 Google Sheets 翻译总表中。打开文档后,为你的语言新增一列(若已存在则直接编辑该列),随后定期回来更新或补充条目。你可以在 GraphHopper Maps 上实时预览自己的语言效果:在路线 URL 中显式追加locale参数,例如:
https://graphhopper.com/maps/?point=40.979898%2C-3.164062&point=39.909736%2C-2.8125&locale=de将locale=de换成zh、fr、es、ja等即可切换语言(de→ 德语,en→ 英语,zh→ 简体中文,以此类推)。动手改翻译前若拿不准,可以到官方翻译讨论版块(GraphHopper discuss 的 translations 分区)咨询。
第二步:本地准备 GraphHopper 开发环境
翻译合入仓库前,需要先让 GraphHopper 在你自己的电脑上跑起来:git clone仓库后按 快速开始指南 完成源码构建。若你创建的是全新语言,还需在两处登记:
- 字典序加入TranslationMap.LOCALES(
core/src/main/java/com/graphhopper/util/下的TranslationMap.java); - 加入脚本core/files/update-translations.sh 中
translations变量对应的语言列表位置(注意该列表首部的en_US SKIP SKIP结构:第一列是参考语言,后两列是跳过占位)。
第三步:导出电子表格为 TSV
进入core目录,用curl将 Google Sheets 以 TSV 格式导出到临时文件(gid=0对应翻译工作表):
cd graphhopper/core curl -L 'https://docs.google.com/spreadsheets/d/18z00Rbt6QvLIkayEV9P89vW9oU0QbTVsjRk9nz1CeFY/export?format=tsv&id=18z00Rbt6QvLIkayEV9P89vW9oU0QbTVsjRk9nz1CeFY&gid=0' > tmp.tsv第四步:运行更新脚本生成资源文件
./files/update-translations.sh tmp.tsv && rm tmp.tsv脚本逻辑(见 update-translations.sh):
- 遍历语言清单,跳过
SKIP占位列; - 对每个 locale 在
src/main/resources/com/graphhopper/util/<locale>.txt生成文件,首行写入"请勿手工编辑"的提示注释; - 用
tail -n+5跳过 TSV 表头等前 4 行,再用cut -f1,INDEX把"英文 key 列 + 该语言列"拆为key=value输出(gcut/cut自动探测,兼容 macOS 与 Linux)。
第五步:检查改动并提交
git diff git status确认只有本次翻译相关的文件发生变更。若未创建新语言,改动应只落在若干.txt资源文件上。
第六步:构建测试验证占位符
mvn clean test该步骤会触发前述postImportHook()的占位符一致性校验与格式化试运行:若你的译文漏了%1$s、多写了%,或写出了无法格式化的占位符,测试会直接失败并打印出错的语言与条目,从而保证"没有丢失参数占位符"(对应上文第三节的告诫)。
第七步:本地服务预览
构建通过后启动 GraphHopper 服务(见 快速开始指南),在本地路由端点追加&locale=de等参数验证效果;若页面未自动切换语言,就显式带上该参数:
http://localhost:8989/route?point=...&point=...&locale=de第八步:提交贡献
阅读 贡献指南 后按规范提交改动,GraphHopper 维护团队也会定期将电子表格中的新翻译合入仓库,因此即使你不走完整的合入流程,只在表格中更新条目也能被周期性地同步进来。
七、客户端翻译:职责边界
需要强调的是,服务器端只翻译转向指令。GraphHopper Maps 前端界面中的其他文案(按钮、弹窗、图层面板等)属于客户端翻译范畴,由graphhopper-maps前端项目独立维护,如需贡献请直接在其翻译帮助文档中操作,与本文所述的服务端资源文件无关。
八、参与注意事项小结
| 事项 | 说明 |
|---|---|
| 语言代码 | 使用 ISO 639-1 双字母码,新增语言按字典序登记到TranslationMap.LOCALES与update-translations.sh |
| 占位符 | %1$s等必须保留且个数与英文一致,位置可按目标语言语序调整 |
| 资源文件 | key=value格式、UTF-8 编码,位于core/src/main/resources/com/graphhopper/util/ |
| 自动校验 | postImportHook()负责缺失补全与占位符格式检查,mvn clean test会拦截错误 |
| 回退顺序 | 完整 locale → 语言代码 →en,任何语言都能安全兜底 |
| 编辑原则 | 资源文件由脚本生成,请勿手工编辑,应修改电子表格后重新导出 |
| 服务端边界 | 只翻转向指令;界面文案走客户端翻译渠道 |
相关资源
- 翻译规范原文:docs/core/translations.md
- 翻译管理器实现:core/src/main/java/com/graphhopper/util/TranslationMap.java
- 更新脚本:core/files/update-translations.sh
- 语言资源目录:core/src/main/resources/com/graphhopper/util/
- 路由中的调用点:core/src/main/java/com/graphhopper/routing/Router.java
- 请求 locale 参数:web-api/src/main/java/com/graphhopper/GHRequest.java
- 源码构建指南:docs/core/quickstart-from-source.md
- 贡献规范:CONTRIBUTING.md
【免费下载链接】graphhopperOpen source routing engine for OpenStreetMap. Use it as Java library or standalone web server.项目地址: https://gitcode.com/GitHub_Trending/gr/graphhopper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考