上一篇写完时,听城还在等 App Store 审核。上架以后,我很快又开始想下个版本做什么。7 月 22 日,在 Codex 里定下的计划是多语言支持,加上一套能先在自己手机上试、不会影响正式用户的内容测试流程。

多语言这件事惦记起来很自然:听城当时已经有 12 座城市、1,427 则故事,西雅图、温哥华、伦敦都在里面,但只能用简体中文读。我想把台港繁中、美式和英式英语也补上。

不过,开工时我特意追了一句:

注意千万不要影响我目前的prod ota

OTA 就是不用去 App Store 更新 App,也能收到新的城市故事。新文案一旦发错地方,影响的就不只是我手上的测试机。所以在试多语言之前,我得先保证:自己这边随便改,已经装出去的正式版还能照常用。

多语言该怎么写

听城的大部分文字是故事,按钮和设置项反倒是小头。让 AI 翻译一千多篇并不难,但我不太想让另外几个版本读起来,都像同一篇简中稿换了一套字。

我很快想到一个更有意思的问题:同一件城市往事,换一批读者,能不能连切入角度也换一下?这和给按钮换语言不一样。有的故事要先解释一个当地人熟悉的东西,有的可以直接从眼前的建筑讲起。开头多花一句话解释什么,会影响后面整篇的节奏。

所以没有立刻铺开。我先拿西雅图的地下街、Ballard Locks 船闸和 Panama Hotel 做试稿,只放在本地看效果。选这几篇也有意让问题不一样:地下街会碰到人行道、行人路、sidewalk、pavement 这些用词;船闸可以试试电梯和升降机的比喻;Panama Hotel 涉及日裔美国人被强制迁移和监禁的历史,就得格外小心,不能为了写得顺,把谁对谁做了什么省略掉。

语言也不能直接当成读者背景。不能因为是英文稿,就假设对方一定熟悉美国历史。我想调整的是解释多少、从哪里开始讲,日期、因果和责任归属还是得对着同一份来源核。

这也是我愿意在多语言上多花时间的部分。听城本来就不是靠信息量取胜的小百科,我希望故事短一点、读着顺一点。换了语言以后,如果只剩「资料翻译准确」,原来在文风上花的功夫就白做了。

做到温哥华和西雅图的完整文案时,我又卡在一个小地方。session 里我问:

不过就是常用语方面,似乎台湾会把自行车称为脚踏车?

接着又问,能不能先整理一套各地常用表达,后面都按这个标准写。否则这一篇改成了「腳踏車」,下一篇 AI 又写回「自行车」,每次审稿都得重新提醒。

但词表也不能变成一键替换。台湾日常说「腳踏車」,正式设施名称里仍可能出现「自行車道」;香港稿可以用当地习惯的词,写西雅图的电车时却不能顺手叫成「叮叮」。我想让读者读得自然,又不想把故事里的城市也一起换掉。

最后整理了各地区的用词和写法约定,放进后续的改写、审稿流程。中文源稿改了,相应译稿也要重新看,不能因为以前检查过,就一直算它没问题。我反复挑出来的那些毛病,也总算有地方记下来,不用等到下个 session 再提醒一遍。

按钮变成英文,故事怎么还是中文

中间有一版装到手机上,底部标签已经变成 Now / Stories / Echoes / Settings,点进去故事还是中文,「回声」里甚至没有内容。看起来特别像语言切换只做了一半。

故事详情页的 Back、Visited 和 Passing Reminder 已是英文,标题和正文仍是中文
7 月 23 日的测试截图:按钮已经说英文了,故事还没跟上。
英文 Echoes 页面只显示 No new echoes for now,没有主题内容
同一轮测试里的英文「回声」,只剩一句 No new echoes for now。

实际上,界面文字跟着 App 发版,城市故事是另外下载的。那时前者做好了,完整的英文内容还没发上去,本地几篇试稿当然也不会自动出现在手机里。App 只能先显示已有的中文。

