Files
bim_engine/docs-old/国际化实现指南.md

11 KiB
Raw Permalink Blame History

国际化实现指南

本文档详细描述项目的国际化i18n实现规范包括翻译键定义、组件实现、语言切换等。


📋 目录

  1. 国际化的重要性
  2. 项目国际化架构
  3. 实现步骤
  4. 组件中的国际化实现
  5. 注意事项
  6. 最佳实践
  7. 添加新语言
  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

// 导入语言服务
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

export interface TranslationDictionary {
    // ... 现有键
    myComponent: {
        title: string;
        description: string;
        buttons: {
            save: string;
            cancel: string;
        };
    };
}

步骤 2添加中文翻译

文件:src/locales/zh-CN.ts

export const zhCN: TranslationDictionary = {
    // ... 现有内容
    myComponent: {
        title: '我的组件',
        description: '这是组件描述',
        buttons: {
            save: '保存',
            cancel: '取消',
        },
    },
};

步骤 3添加英文翻译

文件:src/locales/en-US.ts

export const enUS: TranslationDictionary = {
    // ... 现有内容
    myComponent: {
        title: 'My Component',
        description: 'This is component description',
        buttons: {
            save: 'Save',
            cancel: 'Cancel',
        },
    },
};

步骤 4在代码中使用翻译函数

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 完整实现示例

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)

// ✅ 正确
button.textContent = t('toolbar.home');

// ❌ 错误
button.textContent = '首页';
button.textContent = 'Home';

翻译键使用有意义的路径

// ✅ 正确:按功能模块组织
t('toolbar.home')
t('dialog.title')
t('button.save')
t('message.success')

// ❌ 错误:键名不清晰
t('text1')
t('label')
t('str')

在所有语言文件中添加翻译

  • 添加新键时,必须同时更新 zh-CN.tsen-US.ts
  • 确保所有语言文件的结构一致

实现 setLocales() 方法

  • 所有组件必须实现 setLocales() 方法
  • 在方法中更新所有用户可见的文本

订阅语言变更

  • 组件初始化时订阅 localeManager.subscribe()
  • 组件销毁时取消订阅

5.2 禁止做的

禁止硬编码文本

// ❌ 错误:硬编码中文
const title = '首页';

// ❌ 错误:硬编码英文
const title = 'Home';

// ✅ 正确:使用翻译函数
const title = t('toolbar.home');

禁止在翻译键中使用变量

// ❌ 错误:动态拼接键名(难以追踪和维护)
t(`toolbar.${buttonId}`);

// ✅ 正确:使用完整的键名
t('toolbar.home');
t('toolbar.settings');

禁止忽略语言变更

  • 组件必须响应语言切换
  • 不能只在初始化时设置文本

6. 最佳实践

6.1 翻译键的组织结构

// 按功能模块组织
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 嵌套键的使用

对于复杂组件,可以使用嵌套结构:

// 类型定义
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 处理动态内容

如果需要在翻译中插入动态内容,建议:

// 方案 1翻译后拼接简单场景
const message = t('file.selected') + `: ${fileName}`;

// 方案 2使用模板需要扩展 LocaleManager
// 翻译字典: "已选择 {count} 个文件"
const message = t('file.selectedCount', { count: 5 });

7. 添加新语言

步骤 1扩展语言类型

文件:src/locales/types.ts

export type LocaleType = 'zh-CN' | 'en-US' | 'ja-JP'; // 添加日语

步骤 2创建翻译文件

文件:src/locales/ja-JP.ts

import { TranslationDictionary } from './types';

export const jaJP: TranslationDictionary = {
    toolbar: {
        home: 'ホーム',
        settings: '設定',
        // ... 所有翻译
    },
    // ... 完整的翻译字典
};

步骤 3注册新语言

文件:src/services/locale.ts

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)

toolbar: {
    home: '首页' | 'Home',
    settings: '设置' | 'Settings',
    info: '信息' | 'Info',
    location: '定位' | 'Location',
    measure: '测量' | 'Measure',
    // ...
}

弹窗 (dialog)

dialog: {
    title: '弹窗标题' | 'Dialog Title',
    close: '关闭' | 'Close',
    // ...
}

测量面板 (measure)

measure: {
    modes: {
        distance: '距离' | 'Distance',
        angle: '角度' | 'Angle',
        // ...
    },
    // ...
}

文档版本v1.0 最后更新2026-01-21