549 lines
11 KiB
Markdown
549 lines
11 KiB
Markdown
|
|
# 国际化实现指南
|
|||
|
|
|
|||
|
|
> 本文档详细描述项目的国际化(i18n)实现规范,包括翻译键定义、组件实现、语言切换等。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 目录
|
|||
|
|
|
|||
|
|
1. [国际化的重要性](#1-国际化的重要性)
|
|||
|
|
2. [项目国际化架构](#2-项目国际化架构)
|
|||
|
|
3. [实现步骤](#3-实现步骤)
|
|||
|
|
4. [组件中的国际化实现](#4-组件中的国际化实现)
|
|||
|
|
5. [注意事项](#5-注意事项)
|
|||
|
|
6. [最佳实践](#6-最佳实践)
|
|||
|
|
7. [添加新语言](#7-添加新语言)
|
|||
|
|
8. [检查清单](#8-检查清单)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 国际化的重要性
|
|||
|
|
|
|||
|
|
**所有用户可见的文本都必须支持国际化,这是强制要求。**
|
|||
|
|
|
|||
|
|
- 项目支持多语言(目前:中文 `zh-CN`、英文 `en-US`)
|
|||
|
|
- 所有 UI 文本必须通过翻译函数获取
|
|||
|
|
- **严禁在代码中硬编码任何语言的文本**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 项目国际化架构
|
|||
|
|
|
|||
|
|
### 2.1 相关文件
|
|||
|
|
|
|||
|
|
| 文件路径 | 作用 |
|
|||
|
|
|---------|------|
|
|||
|
|
| `src/locales/types.ts` | 翻译键类型定义 |
|
|||
|
|
| `src/locales/zh-CN.ts` | 中文翻译字典 |
|
|||
|
|
| `src/locales/en-US.ts` | 英文翻译字典 |
|
|||
|
|
| `src/services/locale.ts` | 语言管理服务(单例) |
|
|||
|
|
|
|||
|
|
### 2.2 核心 API
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 导入语言服务
|
|||
|
|
import { t, localeManager } from '../../services/locale';
|
|||
|
|
|
|||
|
|
// 获取翻译文本
|
|||
|
|
const text = t('toolbar.home'); // 返回 "首页" 或 "Home"
|
|||
|
|
|
|||
|
|
// 订阅语言变更
|
|||
|
|
const unsubscribe = localeManager.subscribe(() => {
|
|||
|
|
// 语言变更时的回调
|
|||
|
|
this.setLocales();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 取消订阅(在组件销毁时调用)
|
|||
|
|
unsubscribe();
|
|||
|
|
|
|||
|
|
// 切换语言
|
|||
|
|
localeManager.setLocale('en-US');
|
|||
|
|
|
|||
|
|
// 获取当前语言
|
|||
|
|
const currentLocale = localeManager.getLocale();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 实现步骤
|
|||
|
|
|
|||
|
|
### 步骤 1:在类型文件中定义翻译键
|
|||
|
|
|
|||
|
|
**文件:`src/locales/types.ts`**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export interface TranslationDictionary {
|
|||
|
|
// ... 现有键
|
|||
|
|
myComponent: {
|
|||
|
|
title: string;
|
|||
|
|
description: string;
|
|||
|
|
buttons: {
|
|||
|
|
save: string;
|
|||
|
|
cancel: string;
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 2:添加中文翻译
|
|||
|
|
|
|||
|
|
**文件:`src/locales/zh-CN.ts`**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export const zhCN: TranslationDictionary = {
|
|||
|
|
// ... 现有内容
|
|||
|
|
myComponent: {
|
|||
|
|
title: '我的组件',
|
|||
|
|
description: '这是组件描述',
|
|||
|
|
buttons: {
|
|||
|
|
save: '保存',
|
|||
|
|
cancel: '取消',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 3:添加英文翻译
|
|||
|
|
|
|||
|
|
**文件:`src/locales/en-US.ts`**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export const enUS: TranslationDictionary = {
|
|||
|
|
// ... 现有内容
|
|||
|
|
myComponent: {
|
|||
|
|
title: 'My Component',
|
|||
|
|
description: 'This is component description',
|
|||
|
|
buttons: {
|
|||
|
|
save: 'Save',
|
|||
|
|
cancel: 'Cancel',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 4:在代码中使用翻译函数
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { t } from '../../services/locale';
|
|||
|
|
|
|||
|
|
// 获取翻译文本
|
|||
|
|
const title = t('myComponent.title');
|
|||
|
|
|
|||
|
|
// 在 DOM 中使用
|
|||
|
|
const titleEl = document.createElement('div');
|
|||
|
|
titleEl.textContent = t('myComponent.title');
|
|||
|
|
|
|||
|
|
// 在 HTML 字符串中使用
|
|||
|
|
const html = `<div>${t('myComponent.description')}</div>`;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 组件中的国际化实现
|
|||
|
|
|
|||
|
|
### 4.1 完整实现示例
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { t, localeManager } from '../../services/locale';
|
|||
|
|
import { IBimComponent } from '../../types/component';
|
|||
|
|
import { ThemeConfig } from '../../themes/types';
|
|||
|
|
|
|||
|
|
export class MyComponent implements IBimComponent {
|
|||
|
|
private element: HTMLElement;
|
|||
|
|
private unsubscribeLocale: (() => void) | null = null;
|
|||
|
|
private unsubscribeTheme: (() => void) | null = null;
|
|||
|
|
|
|||
|
|
constructor(container: HTMLElement) {
|
|||
|
|
this.element = this.createDOM();
|
|||
|
|
container.appendChild(this.element);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建 DOM 结构
|
|||
|
|
*/
|
|||
|
|
private createDOM(): HTMLElement {
|
|||
|
|
const root = document.createElement('div');
|
|||
|
|
root.className = 'my-component';
|
|||
|
|
root.innerHTML = `
|
|||
|
|
<div class="my-component-title"></div>
|
|||
|
|
<div class="my-component-content"></div>
|
|||
|
|
<div class="my-component-actions">
|
|||
|
|
<button class="btn-save"></button>
|
|||
|
|
<button class="btn-cancel"></button>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
return root;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 初始化组件
|
|||
|
|
*/
|
|||
|
|
public init(): void {
|
|||
|
|
// 订阅语言变更
|
|||
|
|
this.unsubscribeLocale = localeManager.subscribe(() => {
|
|||
|
|
this.setLocales();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 初始设置语言
|
|||
|
|
this.setLocales();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置/更新所有文本(语言变更时调用)
|
|||
|
|
*/
|
|||
|
|
public setLocales(): void {
|
|||
|
|
// 更新标题
|
|||
|
|
const titleEl = this.element.querySelector('.my-component-title');
|
|||
|
|
if (titleEl) {
|
|||
|
|
titleEl.textContent = t('myComponent.title');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新内容
|
|||
|
|
const contentEl = this.element.querySelector('.my-component-content');
|
|||
|
|
if (contentEl) {
|
|||
|
|
contentEl.textContent = t('myComponent.description');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新按钮
|
|||
|
|
const saveBtn = this.element.querySelector('.btn-save');
|
|||
|
|
if (saveBtn) {
|
|||
|
|
saveBtn.textContent = t('myComponent.buttons.save');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const cancelBtn = this.element.querySelector('.btn-cancel');
|
|||
|
|
if (cancelBtn) {
|
|||
|
|
cancelBtn.textContent = t('myComponent.buttons.cancel');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置主题
|
|||
|
|
*/
|
|||
|
|
public setTheme(theme: ThemeConfig): void {
|
|||
|
|
// 主题设置逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 销毁组件
|
|||
|
|
*/
|
|||
|
|
public destroy(): void {
|
|||
|
|
// 取消语言订阅
|
|||
|
|
if (this.unsubscribeLocale) {
|
|||
|
|
this.unsubscribeLocale();
|
|||
|
|
this.unsubscribeLocale = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 取消主题订阅
|
|||
|
|
if (this.unsubscribeTheme) {
|
|||
|
|
this.unsubscribeTheme();
|
|||
|
|
this.unsubscribeTheme = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除 DOM
|
|||
|
|
this.element.remove();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 关键点说明
|
|||
|
|
|
|||
|
|
1. **在 `init()` 中订阅语言变更**
|
|||
|
|
2. **实现 `setLocales()` 方法更新所有文本**
|
|||
|
|
3. **在 `destroy()` 中取消订阅**
|
|||
|
|
4. **使用 `t('key.path')` 获取翻译文本**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 注意事项
|
|||
|
|
|
|||
|
|
### 5.1 ✅ 必须做的
|
|||
|
|
|
|||
|
|
#### 所有用户可见文本使用 `t(key)`
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ 正确
|
|||
|
|
button.textContent = t('toolbar.home');
|
|||
|
|
|
|||
|
|
// ❌ 错误
|
|||
|
|
button.textContent = '首页';
|
|||
|
|
button.textContent = 'Home';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 翻译键使用有意义的路径
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ 正确:按功能模块组织
|
|||
|
|
t('toolbar.home')
|
|||
|
|
t('dialog.title')
|
|||
|
|
t('button.save')
|
|||
|
|
t('message.success')
|
|||
|
|
|
|||
|
|
// ❌ 错误:键名不清晰
|
|||
|
|
t('text1')
|
|||
|
|
t('label')
|
|||
|
|
t('str')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 在所有语言文件中添加翻译
|
|||
|
|
|
|||
|
|
- 添加新键时,必须同时更新 `zh-CN.ts` 和 `en-US.ts`
|
|||
|
|
- 确保所有语言文件的结构一致
|
|||
|
|
|
|||
|
|
#### 实现 `setLocales()` 方法
|
|||
|
|
|
|||
|
|
- 所有组件必须实现 `setLocales()` 方法
|
|||
|
|
- 在方法中更新所有用户可见的文本
|
|||
|
|
|
|||
|
|
#### 订阅语言变更
|
|||
|
|
|
|||
|
|
- 组件初始化时订阅 `localeManager.subscribe()`
|
|||
|
|
- 组件销毁时取消订阅
|
|||
|
|
|
|||
|
|
### 5.2 ❌ 禁止做的
|
|||
|
|
|
|||
|
|
#### 禁止硬编码文本
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ❌ 错误:硬编码中文
|
|||
|
|
const title = '首页';
|
|||
|
|
|
|||
|
|
// ❌ 错误:硬编码英文
|
|||
|
|
const title = 'Home';
|
|||
|
|
|
|||
|
|
// ✅ 正确:使用翻译函数
|
|||
|
|
const title = t('toolbar.home');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 禁止在翻译键中使用变量
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ❌ 错误:动态拼接键名(难以追踪和维护)
|
|||
|
|
t(`toolbar.${buttonId}`);
|
|||
|
|
|
|||
|
|
// ✅ 正确:使用完整的键名
|
|||
|
|
t('toolbar.home');
|
|||
|
|
t('toolbar.settings');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 禁止忽略语言变更
|
|||
|
|
|
|||
|
|
- 组件必须响应语言切换
|
|||
|
|
- 不能只在初始化时设置文本
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 最佳实践
|
|||
|
|
|
|||
|
|
### 6.1 翻译键的组织结构
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 按功能模块组织
|
|||
|
|
interface TranslationDictionary {
|
|||
|
|
// 工具栏相关
|
|||
|
|
toolbar: {
|
|||
|
|
home: string;
|
|||
|
|
settings: string;
|
|||
|
|
measure: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 弹窗相关
|
|||
|
|
dialog: {
|
|||
|
|
title: string;
|
|||
|
|
content: string;
|
|||
|
|
close: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 按钮相关
|
|||
|
|
button: {
|
|||
|
|
save: string;
|
|||
|
|
cancel: string;
|
|||
|
|
confirm: string;
|
|||
|
|
delete: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 消息相关
|
|||
|
|
message: {
|
|||
|
|
success: string;
|
|||
|
|
error: string;
|
|||
|
|
warning: string;
|
|||
|
|
loading: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 表单相关
|
|||
|
|
form: {
|
|||
|
|
required: string;
|
|||
|
|
invalid: string;
|
|||
|
|
placeholder: {
|
|||
|
|
name: string;
|
|||
|
|
email: string;
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 嵌套键的使用
|
|||
|
|
|
|||
|
|
对于复杂组件,可以使用嵌套结构:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 类型定义
|
|||
|
|
measurePanel: {
|
|||
|
|
modes: {
|
|||
|
|
distance: string;
|
|||
|
|
angle: string;
|
|||
|
|
area: string;
|
|||
|
|
};
|
|||
|
|
labels: {
|
|||
|
|
currentMode: string;
|
|||
|
|
result: string;
|
|||
|
|
};
|
|||
|
|
actions: {
|
|||
|
|
clear: string;
|
|||
|
|
settings: string;
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 使用
|
|||
|
|
t('measurePanel.modes.distance') // "距离"
|
|||
|
|
t('measurePanel.labels.result') // "结果"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 处理动态内容
|
|||
|
|
|
|||
|
|
如果需要在翻译中插入动态内容,建议:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 方案 1:翻译后拼接(简单场景)
|
|||
|
|
const message = t('file.selected') + `: ${fileName}`;
|
|||
|
|
|
|||
|
|
// 方案 2:使用模板(需要扩展 LocaleManager)
|
|||
|
|
// 翻译字典: "已选择 {count} 个文件"
|
|||
|
|
const message = t('file.selectedCount', { count: 5 });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 添加新语言
|
|||
|
|
|
|||
|
|
### 步骤 1:扩展语言类型
|
|||
|
|
|
|||
|
|
**文件:`src/locales/types.ts`**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export type LocaleType = 'zh-CN' | 'en-US' | 'ja-JP'; // 添加日语
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 2:创建翻译文件
|
|||
|
|
|
|||
|
|
**文件:`src/locales/ja-JP.ts`**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { TranslationDictionary } from './types';
|
|||
|
|
|
|||
|
|
export const jaJP: TranslationDictionary = {
|
|||
|
|
toolbar: {
|
|||
|
|
home: 'ホーム',
|
|||
|
|
settings: '設定',
|
|||
|
|
// ... 所有翻译
|
|||
|
|
},
|
|||
|
|
// ... 完整的翻译字典
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 3:注册新语言
|
|||
|
|
|
|||
|
|
**文件:`src/services/locale.ts`**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { jaJP } from '../locales/ja-JP';
|
|||
|
|
|
|||
|
|
class LocaleManager {
|
|||
|
|
private messages: Record<LocaleType, TranslationDictionary> = {
|
|||
|
|
'zh-CN': zhCN,
|
|||
|
|
'en-US': enUS,
|
|||
|
|
'ja-JP': jaJP, // 添加新语言
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 检查清单
|
|||
|
|
|
|||
|
|
在开发新功能时,确保完成以下检查:
|
|||
|
|
|
|||
|
|
### 8.1 翻译键
|
|||
|
|
|
|||
|
|
- [ ] 在 `types.ts` 中定义了新的翻译键类型
|
|||
|
|
- [ ] 在 `zh-CN.ts` 中添加了中文翻译
|
|||
|
|
- [ ] 在 `en-US.ts` 中添加了英文翻译
|
|||
|
|
- [ ] 翻译键名清晰、有意义
|
|||
|
|
- [ ] 翻译键结构与类型定义一致
|
|||
|
|
|
|||
|
|
### 8.2 组件实现
|
|||
|
|
|
|||
|
|
- [ ] 所有用户可见的文本都使用 `t(key)` 函数
|
|||
|
|
- [ ] 组件实现了 `setLocales()` 方法
|
|||
|
|
- [ ] 组件在 `init()` 中订阅了语言变更事件
|
|||
|
|
- [ ] 组件在 `destroy()` 中取消了语言订阅
|
|||
|
|
|
|||
|
|
### 8.3 代码质量
|
|||
|
|
|
|||
|
|
- [ ] 没有硬编码任何语言的文本
|
|||
|
|
- [ ] 没有使用动态拼接的翻译键
|
|||
|
|
- [ ] 翻译文本语法正确、表达清晰
|
|||
|
|
|
|||
|
|
### 8.4 测试验证
|
|||
|
|
|
|||
|
|
- [ ] 在中文环境下测试文本显示正确
|
|||
|
|
- [ ] 在英文环境下测试文本显示正确
|
|||
|
|
- [ ] 切换语言后所有文本正确更新
|
|||
|
|
- [ ] 没有遗漏的未翻译文本
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 附录:现有翻译键参考
|
|||
|
|
|
|||
|
|
### 工具栏 (toolbar)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
toolbar: {
|
|||
|
|
home: '首页' | 'Home',
|
|||
|
|
settings: '设置' | 'Settings',
|
|||
|
|
info: '信息' | 'Info',
|
|||
|
|
location: '定位' | 'Location',
|
|||
|
|
measure: '测量' | 'Measure',
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 弹窗 (dialog)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
dialog: {
|
|||
|
|
title: '弹窗标题' | 'Dialog Title',
|
|||
|
|
close: '关闭' | 'Close',
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 测量面板 (measure)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
measure: {
|
|||
|
|
modes: {
|
|||
|
|
distance: '距离' | 'Distance',
|
|||
|
|
angle: '角度' | 'Angle',
|
|||
|
|
// ...
|
|||
|
|
},
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档版本**:v1.0
|
|||
|
|
**最后更新**:2026-01-21
|