这一点在开发记录里很好区分,拿起手机就没那么好解释了:用户选的是英语,不会关心哪一部分跟着安装包走、哪一部分还在等下载。我也不想拿几篇译好的故事夹着大量中文,算作一座城市已经支持英文。所以后来先把温哥华做完整,再扩到西雅图和其他城市,连栏目介绍这些容易漏掉的地方一起补。到 7 月 25 日,当时 12 座城市都备齐了五个地区版本的内容。

温哥华的完整包发出去之后,还遇到第一次能下载、再切语言就失败的问题。最后查到 HTTP 的 304 Not Modified:服务器是在说「你已有的那份没变,不用再下载」,客户端却把 iOS 返回的空响应处理错了。原来的测试没有模拟出真机上的这种返回方式,修复时也把对应的测试补上了。

英文标点也返过工。我看见全角标点,叫 Codex 去改英文内容包,查完才发现,有些标点是界面自己加上去的,改文案没用。还有英文城市名和分享卡文字太长,挤出了原来给中文留的位置。之后那几轮测试,基本就是一边切语言,一边看哪里又装不下了。

Dude Chilling Park 的英文分享卡,顶部地名挤到编号旁,正文末尾被截断
7 月 24 日,英文分享卡已经能生成,但地名和正文都挤得很勉强。
英文城刊列表顶部的 Greater Vancouver 城市选择按钮延伸到屏幕右侧边缘
同一天,Greater Vancouver 又把顶部的城市选择挤出了屏幕。

给自己一个能放心测试的地方

之所以敢在手机上试这些还没做完的东西,是因为前面先把内容更新分开了。

我最早的打算是开一条新分支做 1.1.0,旧版有 bug 就在旧版上修。这能隔开代码,但听城的故事是从云端更新的。如果新旧 App 还去同一个地方拿内容,我在新分支上传一批测试稿,正式用户一样会收到。Git 不会替我拦住它。

所以需要单独的测试内容渠道。自己先在 TestFlight 上看,确认没问题,再让正式版收到那份已经看过的内容。这里我特别在意的是「那一份」:测试期间仓库还在继续改,正式发布时不能顺手把最新工作区重新打包,弄出一份其实没在手机上看过的东西。旧用户还没升级 App,也得继续有他们能读的内容,不能要求所有人和我同时更新。

第一轮验证没有直接发整座城市。我先在测试版的更新页放了一条多语言通知,确认自己收到了,再检查正式版那边确实没变。之后才开始发完整城市包。

后面准备上架时,还有一个容易绕晕的地方:从 TestFlight 安装,不等于必须读测试内容。要上架的候选 App,也可以先通过 TestFlight 装到自己手机上,连接正式内容渠道,把这一整套组合再试一遍。我后来测的就是这份准备交给正式用户的版本。

开车等红灯,也算停下来了?

做多语言期间,我仍然每天带着听城出门。7 月 25 日,另外开了个 session 查通知:在西雅图走过一些地点,App 里已经标了路过,手机却没怎么弹消息。

查完发现,记录路过和安排通知这两件事被混在了一起。有些地点虽然记下来了,通知却没有成功安排出去,后面又被当作已经提醒过而跳过。所以这次修复得把两件事分清,不能拿一条路过记录证明通知已经办妥。这里也只能确认有没有成功交给系统,至于用户最终有没有看见,又是另一回事。

测试通知能弹了,我接着发现驾驶汇总也不太灵。这个功能原本想做的事很简单:在车上先别一条条打扰,沿途故事留着,停下来以后汇总给你看。可城市里开车经常走走停停,只看速度下降,等红灯也容易被当成行程结束。

我去问 Codex,能不能知道 iPhone 当前是不是处于驾驶模式。聊到可以借助系统的运动状态判断后,我先追问的却是:这会不会变成在收集用户的运动数据?

