接多语言这个需求下来的时候,我以为就是「把中文抽成键值、加几份语言包、切换的时候换一份」。做到一半发现问题远不止这些,四类。
一是布局被撑破。我们的界面是按中文排版的,中文很紧凑,四个字的按钮换成英文可能是十几个字符。结果是按钮文字换行、导航栏挤在一起、卡片上的标题溢出遮住旁边的价格。这不是几个页面的问题,是几乎每一页都要检查。
二是日期和数字是字符串硬拼的。代码里到处是把年月日拼成中文格式的写法。切到英文之后还是中文格式——而且这些拼接散落在几十个地方,没法统一改。
三是界面切了语言,服务端返回的内容还是中文。产品名称、政策说明、退改规则这些内容都来自服务端。我只做了前端文案的切换,请求里没有带语言标识,所以用户看到的是「英文的界面框架 + 中文的产品信息」,比全中文更糟(他会以为翻译坏了)。
四是币种切换让用户误解了扣款金额。我做了个币种切换,把价格按汇率换算展示。但没有说明「这是参考换算、实际按某个币种结算」。有用户按展示的币种理解最终扣款,实际扣款因为汇率和结算币种不同而有差异,来投诉。
所以四个改动:逐页改弹性布局并建立以最长语种验收的流程、日期数字货币收敛到统一格式化入口、语言选择持久化并随请求带给服务端、币种展示标注换算说明与实际结算币种,另外把散落的文案收敛成统一键值结构并做按需加载与缺失降级。
这个模块最想说的一句话是:多语言的工作量大头不在翻译,在「原来的实现里隐含了多少对中文的假设」。固定宽度隐含了「文案就这么长」、字符串拼接隐含了「日期就是这个格式」、不传语言参数隐含了「服务端内容只有一种语言」。这些假设在只有中文的时候完全看不出来,加第二种语言时全部暴露——所以它的难点是「找出全部隐含假设」,而不是「写一个切换函数」。
语言包按需加载而不是全部打进主包。全部打进最简单、切换零延迟,但小程序有包体积限制,语种数量一多就顶到上限。按需加载的代价是切换时有一次加载(要处理加载态和失败回退)。判据是「语种数量会不会继续增加」——会的话就必须按需,否则每加一个语种都要重新评估包体积。
缺失键降级到默认语种,而不是显示键名或者空白。显示键名对开发调试友好(一眼看出哪个键漏了),但对用户是灾难(看到一串标识以为程序坏了)。我的处理是:正式环境降级到默认语种文案,开发环境显示键名并在控制台告警——两个目标都满足。这类「开发友好」和「用户友好」冲突的地方,按环境区分是最常见的正确解法。
切换语言选择「局部刷新文案」而不是「重启应用」。重启最简单可靠(所有地方都会重新读语言包),但用户正在填的表单、正在看的列表位置都会丢——而切换语言这个动作本身不该产生副作用。局部刷新的代价是要保证所有文案都是响应式读取的,有硬编码的地方就不会跟着变(这也反过来帮我们找出了漏抽的文案)。
币种做「展示换算 + 明确标注」而不是「按用户选择的币种结算」。后者体验更完整,但涉及汇率锁定、结算通道、退款币种一致性,远超前端范围。而只做展示换算却不标注,就是在制造误解——所以标注是这个功能能上线的前提。判据是「用户会不会因为这个展示做出错误的判断」:会的话就必须把不确定性说清楚。
踩过的坑一:日期时间是字符串硬拼的,散落几十处。切到英文之后还是中文格式,而我要一处一处找出来改。这个坑花的时间比我预估的多得多,而且很难确认改全了(有些是在条件分支里)。教训是:任何「格式化」的动作都应该从一开始就走统一入口,即使当时只有一种格式——因为「只有一种格式」这个假设迟早会被打破,而到那时改造成本是几十倍。
踩过的坑二:只做了前端文案,没给服务端传语言。用户看到英文界面加中文产品名,反馈是「你们的翻译坏了」。教训是:多语言不是前端一个人的事——界面上的内容有一部分来自服务端,不做语言协商就只完成了一半。我当时的思路完全停留在「前端文案替换」上。
踩过的坑三:按中文验收,上线后英文版大量布局问题。我自己测的时候界面都是正常的(因为我看的是中文)。教训是:验收标准要按「最坏情况」定——多语言场景的最坏情况是最长的那个语种。后来我把「以最长语种过一遍全部页面」固化成了流程,这比逐个修 bug 有效得多。
没做的部分:没做从右向左书写语言的支持。它需要整套布局的镜像(图标方向、滑动方向、对齐方式),工作量远大于加一个语言包,而当前的目标市场不涉及。我在文档里标注了「当前布局假设从左向右」,以便将来评估。把假设写下来,比默默留一个坑要好。
先报规模:几种语言、多少条文案键、语言包多大、主包体积变化。主包体积变化这一项最能说明「按需加载」的价值——报「新增 N 个语种,主包体积基本不变」。
布局问题的发现要报流程而不只是数量:「以最长语种逐页检查,发现并修复 N 处布局问题」。要说明检查覆盖了多少个页面——覆盖面比修了多少个更能说明可靠性。
格式化收敛的验证:断言切换语言后日期、时间、数字、货币、相对时间全部按目标语言呈现。要做成清单逐项核对,因为漏掉一处就会出现中英混杂。
缺失键降级:故意删除某个键,断言正式环境回退默认语种文案、开发环境显示键名并告警。
语言协商:断言切换语言后请求携带了语言标识、服务端返回内容随之变化;服务端无对应语言时返回默认语言且前端给出提示。后一条容易漏。
切换不丢状态:在表单填一半时切换语言,断言已填内容与滚动位置保留。
币种标注:断言换算展示处都有「仅供参考 + 实际结算币种 + 汇率时间」,支付页展示的是实际结算币种与金额。这条是合规性质的,必须逐处核对。
不要报什么:不要报「多语言覆盖率 100%」——服务端内容的翻译完备度不由前端决定。该报的是「语种数量与主包体积基本不变」「以最长语种逐页检查的覆盖页数与修复数」「格式化项逐项核对通过」「缺失键按环境分别降级」这几件可核对的事。
凭证页第一版是最常规的写法:进页面请求接口拿凭证数据,二维码是服务端生成的图片,前端直接展示。时间按设备时区格式化。四个问题,全部只在真实出行场景下暴露。
一是境外或弱网下页面空白。用户下了飞机、没有当地网络、漫游也没开,打开 App 想看凭证——请求失败,页面空白。而这正是他最需要凭证的时刻。更糟的是二维码是服务端图片,即使凭证文字信息有缓存,码图也加载不出来。
二是跨时区后时间显示错了。我按设备时区格式化出行时间。用户从国内飞到另一个时区,手机自动切换了时区,他看到的登机时间就偏移了好几个小时——而航班时间本来就是当地时间,根本不该做换算。有用户因此差点误机。
三是扫码经常扫不上,而且没有替代方案。屏幕亮度低(用户为了省电调暗了)、屏幕有反光,核销设备扫不上。而我的页面上只有一个二维码,没有可供人工录入的编号,用户只能站在那反复调整角度。
四是凭证入口太深。要从「我的订单」进去,找到那个订单,点进详情,再点「查看凭证」。四层。而用户在核销口前面,后面排着队。
所以四个改动:凭证数据本地缓存加二维码本地渲染、出行时间标注出行地时区且不做自动换算、扫码时临时提升亮度并提供文本降级出示、凭证入口提升到首页近期行程卡片直达。
这个模块最想说的一句话是:这个功能的使用场景是「用户站在核销口、可能没网、可能手忙脚乱、后面还排着队」,而我是在办公室有网的电脑上开发和测试的。四个问题没有一个是代码写错了,全都是因为我按自己的处境设计了一个别人用不了的功能。后来我养成的习惯是先把使用场景的约束一条条写下来(有没有网、什么时区、有多少时间、手上还拿着什么),再开始设计。
二维码本地渲染而不是服务端出图,代价是引入一个本地库(包体积增加)并要保证渲染结果被核销设备认可。服务端出图的好处是渲染一致、前端简单。但它有一个致命问题:无网络时码图加载不出来,而凭证的核心就是这个码——文字信息有缓存但码没有,等于凭证不可用。判据是「这个功能在没有网络时还要不要能用」:要,那么任何依赖网络的环节都必须去掉。包体积的增加我实测确认在可接受范围,而且这是必要成本。
缓存优先展示而不是「必须拿到最新数据才展示」。后者更严谨(避免展示过期凭证),但在境外无网络时它等于什么都不给用户。我的取舍是「先给他能用的,同时把不确定性标注清楚」:显著显示数据获取时间、提示「当前为离线数据」、有网络时优先同步作废状态。这一条的核心判断是:在用户最需要的时刻给一份可能过期的数据加一个明确的警示,好过给一个空白页面。
时区选择「不做换算,显式标注」而不是「换算成设备时区」。换算听起来更贴心(用户看到的是自己手机上的时间),但它在这里是错的——航班和酒店的时间本来就是当地时间,换算之后反而不是用户需要的那个数字。而且换算引入了一个新风险:设备时区可能不准(手动设置过、自动切换有延迟)。判据是「原始数据的语义是什么」:语义就是「出行地的当地时间」,那就原样展示并标注。
提升屏幕亮度这类系统级操作要「进入时提升、离开时恢复」而不是一直保持。一直保持会耗电,而用户在旅途中电量往往紧张。这是一个很小的细节,但它体现了「知道使用者的处境」。
踩过的坑一:境外用户打开凭证页是空白的。是客服转过来的反馈——用户在国外没有网络,站在核销口打不开凭证。我当时的第一反应是「加个缓存」,但只缓存了接口数据,二维码还是服务端图片,所以缓存了也用不了。教训是:做离线支持要检查「这条链路上还有哪个环节依赖网络」,只缓存主数据是不够的——一个依赖网络的子资源就能让整个功能失效。
踩过的坑二:按设备时区显示出行时间,用户跨时区后看到偏移数小时的登机时间。有用户因此差点误机,这是我做过的最危险的一个 bug。而它的成因是一个「看起来正确」的做法——把时间戳按用户设备时区格式化,这在大多数业务里是对的。教训是:格式化时间之前必须先问「这个时间的语义是哪个时区的」,而不是无条件按设备时区展示。这类错误在国内测试永远不会暴露。
踩过的坑三:只有二维码,没有可人工录入的编号。扫码失败时用户只能反复调整角度,后面还排着队。教训是:任何依赖硬件识别的交互都要准备人工降级路径——识别失败是常态不是异常(光线、反光、设备差异、屏幕划痕)。
没做的部分:没做把凭证写入系统级的卡包(钱包类应用)。它的离线可用性最好(不用打开我们的 App),但各平台的接入方式差异大、审核要求高,超出了这次改动的范围。我做的是「支持保存到相册」这个最基础的兜底,并把卡包接入作为后续建议提出。
离线可用性必须真的断网测:开飞行模式,断言凭证页能正常展示、二维码能渲染出来、顶部有「离线数据」提示与数据时间。这是这个模块最核心的一条验证,而且必须在真机上做(开发工具的网络模拟不完全一致)。
二维码可用性要用真实的核销设备验证,并报测试条件:「在若干种亮度与屏幕状态下用核销设备扫描,成功率如何」。要说明测了哪些条件(深色主题、低亮度、有反光)——只报一个成功率不说条件没有参考价值。
时区正确性用断言型用例:把设备时区改成不同时区,断言出行时间的显示值不变、且时区标注正确。「显示值不变」是这条用例的关键点——它正好是踩坑的反面。
缓存与更新:断言先渲染缓存再后台更新;更新失败时保留缓存并提示;更新成功后数据时间刷新。
作废同步:凭证在服务端作废后,断言有网络时前端优先同步到作废状态并明确标识。
亮度处理:断言进入页面亮度提升、离开后恢复原值。「恢复」这一步容易漏,而不恢复会持续耗电。
入口可达性可以报操作步数:从打开 App 到看到凭证的点击次数,从四次降到一次。这个数字很具体。
不要报什么:不要报「扫码成功率 100%」——硬件识别受太多物理条件影响。该报的是「飞行模式下凭证与二维码可正常展示」「设备时区变化时显示值不变且标注正确」「多种屏幕条件下的扫码结果与测试条件」「入口点击次数从四次降到一次」这几件可核对的事。
出行前提醒这个需求看起来简单:出发前几小时给用户发个提醒。第一版我用了最直接的做法——在客户端算出提醒时刻,注册一个本地通知。四个问题。
一是提醒时间算错了,跨时区行程下偏移数小时。我用设备时区算「出行时间减 3 小时」。但出行时间是出行地的当地时间,而用户下单时还在国内。结果是提醒在完全不对的时刻触发——有的提前了大半天,有的在他已经登机之后才响。这和凭证页的时区问题同源,但表现更隐蔽(用户不会来报告「提醒时间不对」,他只会觉得这个功能没用)。
二是小程序和 App 的通知能力完全不同,我用了同一套逻辑。原生 App 可以注册本地通知(不依赖网络、到点就响);小程序没有本地通知,只能用订阅消息,而订阅消息受单次授权限制——用户授权一次只能发一条。我按 App 的思路做了「出发前 24 小时、3 小时、1 小时各提醒一次」,在小程序上只有第一条能发出去。
三是权限申请时机不对,授权率很低。我在用户第一次打开 App 时就申请通知权限。那时他还不知道我们要通知他什么,大多数人直接拒绝——而一旦拒绝,后面就很难再要到了(要引导他去系统设置里手动开)。
四是日历事件重复堆积。我做了「添加到日历」,但没有做去重。用户点两次就写入两个事件;改签之后又写一个新的,旧的还在——他的日历里有三个同一趟航班的事件,时间还不一样。
所以四个改动:提醒时点以出行地时区的出行时间为基准推算、按端做通知策略差异(小程序合并提醒内容)、权限申请挪到用户明确表达需要提醒的时刻、日历写入做去重与变更同步,另外把提醒内容改成包含关键信息。
这个模块最想说的一句话是:提醒类功能的价值完全取决于「在正确的时间、通过用户真的能收到的渠道、送达他真的用得上的信息」这三件事同时成立。我第一版三件都没做到:时间算错了、渠道在小程序上失效了、内容只说「有行程即将开始」(他知道自己有行程,他需要的是航班号和值机口)。而这个功能上线后看起来是「正常工作」的——因为通知确实发出去了,只是没有产生任何价值。
分端做不同的通知策略,而不是求一个「两端都能跑」的最小公倍数。取最小公倍数(两端都只发一条)实现最简单,但会浪费原生 App 上「本地通知可以注册多个时点、且离线可触发」这个真实优势——而出行场景恰恰经常没网络。代价是两端的逻辑不同,要分别测。判据是「这个平台能力的差异是否影响到功能的核心价值」:影响,那就必须分别设计。把平台差异当成「兼容问题」去抹平,往往会把两端都做成较差的那个。
客户端注册与服务端定时推送两条路都走,代价是可能重复。只走客户端的问题是它可能因为卸载、清缓存、权限变更而静默失效,而用户不会知道;只走服务端的问题是无网络时收不到——而这正是出行场景的常态。两条都走加客户端去重,是在「可靠性」和「重复风险」之间选了前者。判据是「漏掉一次提醒的代价」:误机的代价远大于多收一条通知。
权限申请挪到「用户明确表达需要」的时刻,代价是有一部分用户永远不会走到那个入口(所以永远不会被申请)。首次打开就申请能覆盖所有用户,但授权率很低,而且拒绝之后基本无法挽回(要引导他进系统设置)。判据是「授权率 × 覆盖率」而不是单看覆盖率——在合适时机申请,实际拿到的授权总数更多。而且被拒绝的用户还有日历写入这条降级路径。
日历写入作为主要降级路径,而不是「引导用户去系统设置开通知」。后者的转化率很低(步骤多、界面陌生)。而日历写入不需要通知权限,且用户对日历的信任度更高(他本来就用日历安排行程)。判据是「哪条降级路径的完成率更高」——不是「哪条更接近原方案」。
踩过的坑一:提醒时刻按设备时区推算,跨时区行程下完全错乱。有的提前大半天、有的在用户已经登机之后才响。而这个坑最麻烦的地方是它不会被投诉——用户不会来说「你们的提醒时间不对」,他只会觉得这个功能没用,然后关掉它。我是在查提醒点击率异常低的时候才发现的。教训是:功能上线后「看起来正常工作」不代表它在产生价值——通知确实发出去了,只是发在了错误的时刻。所以要看的不是「发送成功数」而是「点击打开数」。
踩过的坑二:按原生 App 的思路做了三个提醒时点,小程序上只有第一条发出去。我以为订阅消息和通知是类似的东西,没有仔细看它的单次授权限制。教训是:跨端开发时,「同名的能力」在不同平台可能有完全不同的约束模型——不能假设它们只是 API 形式不同。正确做法是先读清各端的限制,再设计功能形态,而不是设计好了再去适配。
踩过的坑三:日历事件不去重、不同步变更。用户的日历里出现三个同一航班的事件、时间还都不一样。这个反馈很直接(有用户截图给客服)。教训是:任何「写入到用户自己的空间」的操作(日历、相册、通讯录、文件),都必须做去重和生命周期管理——因为这些数据的清理成本在用户身上,而他会因此觉得我们的产品很粗糙。
没做的部分:没做基于用户实时位置的动态提醒(比如「你离机场还有一小时车程,建议现在出发」)。它价值更高,但需要持续定位(耗电、隐私敏感)和路况数据,超出范围。我做的是「固定提前量 + 提醒内容里给出建议到达时间」,把判断留给用户。
核心指标是「提醒的点击打开率」而不是「发送成功率」。这是我踩坑之后最重要的认识——发送成功但时刻错误的提醒,发送成功率是满的,点击率接近零。报法是分端、分提醒时点统计点击率。
时区正确性用断言型用例:构造不同时区的出行地,断言计算出的提醒绝对时刻等于「出行地当地时间往前推 N 小时」对应的时刻;把设备时区改掉,断言结果不变。后一条是踩坑的直接回归。
改签同步:改签后断言旧提醒被替换(不会在原时刻触发)、新提醒时刻正确。「旧提醒不再触发」这一条要真的等到原时刻验证,或者用可控时间测。
分端策略:在小程序端断言只发送合并后的一条且内容包含全部关键信息;在原生端断言多个时点都能触发,且飞行模式下本地通知仍然触发。最后这一条是本地通知的核心价值,必须验证。
授权率要按申请时机拆分对比:报「首次打开时申请 vs 用户主动开启时申请」的授权率。还要报「实际获得授权的用户总数」——因为后者覆盖率低但授权率高,只看比例会得出错误结论。
日历去重与同步:断言重复点击只产生一个事件;改签后事件被更新而不是新增;取消后事件被删除。第三条容易漏。
写入失败:拒绝日历权限,断言给出明确提示而不是静默失败。
不要报什么:不要报「提醒送达率」——它不代表用户看到了,而且时刻错误时这个数字反而很好看。该报的是「分端分时点的点击打开率」「设备时区变化时提醒时刻不变」「飞行模式下本地通知仍触发」「两种申请时机的授权率与实际授权总数」「日历事件去重与变更同步」这几件可核对的事。
没有匹配的内容,换个关键词试试。
项目拆解 · 出行客户端常用页(uni-app · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据