一个英文按钮在开发机上只有六个字母,换成德语后可能占满整行;左侧返回按钮在从右到左界面里,还可能继续指向错误方向。等翻译全部完成再发现这些问题,修复会同时牵动布局、测试与发布时间。更稳妥的做法,是在云端 Mac 的每次合并检查中主动放大字符串、翻转布局方向,并把不可见或不可操作的控件直接判为失败。
先定义门禁要抓什么
伪本地化不是把界面换成一套难以阅读的字符,而是用可控变换放大布局风险。建议先建立两种模式:
expanded:保留原文,在两侧加标记,并把字符串扩展约 40%。rtl:保留可识别内容,但启用从右到左的语义方向,检查排列与图标。- 正常模式:作为对照组,确认测试辅助代码没有改变产品行为。
门禁重点检查四类问题:文本控件的可见区域明显不足;主要按钮不存在、不可点击或超出窗口;界面出现未经过本地化入口的硬编码文案;导航、水平列表和方向图标没有随 RTL 改变。
伪本地化适合暴露结构问题,不负责判断译文是否准确。真实语言验收仍然必要,但不应承担发现固定宽度这种基础故障的成本。
建立可控的字符串变换层
不要在测试里遍历视图并修改标签。那会绕过应用真实的字符串获取路径,也无法发现硬编码。更可靠的方式是让工程已有的本地化入口支持一个仅在调试构建中生效的转换器。
enum PseudoMode: String {
case expanded
case rtl
}
#if DEBUG
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
guard let mode else { return value }
switch mode {
case .expanded:
let padding = String(repeating: "·", count: max(2, value.count * 2 / 5))
return "[\(value)\(padding)]"
case .rtl:
return "\(value)"
}
}
#else
func pseudoTransform(_ value: String, mode: PseudoMode?) -> String {
value
}
#endif
应用启动时读取 PSEUDO_LOCALE,只接受明确枚举值。所有面向用户的字符串经过统一入口,例如 L10n.text("checkout.confirm"),再交给转换器。若某个标题没有出现边界标记,就能判断它绕过了本地化层。
RTL 模式还要在应用根视图设置语义方向。UIKit 可对测试窗口设置 semanticContentAttribute = .forceRightToLeft;SwiftUI 可在根视图注入 layoutDirection。不要反转图片内容,也不要把数字、路径和代码片段强行倒序。
用 UI 测试检查可见性与截断
测试不需要比较每个像素。先覆盖登录后首页、列表、详情、表单和错误态等关键路径,并为主要控件设置稳定的 accessibility identifier。测试进程通过环境变量启动对应模式:
func launchApp(mode: String) -> XCUIApplication {
let app = XCUIApplication()
app.launchEnvironment["PSEUDO_LOCALE"] = mode
app.launchArguments += ["-UI_TESTING", "1"]
app.launch()
return app
}
func testExpandedCheckoutLayout() {
let app = launchApp(mode: "expanded")
let confirm = app.buttons["checkout.confirm"]
XCTAssertTrue(confirm.waitForExistence(timeout: 8))
XCTAssertTrue(confirm.isHittable)
XCTAssertFalse(confirm.frame.isEmpty)
XCTAssertTrue(app.windows.firstMatch.frame.contains(confirm.frame))
}
isHittable 比单纯的 exists 更有价值:按钮可能存在于无障碍树中,却被弹层盖住或落在屏幕外。对标签类控件,可结合 identifier 获取 frame,并验证它没有超出所属容器。不要用 OCR 猜测文本是否截断;优先让关键组件在调试构建中暴露 intrinsicContentSize 与实际 frame 的差值。
把检查范围做成清单
| 场景 | expanded 检查 | RTL 检查 | 失败证据 |
|---|---|---|---|
| 导航栏 | 标题与操作不重叠 | 返回方向与顺序正确 | 界面层级、控件 frame |
| 表单 | 标签可换行,错误文案完整 | 标签与输入轴一致 | 字段 identifier、当前值 |
| 列表 | 长标题不挤掉主操作 | 附件与箭头位置正确 | 失败行索引、截图 |
| 弹层 | 按钮完整且可点击 | 操作顺序符合语义 | 弹层 frame、元素树 |
在云端 Mac 上组织执行环境
为每个任务创建独立模拟器设备集,避免并发作业共享启动状态。设备集目录放在任务临时目录中,结束时无论成功或失败都执行清理。测试前固定 Xcode 路径、模拟器型号和系统版本,并把这些值写进报告。
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
export PSEUDO_LOCALE="${PSEUDO_LOCALE:-expanded}"
RESULT_DIR="$PWD/TestResults/$PSEUDO_LOCALE"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace App.xcworkspace \
-scheme AppUITests \
-destination "platform=iOS Simulator,name=iPhone 16,OS=latest" \
-resultBundlePath "$RESULT_DIR/LayoutTests.xcresult" \
PSEUDO_LOCALE="$PSEUDO_LOCALE"
expanded 与 rtl 应作为两个独立任务执行,而不是在同一模拟器会话里切换。这样失败能明确归属,重跑也不会继承上一个方向模式。云端 Mac 若同时运行多个作业,每个作业还要使用独立的 DerivedData 路径,避免测试产物交叉覆盖。
处理常见误报与真实故障
最常见的误报来自动画尚未结束、系统弹层未关闭和测试数据长度不稳定。解决方式不是增加统一睡眠时间,而是等待明确状态:加载指示器消失、目标元素出现、窗口数量稳定。测试数据也要固定,尤其是用户名、日期和服务端错误文本。
真实故障通常集中在固定高度、单行标签、手工计算宽度以及仅靠视觉位置表达顺序的组件。修复时优先移除约束,而不是为某种语言增加特例。按钮标题允许换行后,还要重新检查点击区域是否覆盖完整文本。
发现失败时,至少归档以下内容:
- 当前伪语言模式、Xcode 与模拟器版本。
- 失败控件的 identifier、frame、label 和是否可点击。
- 当前页面的无障碍层级。
- 对应的
xcresult与失败时截图。 - 复现该页面所需的固定测试数据。
设置合并阈值并防止泄漏
门禁应阻断影响任务完成的问题:主操作不可见或不可点击、文本截断后失去含义、RTL 顺序错误、交互控件缺少可访问名称。轻微间距变化可以记录为待复核项,不必让所有像素差异都阻断合并。
最后增加一条 Release 验收:扫描构建设置与应用资源,确认没有测试专用伪语言文件,并验证正式构建忽略 PSEUDO_LOCALE。同时在正常模式跑一组冒烟测试,防止转换层本身改变字符串。
这套流程的价值不在于生成一张看起来奇怪的界面,而在于把多语言布局从发布前人工抽查,变成每次代码变更都能重复执行的工程约束。先覆盖主路径,再逐步加入错误态和低频弹层,门禁才能保持稳定且便于维护。
常见问题
伪本地化测试可以替代真实语言验收吗?
不能。它适合快速发现固定宽度、字符串未本地化和双向布局错误,最终仍需用至少一种长文本语言和一种 RTL 语言完成真实文案验收。
如何避免伪本地化代码进入正式版本?
将转换器放在 DEBUG 编译条件内,并让 Release 构建在发现 PSEUDO_LOCALE 环境变量或测试专用资源时直接失败。
哪些布局问题应该阻断合并?
核心操作不可见、按钮无法点击、文本被截断到失去含义、RTL 顺序错误和无障碍标签缺失应阻断合并;纯像素级差异可先进入人工复核。
把下一次构建放到云端 Mac mini
选择配置、计费周期与节点区域,使用独享物理机运行开发、构建和自动化任务。