【免费下载链接】pomotroid
:tomato: Simple and visually-pleasing Pomodoro timer
本文以 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" |
| 统计与 Tooltip | stats_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; }解析规则可以归纳为三级匹配:
- 精确匹配:
navigator.language或用户指定标签恰好等于某个受支持 locale(如fr),直接命中; - 前缀匹配:带地区限定符的语言代码(如
de-AT)取-前的语言部分(de)匹配; - 回退:完全无匹配时回退到
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.json | locales数组加入"tr" |
| SystemSection.svelte | 语言下拉框加入 "Türkçe" 选项 |
无Rust 代码改动、无数据库迁移、无新增依赖。新增语言时的标准步骤可归纳为:
- 复制
src/messages/en.json为src/messages/<locale>.json,翻译全部键(保留{n}、{port}、{version}等占位符); - 在
project.inlang/settings.json的locales数组追加新标签; - 在
SystemSection.svelte的LANGUAGES数组中增加{ value: '<locale>', label: '<原生语言名>' }; - 运行
npm run paraglide:compile重新生成类型安全的消息模块,TypeScript 编译器会立即校验键的完整性。
由于SupportedLocale类型由编译产物推导、语言解析走通用前缀匹配、跨窗口热更新走settings:changed事件,这套流程对任何后续新语言(包括未来可能的阿拉伯语、韩语等)完全通用,无需为单一语言编写任何特殊代码。这正是 Paraglide 消息目录 + 类型安全访问 + 响应式运行时这一组合在桌面应用中的典型落地范式。
【免费下载链接】pomotroid
:tomato: Simple and visually-pleasing Pomodoro timer
相关推荐
Pomotroid 土耳其语本地化实战:基于 Paraglide JS 的 `tr` 语言包接入全流程
Pomotroid 土耳其语本地化实战:基于 Paraglide JS 的 tr 语言包接入全流程 本文以 Pomotroid 开源仓库中 土耳其语支持变更设计
Pomotroid 多语言化实战:基于 Paraglide JS 的 Tauri + SvelteKit 国际化架构全解析
Pomotroid 多语言化实战:基于 Paraglide JS 的 Tauri + SvelteKit 国际化架构全解析 Pomotroid 是一款基于 Ta
Pomotroid 多语言支持(i18n)全解析:基于 Paraglide JS 的 Tauri 桌面应用本地化实践
Pomotroid 多语言支持(i18n)全解析:基于 Paraglide JS 的 Tauri 桌面应用本地化实践 Pomotroid 是一个基于 Tauri
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考