TREK 多语言支持完全指南:20 种语言、RTL 布局与语言检测链路解析
【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK
导读
TREK 是一个自托管的旅行规划器,内置了覆盖 20 种语言的完整翻译体系,支持从登录前到登录后的全流程语言切换。本文以 wiki/Languages.md 为骨架,结合@trek/shared国际化包、客户端TranslationProvider与服务器DEFAULT_LANGUAGE配置的源码实现,系统讲解 TREK 支持的语言清单、阿拉伯语 RTL 布局机制、四级语言检测链路,以及管理员如何通过环境变量控制未登录用户的默认语言。读完本文,你将掌握 TREK 多语言体系的全貌,并能独立完成用户端切换与服务器端默认语言配置。
支持的语言一览
TREK 开箱即用提供 20 种语言的界面翻译,用户无需登出即可随时切换语言。文档表格中的语言代码与仓库源码中的权威注册表一一对应,权威来源位于 shared/src/i18n/languages.ts,客户端通过 client/src/i18n/supportedLanguages.ts 将其重新导出(该文件注释明确指出"Canonical language registry now lives in @trek/shared")。
| Code | Language | Intl Locale(源码注册值) |
|---|---|---|
de | Deutsch | de-DE |
en | English | en-US |
es | Español | es-ES |
fr | Français | fr-FR |
hu | Magyar | hu-HU |
nl | Nederlands | nl-NL |
br | Português (Brasil) | pt-BR |
cs | Česky | cs-CZ |
pl | Polski | pl-PL |
ru | Русский | ru-RU |
zh | 简体中文 | zh-CN |
zh-TW | 繁體中文 | zh-TW |
it | Italiano | it-IT |
tr | Türkçe | tr-TR |
ar | العربية | ar-SA |
id | Bahasa Indonesia | id-ID |
ja | 日本語 | ja-JP |
ko | 한국어 | ko-KR |
uk | Українська | uk-UA |
gr | Ελληνικά | el-GR |
补充说明(仓库实际状态):虽然 wiki/Languages.md 表格列出 20 种语言,但当前仓库的翻译目录 shared/src/i18n 中实际还包含
sv(Svenska)、vi(Tiếng Việt)、ca(Català)三个目录,shared/src/i18n/languages.ts 也已注册对应条目,因此运行时实际可用的语言代码为 23 个。用户在语言选择器中看到的具体选项以SUPPORTED_LANGUAGES数组为准。
两个值得注意的代码约定
br≠ 布鲁塞尔:br实际代表巴西葡萄牙语(Português do Brasil),其 Intl locale 映射为pt-BR。客户端在 client/src/i18n/TranslationContext.tsx 中对浏览器上报的pt-BR做了显式映射,而pt-PT和裸pt则不会命中,会继续沿检测链路向下匹配。gr代表希腊语:代码使用gr(而非 ISO 标准的el),对应 locale 为el-GR。
RTL 布局支持:阿拉伯语专属的从右到左渲染
阿拉伯语(ar)是 TREK 目前唯一使用从右到左(RTL)布局的语言,其余所有语言均为从左到右(LTR)。
这一行为由共享包中的isRtlLanguage工具函数实现,RTL 语言集合定义在 shared/src/i18n/languages.ts:
// Languages displayed right-to-left. const RTL_LANGUAGES = new Set<string>(['ar']); export function isRtlLanguage(language: string): boolean { return RTL_LANGUAGES.has(language); }当用户选择语言后,客户端 TranslationProvider 会同步更新 HTML 文档的语言与方向属性:
useEffect(() => { document.documentElement.lang = language document.documentElement.dir = isRtlLanguage(language) ? 'rtl' : 'ltr' }, [language])即:<html lang="ar" dir="rtl">会被自动写入 DOM,配合浏览器的 RTL 排版能力(如 flex/grid 方向的自动镜像)即可完成整个界面的镜像布局,无需为每个组件单独编写 RTL 样式。CSS 层面基于dir属性与逻辑属性(logical properties)即可自适应,这也是所有依赖dir的现代 Web 布局的标准做法。
语言检测链路:四级解析顺序
TREK 解析显示语言时严格按照以下优先级,从高到低:
- 用户偏好——保存在账号中的语言(Settings → General 中设置)
- 浏览器语言——浏览器上报的
navigator.languages(以及navigator.language) - 服务器默认值——管理员设置的
DEFAULT_LANGUAGE环境变量 - 兜底——英语(
en)
第 1 级:用户偏好(localStorage + 账号设置)
用户语言偏好保存在localStorage的app_language键中。见 client/src/store/settingsStore.ts:
// Returns true when the user has explicitly chosen a language (persisted in localStorage). export const hasStoredLanguage = (): boolean => typeof localStorage !== 'undefined' && !!localStorage.getItem('app_language')settingsStore的初始状态直接读取该键(settingsStore.ts),并提供两个写入入口:
setLanguage——持久化写入localStorage并更新 store(对应"Settings → General"中的显式选择);setLanguageTransient——仅对当前会话生效、不写入localStorage(用于登录页的临时检测结果,见 settingsStore.ts)。
TranslationProvider正是从useSettingsStore中读取settings.language作为当前语言(TranslationContext.tsx),因此用户偏好是整条链路的最高优先级。
第 2 级:浏览器语言检测
若用户没有已保存的偏好,登录页逻辑(client/src/pages/login/useLogin.ts)会调用detectBrowserLanguage():
export function detectBrowserLanguage(): string | null { if (typeof navigator === 'undefined') return null const browserLangs = navigator.languages?.length ? navigator.languages : navigator.language ? [navigator.language] : [] const supported = SUPPORTED_LANGUAGES.map(l => l.value) for (const lang of browserLangs) { const exactMatch = supported.find(s => s.toLowerCase() === lang.toLowerCase()) if (exactMatch) return exactMatch // pt-BR has no exact match (our code is 'br'), so map it explicitly. if (lang.toLowerCase() === 'pt-br') return 'br' const prefix = lang.split('-')[0]?.toLowerCase() const prefixMatch = supported.find(s => s.toLowerCase() === prefix) if (prefixMatch) return prefixMatch } return null }检测逻辑(TranslationContext.tsx)依次尝试:
- 遍历
navigator.languages数组(降序偏好列表),对每个语言做精确匹配(如浏览器上报zh-TW直接命中); - 特殊映射
pt-BR→br; - 对每个语言做主语言前缀匹配(如浏览器上报
en-US,取en命中)。
detectBrowserLanguage是纯函数且无副作用,返回null表示浏览器语言不在支持列表内,此时进入第 3 级。
第 3 级:服务器默认值(DEFAULT_LANGUAGE)
当浏览器语言无法匹配时,登录页会向服务器请求公开配置:
configApi.getPublicConfig() .then(({ defaultLanguage }) => { if (defaultLanguage) setLanguageTransient(defaultLanguage) }) .catch((err) => console.warn('Failed to fetch default language config:', err))该公开配置由 server/src/nest/config/config.controller.ts 提供,直接返回服务器端解析后的DEFAULT_LANGUAGE常量。
第 4 级:英语兜底
settingsStore的默认值是'en'(settingsStore.ts),TranslationProvider中也以英文包en作为同步初始值和翻译缺失时的回退源(TranslationContext.tsx)。整个链路保证任何环境下界面都不会出现空字符串。
端到端流程小结
用户已保存偏好? ──是──▶ 使用 localStorage 中的 app_language │否 ▼ detectBrowserLanguage() 命中? ──是──▶ setLanguageTransient(detected) │否 ▼ GET /api/config(DEFAULT_LANGUAGE)──▶ setLanguageTransient(defaultLanguage) │失败/未配置 ▼ 硬编码兜底 'en'语言选择器出现在哪里
- 登录 / 注册页——登录之前即可切换语言(此时语言偏好以"瞬态"方式生效,不写入 localStorage,见
setLanguageTransient的注释与实现); - Settings → General——登录后通过账号设置持久化语言偏好,相关文档见 Display-Settings;
- 公开分享页——旅行分享链接的公开视图;
- 公开旅程页——面向公众的旅程展示视图。
管理员提示:
DEFAULT_LANGUAGE环境变量设置的是登录页及未认证用户的兜底语言。相关文档见 Environment-Variables。
管理员配置:DEFAULT_LANGUAGE 环境变量
服务端解析逻辑
服务器在启动时读取DEFAULT_LANGUAGE并做合法性校验(server/src/config.ts):
// DEFAULT_LANGUAGE sets the language shown on the login page before the user // selects one. Only applies when the user has no saved language preference. const rawDefaultLang = process.env.DEFAULT_LANGUAGE?.toLowerCase() || 'en'; if (!SUPPORTED_LANG_CODES.includes(rawDefaultLang)) { console.warn( `DEFAULT_LANGUAGE="${rawDefaultLang}" is not supported. Falling back to "en". Supported: ${SUPPORTED_LANG_CODES.join(', ')}`, ); } export const DEFAULT_LANGUAGE = SUPPORTED_LANG_CODES.includes(rawDefaultLang) ? rawDefaultLang : 'en';关键行为:
- 未设置时默认
en; - 值会先转小写再匹配(
ZH与zh等价); - 非法值不会导致启动失败,只会打印一条警告日志并回退到
en——这保证了误配置不会让实例崩溃。
各部署方式的配置位置
| 部署方式 | 配置位置 |
|---|---|
| Docker Compose | docker-compose.yml 中取消注释DEFAULT_LANGUAGE=en |
| Helm | charts/trek/values.yaml 中取消注释DEFAULT_LANGUAGE: "en" |
| Unraid 模板 | unraid-template.xml 中的DEFAULT_LANGUAGE高级变量(Advanced View),注释中给出了完整支持列表 |
.env.example(server/.env.example)同样给出了示例。需要注意的是,docker-compose.yml与unraid-template.xml中的注释列出的是 20 个语言代码(未含sv/vi/ca),而仓库源码的SUPPORTED_LANGUAGES实际注册了 23 个;DEFAULT_LANGUAGE的合法值以源码注册表为准,设置为任何未注册代码都会回退到en。
可用值速查
DEFAULT_LANGUAGE支持的值与 共享语言注册表 一致:
de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, tr, ar, id, ja, ko, uk, gr(另有源码中注册的sv, vi, ca)。
翻译体系如何工作:按需加载与键回退
深入理解 TREK 的多语言支持,还需要了解其翻译文件的组织与加载方式。
翻译文件的领域化组织
每种语言的翻译按功能域拆分成多个文件。以英文为例(shared/src/i18n/en/index.ts),聚合了admin、airport、atlas、budget、collab、journey、map、planner、reservations、transport、vacay等 40 余个域模块,最终合并为一个扁平键值对 locale 对象。
每个语言独立的代码分包
客户端为每个 locale 声明了独立的动态导入(client/src/i18n/TranslationContext.tsx):
// One explicit dynamic import per locale — Vite code-splits a separate chunk per locale. // Only the active locale is fetched; en is always available synchronously as the fallback. const localeLoaders: Record<SupportedLanguageCode, () => Promise<{ default: TranslationStrings }>> = { en: () => Promise.resolve({ default: en }), de: () => import('@trek/shared/i18n/de'), es: () => import('@trek/shared/i18n/es'), // ... }TranslationProvider根据当前语言调用对应 loader,仅在语言切换时才请求对应语言包;英文包则始终同步可用(TranslationContext.tsx),兼顾了冷启动速度与按需加载的体积控制。
三层键回退
翻译查找采用当前语言键 → 英文键 → 原样返回键名的三层回退(TranslationContext.tsx):
let val: string = (strings[key] ?? en[key] ?? key) as string这意味着:某条翻译在目标语言中尚未完成时,会优雅地回退到英文;即使英文也缺失,界面也不会崩溃,而是显示键名本身,便于开发排查。
带参插值与 HTML 安全
t()支持{placeholder}形式的参数插值(基于正则替换);tHtml()则是面向含标记模板的变体,采用双层防御:插值参数先 HTML 转义、整体字符串再经sanitizeInlineHtml白名单过滤(TranslationContext.tsx),确保翻译模板即使被恶意构造也无法注入脚本,这对多语言环境尤为重要。
翻译一致性校验
仓库用测试保障翻译体系的结构健康(shared/src/i18n/i18n-parity.spec.ts):每个非英文语言目录必须与en/拥有完全相同的领域文件集(file-level drift 视为结构性 bug 直接 fail),而具体翻译键的增减(key-level drift)则允许渐进推进,由 CLI 脚本 shared/scripts/i18n-parity.mjs 输出差异报告供译者参考。另有 i18n-placeholders.spec.ts 用于校验占位符一致性。这解释了为什么新增语言必须覆盖全部领域文件,也解释了"某些语言个别文案显示英文"是设计内的渐进式翻译策略。
常见问题与排查思路
Q1:为什么我的浏览器是英文,登录页却显示中文?检查三级链路:是否曾在该浏览器上保存过app_language(localStorage)——用户偏好优先级最高;其次检查管理员设置的DEFAULT_LANGUAGE。
Q2:设置了不支持的DEFAULT_LANGUAGE会怎样?不会启动失败。服务器会打印DEFAULT_LANGUAGE="xx" is not supported. Falling back to "en".警告并回退到en(server/src/config.ts)。
Q3:切换语言后页面方向(dir)没有变化?正常情况下TranslationProvider会同步写入<html dir="rtl| ltr">。只有ar会被判定为 RTL,其他语言(包括希伯来语等未支持语言)均按 LTR 处理(languages.ts)。
Q4:为什么某些界面文案还是英文?TREK 的翻译键采用"缺失即回退英文"策略,某个键在该语言中尚未翻译完成时属正常现象;翻译进度属于渐进式推进(参见 i18n-parity.spec.ts 的说明)。
延伸阅读
- Display-Settings —— 显示相关设置(含语言选项的 UI 位置)
- Environment-Variables ——
DEFAULT_LANGUAGE及其余环境变量完整说明 - User-Settings —— 用户账号级设置
- 语言注册表与工具函数:shared/src/i18n/languages.ts
- 客户端翻译运行时:client/src/i18n/TranslationContext.tsx
- 服务器默认语言解析:server/src/config.ts
【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考