311 lines
10 KiB
Markdown
311 lines
10 KiB
Markdown
# 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',
|
||
```
|
||
|
||
### 模式 3:Script 中的中文文本
|
||
|
||
```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 种语言)
|