☰
Pomotroid 土耳其语本地化实践:基于 Paraglide 的 8 语言架构与新增 locale 全流程
2026/10/12 13:58:51 网站建设 项目流程

【免费下载链接】pomotroid

:tomato: Simple and visually-pleasing Pomodoro timer

项目地址:https://gitcode.com/gh_mirrors/po/pomotroid
点击查看免费下载

本文以 Pomotroid 新增土耳其语(tr)支持这一真实变更为主线,讲解其基于 Paraglide JS v2 的国际化(i18n)架构:从消息文件结构、locale 注册、语言解析与即时切换,到桌面通知与 Tooltip 的翻译覆盖,完整演示如何在零 Rust 改动的前提下为一个 Tauri + SvelteKit 桌面应用接入新语言。

为什么新增一个 locale 如此"轻"

在 Pomotroid 中,添加土耳其语(Turkish)不涉及任何架构改动。原因在于其国际化完全建立在 Paraglide JS 之上:

  • 所有面向用户的字符串都集中存放在src/messages/<locale>.json消息文件中;
  • 构建期由 Paraglide 扫描 project.inlang/settings.json 中声明的locales数组,为每个语言生成类型安全的m.<key>()消息函数;
  • 前端通过m.*()调用读取字符串,界面、通知、Tooltip 全部走同一条链路。

因此,新增语言本质上是"加一个文件 + 注册一个条目 + 在下拉菜单里加一项",正如 proposal.md 所述:无 Rust 改动、无数据库迁移、无新增依赖。

一、8 语言架构:从 7 到 8 的版本演进

本次变更将启动时支持的语言从 7 种扩展到 8 种。完整的 locale 集合定义在 project.inlang/settings.json:

{ "$schema": "https://inlang.com/schema/project-settings", "baseLocale": "en", "locales": ["en", "es", "fr", "de", "ja", "zh", "pt", "tr"], "modules": [ "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" ], "plugin.inlang.messageFormat": { "pathPattern": "./src/messages/{locale}.json" } }

要点解读:

  • baseLocale: "en"是基准语言,也是缺失键时的回退语言;
  • locales数组按en, es, fr, de, ja, zh, pt, tr顺序声明 8 种语言:英语、西班牙语、法语、德语、日语、简体中文、葡萄牙语、土耳其语;
  • pathPattern定义了消息文件的存放模式./src/messages/{locale}.json,Paraglide 据此在构建期发现并编译每个语言的文件;
  • 模块@inlang/plugin-message-format负责 JSON 消息格式的解析,@inlang/plugin-m-function-matcher负责消息函数的匹配。

启动时npm run paraglide:compile(即paraglide-js compile --project ./project.inlang --outdir ./src/paraglide)会把上述配置编译为src/paraglide下的运行时与消息模块,前端通过import * as m from '$paraglide/messages.js'使用。类型安全由 Paraglide 自动保证:一旦某消息键在en.json中不存在,TypeScript 编译期即报错,防止界面引用漂移。

二、消息文件:109 个键的完整翻译

新增的 src/messages/tr.json 包含与 src/messages/en.json 完全对齐的109 个翻译键,覆盖五大功能面:

功能面典型键英文示例土耳其语示例
计时器与导航nav_timer、round_label_work、timer_slider_rounds"Focus""Odaklanma"
外观与设置appearance_mode_auto、system_group_language"Auto""Otomatik"
通知与声音notification_work_title、notif_alert_work"Break over — focus time!""Mola bitti — odaklanma zamanı!"
快捷键shortcuts_toggle_timer、shortcuts_local_mute"Toggle Timer""Zamanlayıcıyı Aç/Kapat"
统计与 Tooltipstats_today、tooltip_reset"Open Statistics""İstatistikleri aç"

以几组真实键为例,可以看出翻译不仅是直译,还适配了土耳其语习惯:

"timer_session_round": "tur {n}", "system_toggle_websocket_desc": "Harici entegrasyonlar için yerel bir WebSocket uç noktası sun (port {port}).", "shortcuts_accessibility_notice": "Genel kısayollar macOS'ta Erişilebilirlik iznini gerektirir. Sistem Ayarları → Gizlilik ve Güvenlik → Erişilebilirlik bölümünde Pomotroid'i etkinleştirin."

注意占位符的保留:{n}、{port}、{version}等插值参数必须原样保留在翻译文本中,因为运行时 Paraglide 会按键注入动态值。这在about_update_install("Install v{version} — app will restart")等键中尤其关键。

非英语语言文件必须包含en.json中每一个键。这是 localization spec 的硬性要求:

  • 非英语文件加载后,en.json中所有键都应有对应条目;
  • 若活动语言的某个键缺失,则显示英文回退字符串。

这一约束确保任何语言都不会出现空白界面。

三、语言解析与回退逻辑:auto、前缀匹配与 en 兜底

language设置默认值为auto(见 src-tauri/src/settings/defaults.rs),即默认跟随系统。解析逻辑集中在 src/lib/utils/locale.ts:

export function resolveLocale(language: string): SupportedLocale { const tag = language === 'auto' ? navigator.language : language; return matchLocale(tag); } function matchLocale(tag: string): SupportedLocale { // Exact match first. if ((locales as readonly string[]).includes(tag)) { return tag as SupportedLocale; } // Prefix match: 'de-AT' → 'de'. const prefix = tag.split('-')[0]; if ((locales as readonly string[]).includes(prefix)) { return prefix as SupportedLocale; } return baseLocale as SupportedLocale; }

解析规则可以归纳为三级匹配:

  1. 精确匹配:navigator.language或用户指定标签恰好等于某个受支持 locale(如fr),直接命中;
  2. 前缀匹配:带地区限定符的语言代码(如de-AT)取-前的语言部分(de)匹配;
  3. 回退:完全无匹配时回退到baseLocale(en)。

有趣的是,zh-CN这类标签会命中前缀zh,与 spec 中的旧场景略有出入;实际以 locale.ts 的当前实现为准。这套逻辑对土耳其语同样生效:tr精确匹配、tr-TR前缀匹配、其他语言回退英文。

SupportedLocale类型直接取自 Paraglide 编译产物的locales数组((typeof locales)[number]),因此类型层面与settings.json中声明的 locale 集合天然同步——这正是"加一个 locale 不需要改类型定义"的底层原因。

四、即时切换:语言下拉与跨窗口热更新

语言选择器位于Settings → System → Language。在 SystemSection.svelte 中定义的语言选项如下:

const LANGUAGES = [ { value: 'auto', label: 'Auto' }, { value: 'en', label: 'English' }, { value: 'es', label: 'Español' }, { value: 'fr', label: 'Français' }, { value: 'de', label: 'Deutsch' }, { value: 'ja', label: '日本語' }, { value: 'zh', label: '中文' }, { value: 'pt', label: 'Português' }, { value: 'tr', label: 'Türkçe' }, ];

注意两点设计细节:

  • 每个选项的label用该语言的原生名称展示(土耳其语显示为Türkçe),便于母语用户识别;
  • value与数据库存储值对应:auto或 locale 标签。

用户选择后触发 selectLanguage:

async function selectLanguage(value: string) { langOpen = false; const updated = await setSetting('language', value); settings.set(updated); setLocale(value); }

调用链为:setSetting('language', 'tr')→ Tauri 命令settings_set持久化到 SQLite → Rust 端发出settings:changed事件(见 commands.rs)→ 各窗口监听事件后重新调用setLocale()。这一机制同时作用于主计时器窗口、设置窗口与统计窗口(+page.svelte、settings/+page.svelte、stats/+page.svelte 中均有onSettingsChanged→setLocale(updated.language)的订阅逻辑),因此切换语言无需重启应用。

语言切换的响应式核心在 src/lib/locale.svelte.ts:

let currentLocale: SupportedLocale = $state(baseLocale as SupportedLocale); // Override Paraglide's getLocale so that every m.*() call reads from our // reactive $state instead of its internal plain-JS variable. overwriteGetLocale(() => currentLocale); export function setLocale(language: string): void { const resolved = resolveLocale(language) as SupportedLocale; currentLocale = resolved; info(`[locale] locale set to ${resolved}`).catch(() => {}); }

这是 Svelte 5 rune($state)与 Paraglide 的巧妙结合:通过overwriteGetLocale让所有m.*()调用读取响应式状态,任何模板表达式中读取m.xxx()都会建立依赖跟踪;currentLocale一旦变化,所有界面字符串自动重渲染。这也是"两个窗口同时更新"的实现基础——每个窗口各自持有setLocale,通过settings:changed事件驱动。

五、翻译覆盖延伸:桌面通知与 Tooltip

桌面通知

通知字符串的翻译完全在前端完成。在 Timer.svelte 的onRoundChange回调中:

if (snap.round_type === 'work') { const afterBreak = snap.previous_round_type === 'short-break' || snap.previous_round_type === 'long-break'; title = afterBreak ? m.notification_work_title() : m.notification_work_start_title(); body = afterBreak ? m.notification_work_body() : m.notification_work_start_body(); } // ... notificationShow(title, body).catch(() => {});

前端根据轮次类型选择已翻译的标题与正文,再通过notificationShow(title, body)命令传给 Rust。Rust 端 notifications/mod.rs 完全不参与字符串构造,保持 locale 无关:

  • Linux 下通过notify-send子进程发送(避免 D-Bus 会话总线连接问题);
  • macOS / Windows 下使用tauri-plugin-notification。

因此当用户选择土耳其语时,通知会显示如 "Mola bitti — odaklanma zamanı!" 这样的土耳其语内容。

Tooltip 提示

所有 Tooltip 的 i18n 键也必须在每个语言文件中存在(可机器翻译)。tr.json中覆盖了完整的 Tooltip 键集,包括:

  • tooltip_settings→ "Ayarları aç"
  • tooltip_restart_round→ "Mevcut turu baştan başlat."
  • tooltip_reset→ "Zamanlayıcıyı ilk çalışma turuna sıfırla. Mevcut oturum ilerlemesi silinecek."
  • tooltip_verbose_logging→ 包含完整诊断日志指引
  • tooltip_websocket→ WebSocket 服务器功能说明
  • 以及tooltip_dial_countdown、tooltip_auto_start_work、tooltip_auto_start_break等

值得一提的是system_tray_gnome_hint:它是 Linux 专属的TooltipInfo图标复用的既有键(早于本次变更),无需新增键,tr.json中同样提供了土耳其语翻译("GNOME'da sistem tepsisi simgeleri AppIndicator eklentisini gerektirir…")。

六、本次变更的影响面清单

按 proposal.md 的 Impact 总结,变更只触及三个文件:

文件变更内容
src/messages/tr.json新增文件,109 个已翻译键
project.inlang/settings.jsonlocales数组加入"tr"
SystemSection.svelte语言下拉框加入 "Türkçe" 选项

无Rust 代码改动、无数据库迁移、无新增依赖。新增语言时的标准步骤可归纳为:

  1. 复制src/messages/en.json为src/messages/<locale>.json,翻译全部键(保留{n}、{port}、{version}等占位符);
  2. 在project.inlang/settings.json的locales数组追加新标签;
  3. 在SystemSection.svelte的LANGUAGES数组中增加{ value: '<locale>', label: '<原生语言名>' };
  4. 运行npm run paraglide:compile重新生成类型安全的消息模块,TypeScript 编译器会立即校验键的完整性。

由于SupportedLocale类型由编译产物推导、语言解析走通用前缀匹配、跨窗口热更新走settings:changed事件,这套流程对任何后续新语言(包括未来可能的阿拉伯语、韩语等)完全通用,无需为单一语言编写任何特殊代码。这正是 Paraglide 消息目录 + 类型安全访问 + 响应式运行时这一组合在桌面应用中的典型落地范式。

【免费下载链接】pomotroid

:tomato: Simple and visually-pleasing Pomodoro timer

项目地址:https://gitcode.com/gh_mirrors/po/pomotroid
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询