Files
everything-is-suitable/docs/superpowers/specs/2026-04-28-uniapp-i18n-design.md
T
张翔 a39b4388f3 chore: 添加文档、资源与测试输出
- assets/ 静态资源
- docs/superpowers/ 技能文档
- dogfood-output/ 测试验证输出
2026-04-29 21:17:10 +08:00

311 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UniApp 主应用国际化(i18n)设计规格
## 概述
`everything-is-suitable-uniapp` 引入 vue-i18n 国际化体系,支持 9 种语言,与落地页语言覆盖对齐。领域术语(天干地支、星曜名、宫位名等)保留中文原词,仅翻译 UI 文本。
## 决策记录
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 语言范围 | 9 种(zh-CN, zh-TW, en, ja, ko, de, fr, es, pt | 与落地页对齐 |
| 领域术语策略 | 保留中文原词 | 命理术语缺乏行业标准翻译,多数命理 App 的做法 |
| i18n 库 | vue-i18n | Vue 生态最成熟,功能完善 |
| 架构方案 | 集中式翻译文件(方案 A) | 项目规模适中,集中式便于审查覆盖率 |
| 语言切换 | 跟随系统语言,不提供 App 内切换 UI | 与落地页行为一致 |
## 目录结构
```
everything-is-suitable-uniapp/src/
├── locales/
│ ├── index.ts # vue-i18n 实例创建、语言检测/切换/持久化
│ ├── zh-CN.ts # 简体中文(主翻译源)
│ ├── zh-TW.ts # 繁體中文
│ ├── en.ts # English
│ ├── ja.ts # 日本語
│ ├── ko.ts # 한국어
│ ├── de.ts # Deutsch
│ ├── fr.ts # Français
│ ├── es.ts # Español
│ └── pt.ts # Português
├── main.ts # 挂载 i18n 实例
└── ...
```
## locales/index.ts 核心职责
1. 创建 vue-i18n 实例,注入所有 9 种语言的翻译
2. 语言检测优先级:`uni.getStorageSync('preferred-locale')``uni.getSystemInfoSync().language` → 默认 `zh-CN`
3. 暴露切换函数 `setLocale(locale)`,同步更新 `uni.setStorageSync` + vue-i18n 的 `locale`
4. 暴露当前语言 `getLocale()`
5. 回退机制:目标语言缺失某 key 时,回退到 `zh-CN`
## main.ts 改造
```ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import { i18n } from './locales'
export function createApp() {
const app = createSSRApp(App)
app.use(i18n)
return { app }
}
```
## 翻译键命名空间
按功能模块划分,扁平 key 结构(与落地页 i18n.js 一致):
| 命名空间 | 覆盖范围 | 示例 key |
|---------|---------|---------|
| `nav.*` | 底部导航栏 | `nav.almanac``nav.ziwei``nav.fortune` |
| `almanac.*` | 黄历搜索页及相关组件 | `almanac.pageTitle``almanac.searching` |
| `ziwei.*` | 紫微斗数页 | `ziwei.birthInfo``ziwei.generating` |
| `fortune.*` | 运势分析页 | `fortune.daily``fortune.monthly` |
| `search.*` | 搜索相关组件 | `search.conditions``search.addCondition` |
| `export.*` | 导出面板 | `export.title``export.format` |
| `history.*` | 搜索历史 | `history.title``history.clear` |
| `template.*` | 搜索模板 | `template.title``template.search` |
| `common.*` | 通用文本 | `common.retry``common.cancel` |
## 领域术语策略
保留中文原词,不纳入翻译体系:
- `enums.ts` 中的 `HeavenlyStem.name()``EarthlyBranch.name()``MajorStar.name()``PalaceType.name()` 等保持不变
- `SearchConditionItem` 中的黄历活动列表(`suitableActivities``avoidActivities`)保持中文硬编码
- `templateService.ts` 中的模板名/描述/分类保持中文
- 这些领域数据未来如需国际化,可作为独立增量迭代
## 组件改造模式
### 模式 1:模板中的静态文本
```html
<!-- 改造前 -->
<text class="section-title">出生信息</text>
<!-- 改造后 -->
<text class="section-title">{{ $t('ziwei.birthInfo') }}</text>
```
### 模式 2:动态拼接文本(插值)
```html
<!-- 改造前 -->
<Typography variant="caption">共{{ results.length }}条结果</Typography>
<!-- 改造后 -->
<Typography variant="caption">{{ $t('search.resultCount', { count: results.length }) }}</Typography>
```
翻译文件:
```ts
// zh-CN
'search.resultCount': '共{count}条结果',
// en
'search.resultCount': '{count} results',
```
### 模式 3Script 中的中文文本
```ts
// 改造前
error.value = '请至少添加一个搜索条件'
// 改造后
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
error.value = t('search.addConditionHint')
```
## pages.json 国际化
`navigationBarTitleText` 不支持 vue-i18n 动态翻译,改用运行时设置。`pages.json` 中保留中文初始值(作为首次渲染前的兜底),由 `onShow` 生命周期动态覆盖为当前语言文本:
```json
{
"pages": [
{
"path": "pages/almanac-search/index",
"style": { "navigationBarTitleText": "黄历搜索" }
}
]
}
```
各页面 `onShow` 生命周期中动态设置:
```ts
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
onShow(() => {
uni.setNavigationBarTitle({ title: t('almanac.pageTitle') })
})
```
## EmptyState 组件默认值处理
移除中文默认值,由调用方显式传入翻译文本:
```ts
// 改造前
withDefaults(defineProps<Props>(), {
title: '暂无数据',
description: '没有找到相关结果',
actionText: '重新搜索',
})
// 改造后
withDefaults(defineProps<Props>(), {
icon: 'empty',
showAction: true,
size: 'medium'
})
```
调用方:
```html
<EmptyState
:title="$t('common.noData')"
:description="$t('common.noResult')"
:actionText="$t('common.retry')"
:showAction="hasSearched"
@action="performSearch"
/>
```
## 翻译键全量清单(约 80 条)
```
common.retry = 重试
common.cancel = 取消
common.confirm = 确认
common.loading = 加载中...
common.noData = 暂无数据
common.noResult = 没有找到相关结果
common.search = 搜索
common.male = 男
common.female = 女
nav.almanac = 黄历
nav.ziwei = 紫微
nav.fortune = 运势
almanac.pageTitle = 黄历搜索
almanac.searching = 搜索中...
almanac.searchFailed = 搜索失败,请稍后重试
almanac.noResult = 没有找到符合条件的结果
almanac.keywordHint = 请输入搜索关键词
ziwei.pageTitle = 紫微斗数
ziwei.birthInfo = 出生信息
ziwei.birthDate = 出生日期
ziwei.birthHour = 出生时辰
ziwei.gender = 性别
ziwei.pleaseSelect = 请选择
ziwei.generating = 排盘中...
ziwei.generate = 排盘
ziwei.chartTitle = 紫微斗数盘
ziwei.sanFangAnalysis = 三方四正分析
ziwei.sanFangScore = 三方得分
ziwei.siZhengScore = 四正得分
ziwei.overallEval = 综合评价
ziwei.summary = 盘面概述
ziwei.scoreUnit = 分
fortune.pageTitle = 运势分析
fortune.daily = 日运
fortune.monthly = 月运
fortune.selectDate = 选择日期
fortune.selectMonth = 选择月份
fortune.overallFortune = 综合运势
fortune.career = 事业
fortune.wealth = 财运
fortune.relationship = 感情
fortune.health = 健康
fortune.luckyColor = 幸运色
fortune.luckyNumber = 幸运数字
fortune.luckyDirection = 幸运方位
fortune.noChart = 请先在紫微斗数页面完成排盘
search.conditions = 搜索条件
search.addCondition = 添加条件
search.addConditionHint = 请至少添加一个搜索条件
search.addConditionEmpty = 请添加搜索条件
search.days = 搜索天数
search.dayUnit = 天
search.searchResult = 搜索结果
search.resultCount = 共{count}条结果
search.matchCount = 匹配度: {count}
search.sort = 排序
search.sortByDate = 日期
search.sortByMatch = 匹配度
search.typeSuitable = 宜
search.typeUnsuitable = 忌
search.operatorAnd = 与(AND
search.operatorOr = 或(OR
search.excludeCondition = 排除条件
search.items = 事项
search.operator = 逻辑运算符
template.title = 搜索模板
template.searchPlaceholder= 搜索模板
template.empty = 暂无模板
template.emptyDesc = 没有找到符合条件的模板
template.all = 全部
export.title = 导出搜索结果
export.selectFormat = 选择导出格式
export.selectContent = 选择导出内容
export.date = 日期
export.lunarDate = 农历日期
export.weekday = 星期
export.matchedItems = 匹配事项
export.matchCount = 匹配度
history.title = 搜索历史
history.clear = 清除历史
history.clearConfirm = 确认清除
history.clearConfirmMsg = 确定要清除所有搜索历史吗?
history.empty = 暂无搜索历史
history.emptyDesc = 执行搜索后,搜索条件将显示在这里
history.justNow = 刚刚
history.hoursAgo = {count}小时前
history.daysAgo = {count}天前
```
## 测试策略
### 1. 单元测试 — 翻译完整性校验
新增 `locales/__tests__/completeness.test.ts`
- 校验所有 8 种非中文语言的翻译文件,key 集合必须与 `zh-CN` 相等
- 发现缺失 key 时测试失败,输出具体缺失的 key 列表
### 2. 单元测试 — 插值格式校验
- 校验所有使用插值(`{count}` 等)的 key,在每种语言中插值占位符名称一致
### 3. 组件测试 — 翻译渲染验证
对关键组件(BottomNavigation、SearchConditionPanel、EmptyState)编写测试:
- 切换 locale 后,验证渲染文本对应正确语言
- 验证插值参数正确替换
### 4. E2E 测试 — 语言切换端到端验证
- 在 H5 模式下模拟不同 `navigator.language`,验证页面标题和关键文本正确切换
- 验证 `preferred-locale` 持久化后重启仍生效
## 不在范围内
- 领域术语的国际化(天干地支、星曜名、宫位名、黄历活动名)
- templateService 中模板名/描述/分类的国际化
- App 内语言切换 UI
- 落地页 i18n.js 的改动(已独立支持 9 种语言)