千灵

Back

MapsLingo 语言选择面板

我的 iOS 系统语言是英语,地图 App 语言强制跟随系统语言,无法单独设置。

在国内使用时,地名会显示为英文拼音,难以辨识,如下图。

Apple Maps

于是我开发了 MapsLingo,给 Apple 地图增加了独立语言设置功能。

一、使用方式#

安装注入#

  1. 在多任务界面把 Apple 地图向上划掉,彻底退出;
  2. 打开 TrollFoolsAdvanced Settings,开启 Prefer Main Executable,其余保持默认;
  3. 只注入 MapsLingo-0.1.0.dylib,不要和应急恢复库 MapsLingoRestore-0.1.0.dylib 同时注入。

切换语言#

  1. 打开地图,首次会自动弹出语言面板(之后在地图内双指按住不动 1.2 秒唤出);
  2. 选择语言并确认,面板上的勾选表示”已保存”,当前界面不会立即变化;
  3. 关键一步:进多任务界面把地图划掉彻底退出,再重新打开。只回桌面不算退出——语言偏好只在下一次冷启动时加载。

二、原理#

iOS 的语言选择落在偏好系统里。每个 App 的沙盒偏好域(这里是 com.apple.Maps)都有自己的 AppleLanguages 数组,App 冷启动时读自己域里的这份值来决定界面语言——不写这份值时才会回落到系统首选语言

所以”独立语言”不需要改系统设置,只需要往地图自己的偏好域里写一份数组,等于替地图做了一个 per-App 语言覆盖:

// language 是地图自带本地化里的语言标识符,如 "en" / "zh-Hans"
const void *values[] = {language};
CFArrayRef requested = CFArrayCreate(kCFAllocatorDefault, values, 1, &kCFTypeArrayCallBacks);
CFPreferencesSetValue(CFSTR("AppleLanguages"), requested, CFSTR("com.apple.Maps"),
                      kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
CFPreferencesSynchronize(CFSTR("com.apple.Maps"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
c

真实代码里这些字符串都是运行时构造的(CFStringCreateWithCString),不落静态常量,见下节。

可选项直接取自地图包自身的本地化资源,不做任何猜测:

CFArrayRef localizations = CFBundleCopyBundleLocalizations(CFBundleGetMainBundle()); // 跳过 "Base" 并去重
c

所以面板里只会出现当前地图版本真正带了的语言;选一个地图没有的语言会被拒绝并提示,不会写脏配置。列表按语言名排序,zh-Hans / zh-Hant / en 置顶。

三、备份与恢复#

只写不备份的插件是不敢用的。MapsLingo 在地图偏好域里另存一份 MapsLingo.SafeBackup.v1,用三个状态描述”谁改的、改到哪了”:

字段含义
originalValue首次修改前的原始值(只在第一次修改时写入,之后不再覆盖)
lastAppliedValue上一次由本插件写入的值
pendingValue正在写入、尚未确认完成的值

写入顺序是:先记备份 → 再写语言 → 回读校验(CFPreferencesSynchronize + 读回比对)→ 清掉 pending。即使中途断电或被杀进程,pending 也能让下一次操作识别出”上次没写完”。

「恢复原设置」同样保守:只有当偏好域当前值仍然等于插件写进去的值(或等于 pending 值)时才会回滚;如果期间用别的方式换过语言,插件会保留你后来的改动并清除旧备份,而不是硬拽回去。

四、为什么不做 Hook#

  • 不替换任何 Objective-C 方法,不用 Substrate / ElleKit,不注入系统框架。
  • 回调类在运行时创建,字符串也运行时构造,二进制里没有静态 ObjC 类表、category、protocol 列表,也没有 __cfstring 段;-no_fixup_chains + ad-hoc 签名,对 arm64e 的 PAC 与签名校验更友好。
  • 插件只在 getprogname() == "Maps" 且 bundle id 为 com.apple.Maps 时启动,误注入到别的 App 里直接不工作。
Class callbackClass = objc_allocateClassPair(objc_getClass("NSObject"), "ML031LanguageCallbacks", 0);
class_addProtocol(callbackClass, objc_getProtocol("UITableViewDataSource"));
// 方法签名从协议里取,避免手写 type encoding 出错
struct objc_method_description method =
    protocol_getMethodDescription(protocol, selector, YES, YES);
class_addMethod(callbackClass, selector, implementation, method.types);
objc_registerClassPair(callbackClass);
c

面板本身是标准 UIKit:UITableViewController(Inset Grouped)+ UINavigationController(Form Sheet),走系统控件、动态字体与深色模式,没有自绘 UI。

唤出用的是双指长按手势,参数都调到”不干扰地图”:

UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:...];
gesture.numberOfTouchesRequired = 2;      // 双指
gesture.minimumPressDuration = 1.2;       // 1.2 秒
gesture.cancelsTouchesInView = NO;        // 不吞地图自己的触摸
gesture.delaysTouchesBegan = NO;
gesture.delaysTouchesEnded = NO;
objc

再实现 gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: 返回 YES,保证拖动、缩放、搜索、导航手势照常工作。手势按窗口安装并用弱引用表去重,前后台切换不会重复叠加。

五、构建#

bash build.sh            # 编译 arm64 与 arm64e 两个切片并 lipo 成通用 dylib
node scripts/verify.mjs  # 校验产物
node scripts/package.mjs # 打包
bash

macOS 走 Xcode 的 iPhoneOS SDK;Linux / WSL 指定交叉工具链即可:

TOOLCHAIN_BIN=/path/to/iphone/bin \
SDKROOT=/path/to/iPhoneOS.sdk \
bash build.sh
bash

官方 0.1.0 的 release 二进制就是在 Linux 上编的(BUILD-INFO 里能看到 clang version 19.0.0git、target x86_64-unknown-linux-gnu、iOS 15.0 最低版本)。

verify.mjs 不是走个过场,它按 Mach-O 结构逐项断言:

  • fat 头 + 两个切片(arm64 / arm64e),切片与单独构建产物字节一致;
  • 依赖白名单(CoreFoundation / UIKit / Foundation / libobjc / libSystem);
  • 没有 __cfstring 段、没有静态 ObjC 类/category/protocol 注册表——这两条是”回归检测”,防止后续改动把静态元数据又带回来;
  • 未启用 chained fixups、最低系统版本、install name、dylib 版本号;
  • 每个代码签名页的哈希逐页校验,且确认是 ad-hoc 签名。

脚本自己也会打印一句免责声明:静态分析与签名校验不能替代设备测试,报告里的 onDeviceVerifiedfalse

六、已知限制#

  • 只切界面语言:App 界面、地图 POI 地名、Siri 导航语音属于不同的数据链路,插件只能改前者,不可能改服务器返回的矢量地名与语音引擎。
  • 必须冷启动生效:多任务划掉重开,只回桌面无效。
  • 只列地图自带语言:地图包里有哪个语言才能选哪个。
  • 未全面验证:iPad、多窗口、VoiceOver、其它 iOS 版本都没有测过;0.1.0 是预发布版。

七、恢复与卸载#

⚠️ 仅在 TrollFools 里移除 dylib 不会撤销已经保存的语言偏好。

方案操作适用场景
A. 跟随系统面板里选「Follow System / 跟随系统」→ 划掉地图重开(可选:移除 dylib)常规还原
B. 恢复原设置面板里选「Restore Original / 恢复原设置」→ 划掉地图要回到首次修改前
C. 应急恢复先移除主库,单独注入 MapsLingoRestore-0.1.0.dylib → 打开地图等 3~5 秒 → 移除恢复库面板打不开时

八、排错#

  • 面板不弹:首次提示关掉后不会每次再弹,用双指长按 1.2 秒唤出;部分辅助功能或手势插件可能冲突。
  • 确认注入目标:TrollFools 日志里应出现 Best matched Mach-O is .../Maps.app/Maps,才算注入到了主程序。
  • mapping process is a platform binary, but mapped file is not:加载或签名异常。先在 TrollFools 里 Eject All,确认原版地图能正常启动,再重来。不要删 Apple 系统框架,也不要盲目叠加注入。
  • 闪退日志:「设置 → 隐私与安全性 → 分析与改进 → 分析数据」,找 Maps- 开头的 .ips 文件;公开反馈前记得隐去设备标识与路径。
Apple 地图独立多语言调整插件:MapsLingo 开发记录
https://qianling.pw/mapslingo/
Author 千灵
Published at September 12, 2026
Comment seems to stuck. Try to refresh?✨