TREK 多语言支持完全指南:20 种语言、RTL 布局与语言检测链路解析
2026/9/15 19:55:27 网站建设 项目流程

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")。

CodeLanguageIntl Locale(源码注册值)
deDeutschde-DE
enEnglishen-US
esEspañoles-ES
frFrançaisfr-FR
huMagyarhu-HU
nlNederlandsnl-NL
brPortuguês (Brasil)pt-BR
csČeskycs-CZ
plPolskipl-PL
ruРусскийru-RU
zh简体中文zh-CN
zh-TW繁體中文zh-TW
itItalianoit-IT
trTürkçetr-TR
arالعربيةar-SA
idBahasa Indonesiaid-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 解析显示语言时严格按照以下优先级,从高到低:

  1. 用户偏好——保存在账号中的语言(Settings → General 中设置)
  2. 浏览器语言——浏览器上报的navigator.languages(以及navigator.language
  3. 服务器默认值——管理员设置的DEFAULT_LANGUAGE环境变量
  4. 兜底——英语(en

第 1 级:用户偏好(localStorage + 账号设置)

用户语言偏好保存在localStorageapp_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)依次尝试:

  1. 遍历navigator.languages数组(降序偏好列表),对每个语言做精确匹配(如浏览器上报zh-TW直接命中);
  2. 特殊映射pt-BRbr
  3. 对每个语言做主语言前缀匹配(如浏览器上报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
  • 值会先转小写再匹配(ZHzh等价);
  • 非法值不会导致启动失败,只会打印一条警告日志并回退到en——这保证了误配置不会让实例崩溃。

各部署方式的配置位置

部署方式配置位置
Docker Composedocker-compose.yml 中取消注释DEFAULT_LANGUAGE=en
Helmcharts/trek/values.yaml 中取消注释DEFAULT_LANGUAGE: "en"
Unraid 模板unraid-template.xml 中的DEFAULT_LANGUAGE高级变量(Advanced View),注释中给出了完整支持列表

.env.example(server/.env.example)同样给出了示例。需要注意的是,docker-compose.ymlunraid-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),聚合了adminairportatlasbudgetcollabjourneymapplannerreservationstransportvacay等 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),仅供参考

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

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

立即咨询