前面几篇写过,我自己就不喜欢一个一直拿着定位的 App 再把信息往外发。为了少误判几次红灯,要是顺便多收一份运动记录,我是不愿意的。最后采用的方案只在本机用系统提供的粗略活动状态辅助判断,不读取步数,也不上传运动状态;不授予这项权限,仍然可以靠原有的定位判断工作。

结果功能做完,我自己都没找到该在哪儿设置。session 里的原话是:

以及这个驾驶模式好像现在没有什么说明文本,都不知道这是干啥的

我参与了从想法到实现的整个过程,拿着手机还是不知道怎么用,更别说别人。于是设置页又改了一遍,把驾驶汇总单独放出来,解释它什么时候提醒、为什么要权限,以及不开权限会怎样。权限入口和初次使用的说明也跟着调整。

7 月 29 日的英文设置页,在 Permissions & Background 中并列显示定位、通知和驾驶识别的状态与调整入口

7 月 29 日,Build 115 的设置页。定位、通知和驾驶识别已经放到一起,旁边能找到调整入口;英文排版还在继续改。

这些都不是最初多语言计划里的内容,但我明确说了,还是放在 1.1.0 一起发。正式版还没提交,国际化和驾驶改进就算同一轮更新,没必要因为中间多发了几个测试包,又跳到 1.1.1。

Build 114 好好的,怎么又多了这么多代码

7 月底,Build 114 我测着已经没什么大问题,只是用的还是测试内容渠道。我问 Codex,如果要在 App Store 更新 1.1.0,还需要做什么。

它列了一套发布前的工作,包括正式内容的切换、校验、恢复,以及相应的测试。我看完同意了,让它做前几项并验证。

等做完,改动量到了 19,000 多行,里面有不少测试和发布配套。我又回来问:

刚才我看加了很多东西,都在改什么内容?有必要么?我目前测的build 114没有什么大问题,只是ota用的是beta通道

这次不能全怪 AI 自己发挥,前面的清单是我点头的。每一项单独看都像是发布前应该有的东西,合起来却已经远远超过我原本想做的那次切换。代码出来得太快,我还没逐项想清楚哪些现在就要维护,它已经写完了。

重新对了一遍眼前的需求,接下来那份上架候选先围绕必要的改动来准备:接上正式内容,确认拿到的是已经测过的那批,别影响旧用户。我仍然用 TestFlight 装到手机上验证。

较完整的发布工具后来仍进了主线,并没有全部扔掉。不过,准备上架候选时,先抽出了这次需要的部分来验证。写这篇回看时,我觉得这次该更早问的就是 session 里那句「有必要么」——在同意开工之前问。

最后卡在开屏

接上正式内容之后,我以为差不多了,开屏又出了问题。

听城有一张带品牌文字的底图,还有每座城市自己的 wave 动画。我想要的效果就是点开 App 时,它们能好好地待在同一个画面里。真机上却会先出现没字的图,再蹦出文字和 wave;改着改着,又变成了只有文字没有图。单独渲染出来的字体,跟原先图片里的字也不太一样。

这里麻烦在于,用户看见的一次开屏,其实有系统启动画面和 App 自己画的画面在接力,城市 wave 还要等定位。每部分单看都能显示,接起来就可能闪一下、晚一拍。

折腾到 Build 119,我的要求已经退到:

要不直接用几张静态图片也行

最后把底图、品牌名和 slogan 预先做在一起,让启动前后用同一张图,少几样需要现场拼起来的东西。城市 wave 还是留了下来,但也得约束等待定位的时间,拿不到就用备用画面,不能为了播对一段动画,让人一直进不去 App。

到 Build 122,我终于在 session 里说,这版感觉没什么问题,可以作为 App Store 的 1.1.0 候选了。8 月 1 日,确认 1.1.0 已经上线,才把开发分支合回主线。

这一篇就写到这里。1.2.0 以后那些新的想法,留到下一篇慢慢讲。