文章目录
Sapphire 是一个把 MacBook 刘海变成信息入口的开源应用。音乐播放、天气、日历、电池、文件架、窗口吸附,这些原本散落在菜单栏和桌面上的信息,都可以收进刘海周围那一小块区域里。
项目来源与作者
- 原项目:Sapphire
- 原作者:Shariq Charolia(GitHub:@cshariq)
- 原仓库:github.com/cshariq/Sapphire
- 上游版本:
475f309(Update 3.5)- 开源许可证:AGPL-3.0
- 本文作者:Henry
本文记录的是我在原作者开源代码基础上进行的简体中文本地化和个人可用性改造,不是 Sapphire 官方版本,也不代表原作者的观点或立场。Sapphire 的名称、原始界面与代码版权归原作者及贡献者所有。
我最初的目标很简单:把它汉化成简体中文。
但真正开始之后才发现,这不是一个“把英文替换成中文”的任务。
公开仓库无法直接编译,界面里大量字符串根本没有进入本地化系统,订阅和账户逻辑把一部分已经实现的功能锁住,Helper 在 ad-hoc 签名下无法启动,macOS 的权限记录还会因为每次重新签名而失效。
最后,这个项目变成了一次完整的工程改造:从 565 个编译错误,到 2742 条中文文案;从一个只能阅读的公开仓库,到一台真正可以日常使用的 Mac 应用。
这篇文章记录的,是这次改造里比“翻译”更重要的事情。
一、第一个意外:公开仓库编译不过
我原本以为,开源项目至少应该能编译。
Sapphire 的公开仓库并不是这样。它提交了引用私有子系统的代码,却把这些子系统本身排除在仓库之外。结果就是,项目看起来完整,实际打开 Xcode 后会出现大约 565 个编译错误。
这不是简单的“少一个文件”。缺失的部分包括文件提供商、Finder 扩展、相机扩展、连续互通、OCR 镜射取字和存储空间地图等。它们不是主界面,却和主工程紧密耦合。
我没有办法凭空恢复这些私有实现,只能做两件事:
- 补齐工程需要存在的最小模块外壳;
- 对缺失的私有能力提供明确的空实现,而不是伪装成已经可用。
这一步很重要。一个可编译的项目,不代表所有功能都完整;但一个连编译都过不了的项目,连验证其他改动的机会都没有。
最终,我用 PublicExtensionStubs 补出了构建所需的扩展目录,又为 Continuity、OCR 和存储地图补了公共构建 shim。它们不承诺提供原版能力,但能让项目重新启动,让其余已经开源的功能正常工作。
这也让我第一次意识到:开源不等于可构建,可构建也不等于可用。
二、汉化不是替换字符串
真正的本地化工作从界面开始。
Sapphire 的主体是一个 SwiftUI 应用。SwiftUI 中看起来最普通的一行代码,可能正是本地化失败的原因:
Text(title)
如果 title 是一个普通的 String,SwiftUI 会把它当作已经处理好的显示文本,不会主动去查本地化表。更可靠的写法是:
Text(LocalizedStringKey(title))
或者在计算属性里直接使用:
String(localized: "Battery")
这类问题不会报错,界面也照常显示。它只是安静地绕过本地化系统,让你以为“已经翻译了”,实际运行时仍然是英文。
这次改造一共处理了 95 个 Swift 文件、约 700 处字符串入口。除了最明显的按钮和标题,还包括:
- 菜单栏菜单项
- 搜索框占位符
.help()提示- 设置分组标题
- 状态字段和统计字段
- 枚举
rawValue的显示文本 - 自定义组件里以
String传参的Text case .x:换行写法的隐式返回- 系统授权弹窗的
InfoPlist.strings
最终,Localizable.strings 收录了 2742 条简体中文文案,InfoPlist.strings 收录了 22 条系统权限说明。
但数量并不是重点。真正的难点是:中文比英文长。
“Battery” 可以放进一个狭窄的设置行,“电池”也没有问题;但 “Reset privileged helper” 变成“重置特权 Helper”之后,按钮就可能挤压旁边的说明文字。刘海应用尤其如此,它的大量界面都围绕一块极小的屏幕区域设计,任何一个多出来的字都可能破坏布局。
所以本地化从来不是“替换文本”,而是在另一种语言里重新检查布局、节奏和信息密度。
三、有些英文,我刻意没有翻译
一开始我尝试把所有英文都翻掉,后来发现那样反而更难用。
Sapphire、Spotify、Apple Music、TIDAL、Gemini Live、Blip、Touch ID、Face ID、HUD、EQ、RSSI、DDC、CPU、GPU、RAM、USB,这些词在中文技术语境里已经有稳定的表达习惯。强行翻译成“触摸标识”“图形处理器”“通用串行总线”,不会让界面更亲切,只会增加理解成本。
浏览器名、开发工具名、Spotify API 字段名也一样。
还有一些词需要区分:
Sidecar在 macOS 的中文界面里叫“随航”AirPlay对应“隔空播放”- 但
Gemini Live、Blip这类产品名保留原文
本地化的目标不是让界面里一个英文单词都不剩,而是让中文用户更快理解。该翻译的翻译,该保留的保留,这比机械地追求“全中文”更重要。
四、订阅弹窗背后,其实是产品边界
Sapphire 的原版包含账户、套餐和订阅门槛。侧边栏有 Guest / Free 卡片,引导流程里有 “Your Sapphire Plan”,部分功能上锁。
在个人使用场景里,我做了两类不同处理。
第一类是空壳功能:Blip、体育、财经、App 锁。它们没有实际实现,或者在公开版本里无法正常工作。与其保留“升级后可用”的假入口,不如直接移除。
第二类是被订阅锁住、但代码已经实现的功能:电池和存储小组件。它们本身是完整的,只是被 PremiumGate 挡住了。把这类功能一并删掉并不合理,所以我选择放开访问,而不是为了“去掉订阅”而牺牲已有能力。
这件事让我想到一个很常见的产品问题:付费墙保护的是商业价值,不是功能价值。
一个功能如果本来就不存在,给它加锁只会制造失望;一个功能如果已经写好,却被一个与当前使用场景无关的账户系统遮住,用户看到的也不是“高级”,而是“坏了”。
五、一个 Helper,和 macOS 签名的坑
这次改造里最技术、也最值得记录的问题,是 Helper 无法启动。
Sapphire 有一个特权 Helper,用来读取电池、风扇和 SMC 等信息。它通过 XPC 与主应用通信。原版代码里有一个签名检查:Helper 和主应用都使用 ad-hoc 签名时,理论上应该允许连接。
但判断条件写成了类似这样:
geteuid() != 0 && helper.isAdHoc && client.isAdHoc
问题在于,Helper 是 root 守护进程。geteuid() 返回 0,所以 geteuid() != 0 永远为 false。即使两个 ad-hoc 签名完全匹配,XPC 连接仍然会被拒绝。
修复只需要改一行,但定位它并不容易。最终保留前面的 bundle id 和签名标识校验,只调整 ad-hoc 兜底条件,Helper 就能正常启动,电池页的风扇、功耗、循环次数和健康度也都恢复了。
另一个坑是 TCC 权限。
ad-hoc 签名默认的 designated requirement 可能使用 cdhash。这意味着每次重新构建,哈希都会变化,macOS 会把新版本当成另一个应用。于是系统设置里辅助功能和屏幕录制看起来是开着的,应用自己却检测不到权限。
我的处理方式是先整体 ad-hoc 签名,再给主程序添加固定 designated requirement:
codesign --force --sign - \
--requirements '=designated => identifier "com.cshariq.sapphire"' \
Sapphire.app
这样重新构建后,TCC 权限就不会因为哈希变化而失效。代价是:Keychain 里的 API Key 仍可能在重建后需要重填;如果要彻底稳定,最终还是需要正式的开发者证书。
这里没有“绕过 macOS 安全机制”的捷径。能做的,是把签名身份和权限记录整理成系统能够持续识别的方式。
六、最后得到的不是一个“中文皮肤”
这次改造的产物包括:
- 2742 条简体中文界面文案
- 22 条系统授权弹窗文案
- 95 个 Swift 文件的本地化改造
- 109 个文件、482 KB 的完整补丁
- 可编译的公开工程和一套公共扩展 stub
- 修复后的 Helper
- 去掉账户系统、空壳功能和无效订阅门槛后的个人可用版本
补丁已经在对应上游提交 475f309 上验证过,可以干净地 git apply。
但它仍然不是一个“完整汉化版”。Continuity、OCR 镜射取字、存储空间地图、Blip、体育、财经和 App 锁这些私有子系统没有开源,本版只能提供空实现或直接移除;自动更新在 ad-hoc 签名下也不可用。
这反而让我更清楚地看到开源项目的边界:你能修改什么,取决于代码真正公开了什么。
七、这次改造留下的三个判断
如果让我总结这次项目,我会留下三个判断。
第一,本地化是工程问题,不是翻译问题。 一个字符串能不能被系统识别,取决于它是不是走了本地化入口;一段中文能不能放进界面,取决于布局有没有为更长文本留出空间。
第二,开源项目的“可用”需要自己重新定义。 公开仓库可能缺模块,开源代码可能缺实现,能编译不代表能运行,能运行也不代表功能完整。诚实地标记空实现,比假装一切正常更重要。
第三,产品上的克制,不等于功能上的残缺。 去掉账户、订阅和空壳入口之后,应用反而更安静,也更接近一个真正属于使用者的工具。
我最初只是想把它变成中文。
最后改掉的,其实是它和我之间的关系:不再是一个“看起来功能很多,但处处被挡住”的应用,而是一个边界清楚、能自己维护、能日常使用的工具。
这大概也是折腾开源项目最有意思的地方。你不是在消费一个成品,而是在重新决定它应该成为什么。
致谢: 感谢 Shariq Charolia 及 Sapphire 的所有贡献者将项目开源。本文涉及的改造基于 cshariq/Sapphire 的上游提交 475f309,原项目采用 AGPL-3.0 许可证。本文所述的汉化与个人定制内容并非官方发行版。
评论 0
留下想法或补充。评论经审核后会显示在这里。
正在读取评论…
成为第一个留下想法的人。