大型小程序国际化实践:滴滴出行 i18n 的工程方法

复盘滴滴出行小程序英文版改造,从文案治理、双线程架构、WXS 翻译函数、跨平台适配和协作流程中提炼大型小程序国际化方法论。

2020 年,滴滴出行小程序需要支持英文版。这个需求看起来是“把中文换成英文”,真正落地时却是一个完整的工程协作问题。

当时小程序里有大量业务线、公共库、前端硬编码文案和服务端下发文案。英文版上线时间明确,前端投入有限,翻译、联调、测试、发布都要在同一条链路里完成。

最后英文版按期上线并稳定运行。这篇文章复盘的重点,不是证明某个框架功能有多强,而是总结大型小程序做国际化时真正需要处理的几类问题。

English version: Internationalization for Large Mini Programs

滴滴出行微信小程序 i18n

国际化不是翻译,是内容和运行时治理

i18n 是 Internationalization 的缩写,指软件具备支持多语言、多地区格式的能力。对应用来说,它不只是把中文翻成英文,还包括:

  • 文案如何收集和命名。
  • 翻译资源如何维护。
  • 模板和 JS 中如何统一取文案。
  • 日期、时间、数字、货币如何格式化。
  • 语言切换后界面如何响应更新。
  • 服务端下发文案如何和前端文案协同。
  • 多业务线如何在同一套规范下接入。

大型小程序的国际化难点,不在单个 $t('hello') 调用,而在规模化治理:业务线多、页面多、平台多、团队多,任何临时约定都会在后续维护中放大成本。

先把文案当资产管理

国际化项目的第一步,不应该是改代码,而是清点文案。

文案通常来自几类地方:

  1. 前端模板里的静态文本。
  2. JS 逻辑里的 toast、弹窗、错误提示。
  3. 组件库和公共库里的默认文案。
  4. 服务端下发的活动、订单、状态和运营文案。
  5. 图片、图标、空状态和营销素材里的文字。

如果不先清点,后面就会出现大量“页面已经切英文,但某个弹窗还是中文”的问题。

可复用的做法是:

  • 给每条文案稳定 key,而不是直接用中文做 key。
  • key 按业务域和页面层级组织,例如 order.detail.cancelTitle
  • 明确哪些文案归前端语言包,哪些由服务端按 locale 下发。
  • 把新增文案纳入代码审查,避免继续写硬编码。
  • 对兜底语言做明确约定,防止 key 缺失时页面空白。

文案一旦有了结构,翻译、测试和后续增量维护才会变成可管理流程。

小程序运行时带来的特殊挑战

Web 应用里,模板表达式调用 JS 函数很自然。但小程序不是标准浏览器运行时。

以微信小程序为例,它采用逻辑层和渲染层分离的架构:

  • 逻辑层运行 JavaScript。
  • 渲染层负责页面展示。
  • 数据通过 setData 从逻辑层传到渲染层。
  • 渲染层不能直接执行普通 JS。
  • WXS 是运行在视图层的类 JS 脚本能力。

这带来一个关键问题:如果所有翻译都在逻辑层完成,再通过 setData 把结果传给模板,语言切换或列表渲染时就会增加线程通信成本。

国际化方案必须考虑运行时边界,而不是只考虑 API 是否好看。

为什么模板翻译函数很重要

理想使用方式应该接近 Web 里的 i18n 体验:

<template>
  <view>{{ $t('message.hello', { name: userName }) }}</view>
  <view>{{ formattedDatetime }}</view>
</template>

在 JS 中也应该能使用同一套能力:

import mpx, { createComponent } from '@mpxjs/core'

createComponent({
  ready () {
    console.log(this.$t('message.hello', { name: 'Didi' }))
    this.$i18n.locale = 'en-US'
  },
  computed: {
    formattedDatetime () {
      return this.$d(new Date(), 'long')
    }
  }
})

这个 API 看起来简单,背后要解决两个问题:

  1. 模板里能不能直接执行翻译函数。
  2. JS 里能不能复用同一份语言包和格式化逻辑。

Mpx 的做法,是在构建阶段把语言字典和翻译函数合成可在视图层执行的 WXS,并自动注入到使用翻译函数的模板中。JS 侧则通过框架能力把对应翻译逻辑转换并注入到逻辑层运行时。

这样模板和 JS 都可以使用统一的 i18n API,同时减少不必要的跨线程数据传递。

语言包应该在构建体系里统一管理

语言包配置通常长这样:

new MpxWebpackPlugin({
  i18n: {
    locale: 'en-US',
    messages: {
      'en-US': {
        message: {
          hello: '{name} world'
        }
      },
      'zh-CN': {
        message: {
          hello: '{name} 世界'
        }
      }
    }
  }
})

语言包既可以直接写在配置里,也可以独立成模块路径。大型项目更适合后者,因为语言资源需要被翻译、审查、测试和持续维护。

把语言包纳入统一构建体系有几个好处:

  • 模板、JS、组件都使用同一份资源。
  • 构建时可以检查 key 是否存在。
  • 语言包更新不会脱离应用构建流程。
  • 多端产物可以共享同一套配置。
  • 后续可以扩展日期、数字、复数等格式化能力。

国际化一旦脱离构建体系,就容易变成“改了语言包但忘记重新生成某份中间产物”的维护问题。

跨平台适配要藏在框架层

小程序国际化还有一个特殊难点:不同平台的视图层脚本能力并不完全一样。

微信有 WXS,支付宝有 SJS,百度、QQ、字节等平台也有自己的语法和运行限制。业务开发者不应该在每个页面里手写一套平台差异处理。

Mpx 的跨平台能力在这里发挥了作用:以微信 WXS 作为 DSL,在构建阶段解析、转换,再输出到不同平台可识别的脚本形式。模板侧和 JS 侧的 i18n 能力都建立在这套转换能力上。

这背后的方法论是:跨平台差异应该被框架和构建系统吸收,而不是泄露给业务代码。

业务代码越接近统一 API,后续语言扩展和平台扩展的成本越低。

和其他方案的取舍

当时也对比过其他思路。

一种方案是利用 computed,把翻译结果在逻辑层算好,再传给模板。这个方案理解成本低,但会增加 setData 通信,列表场景里还可能放大数据传输量。它适合小项目,但在大型复杂页面里会让性能和维护成本变高。

另一种方案是微信官方的 i18n 方案,思路也使用视图层脚本。但如果周边构建、JS 注入、跨平台适配和响应式能力没有统一起来,业务接入仍然需要处理更多零散环节。

Mpx 的优势不只是“能翻译”,而是把几个环节串成一条链:

  • 构建时注入语言包。
  • 模板中直接使用翻译函数。
  • JS 中使用同一套 API。
  • locale 变化可以响应式更新。
  • 跨平台输出由框架统一处理。
  • Web 产物可以复用类似 vue-i18n 的体验。

大型项目选方案时,应该优先看整条链路是否闭合,而不是只比较单个 API。

可复用的方法论

大型小程序 i18n 改造可以抽象成几个步骤。

1. 先做文案盘点和归属划分

把所有文案按来源分类:前端静态文案、服务端动态文案、组件库文案、运营素材文案。先定归属,再改代码。

2. 设计稳定 key,而不是依赖中文原文

中文原文会修改,业务文案会调整。key 应该表达业务含义和位置,不能直接依赖当前文案。

3. 把语言资源纳入构建和审查

新增页面、新增组件、新增 toast,都应该同步新增语言 key。代码审查时要看是否还有硬编码文案。

4. 根据运行时选择翻译执行位置

Web、小程序、React Native、Flutter 的运行时不同,翻译函数放在哪里执行会影响性能和维护成本。小程序尤其要考虑逻辑层和渲染层通信。

5. 让业务代码使用统一 API

业务开发者应该只关心 $t$d$n 这类能力,不应该关心 WXS、SJS 或平台差异。

6. 把测试清单产品化

国际化测试不能只靠看几个页面。至少要覆盖:

  • 首屏和核心流程。
  • toast、弹窗、错误态、空状态。
  • 长英文导致的换行和截断。
  • 日期、时间、金额、单位。
  • 服务端下发文案。
  • 语言切换后的页面刷新。
  • 多平台产物差异。

7. 接受国际化会反推产品设计

中文短,英文长;中文没有复数,英文有复数;部分文案在不同地区表达方式不同。国际化不是最后套一层翻译,它会反过来要求组件布局、文案长度和信息结构更稳健。

总结

滴滴出行小程序英文版改造的核心经验,不是“找一个 i18n 库”,而是把国际化当成工程系统来做。

文案要有资产管理,语言包要进入构建,模板和 JS 要使用统一 API,跨平台差异要被框架吸收,测试要覆盖真实业务路径。

对大型小程序来说,国际化的难点从来不是翻译函数本身,而是规模化协作和运行时约束。只要把这两件事处理好,多语言支持就不会变成后续迭代里的负担。