解决 uni-app App 端 vue-i18n 占位符丢失:封装跨端可用的 tf 格式化方法

背景

在一个 uni-app 多端项目中,某些列表和详情模块需要展示接口返回的数量、时间、状态等信息。

接口返回的数据本身是正常的,例如:

js 复制代码
{
  title: '任务名称',
  totalCount: 1,
  pointCount: 1,
  startTime: '2026-06-30 10:59:00',
  endTime: '2026-06-30 10:59:48',
  type: 1
}

但页面显示出来却变成了:

复制代码
总用时:
张地图
个点位
开始时间:
结束时间:

数字和时间都丢失了。

最初的代码

项目里原来有这样的格式化方法:

scss 复制代码
const formatCountText = (localeKey, count) => t(localeKey).replace('{count}', count)
const formatTimeText = (localeKey, time) => t(localeKey).replace('{time}', time)

对应语言包:

css 复制代码
{
  totalTime: '总用时:{time}',
  durationMinutes: '{count}分钟',
  mapCount: '{count}张地图',
  markerCount: '{count}个点位',
  startTime: '开始时间:{time}',
  endTime: '结束时间:{time}',
}

表面看没有问题,但在 uni-app 多端环境下会出现兼容问题。

根因

问题根因是:

scss 复制代码
t(localeKey)

返回时,{count}{time} 这类占位符已经可能被 vue-i18n 处理掉了。

代码期待的是:

scss 复制代码
t('module.mapCount')
// "{count}张地图"

但实际可能拿到的是:

arduino 复制代码
"张地图"

这时候再执行:

arduino 复制代码
.replace('{count}', count)

已经没有 {count} 可以替换了,所以最终显示就是:

复制代码
张地图

时间同理,会变成:

复制代码
开始时间:

为什么不直接用 t(key, params)

在普通 Vue 项目里,可以这样写:

php 复制代码
t('module.mapCount', { count: 1 })

但在 uni-app 的 App、小程序等非 H5 端,vue-i18n 的参数插值可能存在兼容问题,导致 {count} 不被正常替换。

所以不能简单依赖:

scss 复制代码
t(localeKey, { count })

需要封装一个跨端稳定的方法。

封装目标

希望组件里统一使用:

scss 复制代码
tf(localeKey, data)

例如:

php 复制代码
tf('module.mapCount', { count: 1 })
// "1张地图"

tf('module.startTime', { time: '2026-06-30 10:59:00' })
// "开始时间:2026-06-30 10:59:00"

核心封装

先封装一个获取翻译文本的方法:

vbnet 复制代码
export const translate = (key: string): string => {
  return i18n.global.t(key) as string
}

再封装手动替换占位符的方法:

typescript 复制代码
export function formatI18n(template: string, data: Record<string, any>): string {
  if (!template || !data) {
    return template
  }

  const match = /{(.*?)}/g.exec(template)

  if (match) {
    const variableList = match[0].replace('{', '').replace('}', '').split('.')
    let result: any = data

    for (let i = 0; i < variableList.length; i++) {
      result = result?.[variableList[i]] ?? ''
    }

    return formatI18n(template.replace(match[0], String(result)), data)
  }

  return template
}

这个方法支持普通占位符:

arduino 复制代码
'{count}个点位'

也支持嵌套属性:

arduino 复制代码
'身高 {detail.height}'

封装 translateFormat

vbnet 复制代码
export function translateFormat(key: string, data?: Record<string, any>): string {
  if (!data) {
    return translate(key)
  }

  if (isH5) {
    return i18n.global.t(key, data) as string
  }

  const template = translate(key)
  return formatI18n(template, data)
}

这里的策略是:

  • H5 端:继续使用 vue-i18n 原生插值
  • 非 H5 端:先取模板字符串,再手动替换占位符

这样可以兼顾 H5 和 App/小程序。

封装 Composable

为了在组件里使用方便,可以再包一层 useI18nFormat

javascript 复制代码
import { translate, formatI18n, translateFormat } from '@/locales'

export const useI18nFormat = () => {
  return {
    translate,
    formatI18n,
    tf: translateFormat,
    translateFormat,
  }
}

组件里使用:

javascript 复制代码
import { useI18nFormat } from '@/hooks/useI18nFormat'

const { tf } = useI18nFormat()

组件中的改法

原来:

scss 复制代码
const formatCountText = (localeKey, count) => t(localeKey).replace('{count}', count)
const formatTimeText = (localeKey, time) => t(localeKey).replace('{time}', time)

改成:

scss 复制代码
const formatCountText = (localeKey, count) => tf(localeKey, { count })
const formatTimeText = (localeKey, time) => tf(localeKey, { time })

业务映射时:

css 复制代码
countText: formatCountText('module.countText', item.count),
timeText: formatTimeText('module.startTime', item.startTime),

模板里:

scss 复制代码
<text>{{ item.countText }}</text>
<text>{{ formatTimeText('module.endTime', item.endTime) }}</text>

排查经验

这类问题容易误判成接口字段问题。

排查时可以按这个顺序:

  1. 先看接口返回,确认字段是否存在。
  2. 再打印映射后的列表数据。
  3. 如果映射后已经变成 "张地图""分钟",说明问题在 i18n 格式化。
  4. 如果映射后是 "1张地图",但页面不显示,才继续查模板或样式。
  5. 如果接口有值但映射后是空,才查字段名或响应层级。

这次就是通过打印映射后的数据,确认了接口数据已经进入前端,但 {count}{time} 在格式化阶段被处理掉了。

总结

不要在 uni-app 多端项目里依赖这种写法:

scss 复制代码
t(key).replace('{count}', count)

也不要无脑改成:

scss 复制代码
t(key, { count })

更稳妥的方式是封装统一的跨端格式化方法:

scss 复制代码
tf(key, params)

让组件只关心业务语义,不关心当前运行环境是 H5、App 还是小程序。

最终代码会更稳定,也更容易统一维护。

相关推荐
systemPro1 小时前
光储充系统数据流全解析:PV / ESS / GRID 数据怎么流转,线损怎么算
前端
古茗前端团队3 小时前
急招!前端|测试|后端|产品(名额多,速来)
前端·后端·架构
Lsx_3 小时前
不只是 Prompt:用 Superpowers Skill 给 AI 编程装上工程化工作流
前端·ai编程·claude
小碗细面4 小时前
前端 Prompt 工程实战:如何搭建场景化 Prompt 库
前端·ai编程
阿瑞IT4 小时前
2026年 AI Agent 生产化落地全景:四大高频故障根因分析与工程解法
前端
木木剑光4 小时前
我开源了一个 React 组件库,沉淀了多个高频组件和实用 Hooks
前端·javascript·react.js
kyriewen4 小时前
DeepSeek API 高峰时段涨价 2 倍,便宜大碗的时代要结束了?
前端·ai编程·deepseek
Moment4 小时前
牛逼,NextJs 从 16.3 开始全面拥抱 Agent Native 🥰🥰🥰
前端·后端·面试
沸点小助手5 小时前
6月沸点活动获奖名单公示|本周互动话题上新🎊
前端·后端