npm package: @gulibs/vite-plugin-i18n
Description: A Vite plugin for automatic i18n locales generation and management
Author: gulibs
Latest version: 0.0.10
Deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

# @gulibs/vite-plugin-i18n

一个智能的 Vite 插件，用于自动生成国际化资源和类型安全的翻译键文件。

> 🎉 **Version 0.0.10** - 新增了对 TypeScript 翻译文件的完整支持，提供更好的开发体验！

## ✨ 核心特性

- 🔍 **自动文件扫描** - 递归扫描翻译文件，支持嵌套目录结构
- 📦 **虚拟模块生成** - 零运行时开销，编译时生成优化的资源对象
- 🎯 **类型安全的 Keys** - 自动生成翻译键文件，提供完整的 TypeScript 支持
- 🔥 **智能热更新** - 文件变化时自动重新生成，支持增量更新
- 🎨 **优雅的调试输出** - 带时间戳的彩色日志，便于开发调试
- 🌍 **灵活的目录结构** - 支持多种组织方式：目录分离、文件分离
- ⚡ **TypeScript 支持** - 原生支持 TypeScript 翻译文件，自动编译和热更新

## 📦 安装

```bash
npm install @gulibs/vite-plugin-i18n --save-dev
# 或
pnpm add @gulibs/vite-plugin-i18n -D
# 或
yarn add @gulibs/vite-plugin-i18n -D
```

**TypeScript 支持（可选）**：

如果您需要使用 TypeScript 翻译文件，请安装 TypeScript：

```bash
npm install typescript --save-dev
# 或
pnpm add typescript -D
# 或
yarn add typescript -D
```

## 🚀 快速开始

### 1. 配置插件

```typescript
// vite.config.ts
import { defineConfig } from 'vite';
import i18nPlugin from '@gulibs/vite-plugin-i18n';

export default defineConfig({
  plugins: [
    i18nPlugin({
      basePath: 'src/locales',           // 翻译文件根目录
      keysOutput: 'src/i18n-keys.ts',   // 生成类型安全的键文件
      debug: true                        // 开启调试模式查看详细日志
    })
  ]
});
```

### 2. 创建翻译文件

支持 **JSON**、**JavaScript** 和 **TypeScript** 格式：

```
src/locales/
├── zh/                     # 中文翻译
│   ├── common.json        # 静态翻译文案
│   ├── auth.ts           # TypeScript 动态翻译
│   ├── messages.js       # JavaScript 动态翻译
│   └── modules/          # 模块化组织
│       ├── user.json
│       └── product.ts
└── en/                    # 英文翻译
    ├── common.json
    ├── auth.ts
    └── modules/
        ├── user.json
        └── product.ts
```

**示例翻译文件：**

```json
// src/locales/zh/common.json
{
  "welcome": "欢迎",
  "buttons": {
    "save": "保存",
    "cancel": "取消"
  }
}
```

```typescript
// src/locales/zh/auth.ts
export default {
  login: {
    title: "用户登录",
    username: "用户名",
    password: "密码",
    // 支持动态计算
    currentYear: `${new Date().getFullYear()} 年`,
    // 支持条件判断
    mode: process.env.NODE_ENV === 'development' ? '开发模式' : '生产模式'
  },
  register: {
    title: "用户注册"
  }
} as const; // 使用 as const 获得更好的类型推断
```

```javascript
// src/locales/zh/messages.js
const currentHour = new Date().getHours();

export default {
  greeting: currentHour < 12 ? '早上好' : currentHour < 18 ? '下午好' : '晚上好',
  notifications: {
    count: (num) => `您有 ${num} 条新消息`
  }
};
```

### 3. 使用虚拟模块

```typescript
// 导入自动生成的资源
import { resources, supportedLocales } from '@gulibs/i18n-locales';

// 输出所有翻译资源的结构化数据
console.log('所有语言资源:', resources);
// {
//   zh: {
//     common: { welcome: "欢迎", buttons: { save: "保存", cancel: "取消" } },
//     auth: { login: { title: "用户登录", username: "用户名", password: "密码", currentYear: "2024 年", mode: "生产模式" }, register: { title: "用户注册" } },
//     messages: { greeting: "下午好", notifications: { count: [Function] } },
//     modules: { user: {...}, product: {...} }
//   },
//   en: { ... }
// }

console.log('支持的语言:', supportedLocales);
// ['zh', 'en']
```

### 4. 使用类型安全的翻译键

```typescript
// 导入自动生成的翻译键
import { i18nKeys, type I18nKeys } from './src/i18n-keys';

// 类型安全的翻译函数
function t(key: I18nKeys, locale = 'zh'): string {
  const keys = key.split('.');
  let value = resources[locale];

  for (const k of keys) {
    value = value?.[k];
  }

  return value || key;
}

// ✅ 编译时类型检查通过
const welcomeText = t('common.welcome');
const saveButton = t('common.buttons.save');
const loginTitle = t('auth.login.title');
const greeting = t('messages.greeting');

// ❌ TypeScript 编译错误：不存在的键
// const invalid = t('nonexistent.key');
```

## 🛠️ TypeScript 翻译文件支持详解

### 编译机制

插件自动处理 TypeScript 翻译文件的编译过程：

1. **自动检测**：识别 `.ts` 扩展名的翻译文件
2. **智能编译**：
   - 优先使用项目的 TypeScript 编译器
   - 如果 TypeScript 不可用，使用内置的简化编译器
3. **临时编译**：编译后的代码写入临时 `.mjs` 文件
4. **动态导入**：导入编译后的模块获取翻译内容
5. **自动清理**：导入完成后自动删除临时文件

### 支持的 TypeScript 功能

```typescript
// src/locales/zh/advanced.ts

// ✅ 支持类型注解
interface User {
  name: string;
  age: number;
}

// ✅ 支持计算属性
const currentYear = new Date().getFullYear();

// ✅ 支持条件逻辑
const isDevelopment = process.env.NODE_ENV === 'development';

// ✅ 支持函数
function getTimeGreeting(): string {
  const hour = new Date().getHours();
  if (hour < 12) return '早上好';
  if (hour < 18) return '下午好';
  return '晚上好';
}

// ✅ 支持 as const 断言
export default {
  user: {
    welcome: (user: User) => `欢迎，${user.name}！`,
    profile: {
      age: `${currentYear} 年出生`,
      status: isDevelopment ? '开发用户' : '正式用户'
    }
  },
  greetings: {
    time: getTimeGreeting(),
    formal: '您好'
  }
} as const;
```

### 类型安全最佳实践

```typescript
// src/locales/zh/typed-messages.ts

// 定义消息类型
export interface MessageTemplates {
  user: {
    welcome: (name: string) => string;
    count: (count: number) => string;
  };
  system: {
    loading: string;
    error: (code: number) => string;
  };
}

// 实现类型安全的消息
const messages: MessageTemplates = {
  user: {
    welcome: (name: string) => `欢迎回来，${name}！`,
    count: (count: number) => `共 ${count} 项`
  },
  system: {
    loading: '加载中...',
    error: (code: number) => `错误代码：${code}`
  }
};

export default messages;
```

### 热更新支持

TypeScript 翻译文件享受完整的热更新支持：

- **文件修改**：保存 `.ts` 文件后自动重新编译和加载
- **类型检查**：实时的 TypeScript 类型错误检测
- **增量更新**：只重新编译修改的文件
- **缓存管理**：智能的编译缓存，避免重复编译

### 性能优化

```typescript
// vite.config.ts - 针对 TypeScript 翻译文件的性能优化

export default defineConfig({
  plugins: [
    i18nPlugin({
      basePath: 'src/locales',
      // 如果只使用 TypeScript 文件，可以限制扩展名
      extensions: ['.ts'],
      // 排除大型文件提升扫描速度
      exclude: ['**/node_modules/**', '**/dist/**', '**/*.d.ts'],
      debug: true
    })
  ]
});
```

## 🛠️ 实现原理详解

### 核心架构

插件的核心由三个主要组件构成：

#### 1. **LocaleParser** (`src/parser.ts`)

**作用**：负责扫描和解析翻译文件
**实现位置**：`src/parser.ts:394-567`

```typescript
class LocaleParser {
  // 递归扫描翻译文件
  async scanLocaleFiles(localesPath: string): Promise<string[]>

  // 解析单个翻译文件（支持 JSON/TS/JS）
  async parseLocaleFile(filePath: string, basePath: string): Promise<any>

  // 提取语言代码和命名空间
  createFileInfo(filePath: string, basePath: string): FileInfo
}
```

**核心功能**：
- 使用 `fast-glob` 高效扫描文件
- 支持动态导入 TypeScript/JavaScript 文件
- 智能提取语言代码（目录名或文件名）
- 自动生成命名空间（基于目录结构）

#### 2. **I18nContext** (`src/context.ts`)

**作用**：管理插件状态、缓存和代码生成
**实现位置**：`src/context.ts:15-367`

```typescript
class I18nContext {
  // 生成虚拟模块代码
  async generateLocales(): Promise<string>

  // 生成独立的翻译键文件
  private async generateKeysFile(resources: LocaleResourceMap): Promise<void>

  // 智能缓存管理
  private invalidateCache(): void

  // 优雅的调试日志
  private logDebug(message: string): void
  private logSuccess(message: string): void
  private logError(message: string): void
}
```

**核心功能**：
- **智能缓存系统**：避免重复解析，提升性能
- **虚拟模块生成**：将翻译资源编译为优化的 JavaScript 代码
- **类型安全的 Keys 生成**：自动分析所有翻译键，生成联合类型
- **热更新支持**：监听文件变化，增量更新

#### 3. **Plugin Factory** (`src/plugin.ts`)

**作用**：Vite 插件接口实现
**实现位置**：`src/context.ts:875-904`

```typescript
function createI18nPlugin(options = {}) {
  const context = new I18nContext(options);

  return [{
    name: '@gulibs/vite-plugin-i18n',
    configureServer(server) {
      // 设置开发服务器和文件监听
      context.setupServer(server);
    },
    resolveId(id) {
      // 解析虚拟模块 ID
      if (MODULE_IDS.includes(id)) {
        return `${VIRTUAL_MODULE_ID}?id=${id}`;
      }
    },
    async load(id) {
      // 加载虚拟模块内容
      const { moduleId, pageId } = context.parseRequest(id);
      if (moduleId === VIRTUAL_MODULE_ID && pageId && MODULE_IDS.includes(pageId)) {
        return await context.generateLocales();
      }
    }
  }];
}
```

### 🎯 翻译键文件生成机制

#### 智能键收集算法

**实现位置**：`src/context.ts:244-280`

```typescript
// 递归收集所有翻译键
private collectAllResourceKeys(resources: LocaleResourceMap): string[] {
  const allKeys = new Set<string>();

  Object.values(resources).forEach(localeResources => {
    Object.entries(localeResources).forEach(([namespace, content]) => {
      if (namespace === 'default') {
        // 默认命名空间：common.welcome
        this.collectKeysFromObject(content, '', allKeys);
      } else {
        // 命名空间前缀：auth.login.title
        this.collectKeysFromObject(content, namespace, allKeys);
      }
    });
  });

  return Array.from(allKeys).sort();
}
```

#### 类型生成策略

**实现位置**：`src/context.ts:283-309`

根据文件扩展名智能选择输出格式：

- **`.ts` 文件**：生成 TypeScript 代码，包含 `as const` 断言和联合类型
- **`.js` 文件**：生成纯 JavaScript 代码，无类型信息
- **`undefined`**：在虚拟模块中包含键数组

```typescript
// TypeScript 输出示例
export const i18nKeys = ["auth.login", "common.welcome"] as const;
export type I18nKeys = 'auth.login' | 'common.welcome';

// JavaScript 输出示例
export const i18nKeys = ["auth.login", "common.welcome"];
```

### 🔥 热更新机制

#### 文件监听器

**实现位置**：`src/context.ts:75-95`

```typescript
private setupWatcher(watcher: any) {
  const localesPath = path.resolve(process.cwd(), this.options.basePath);

  // 监听文件添加
  watcher.on('add', (filePath: string) => {
    if (filePath.startsWith(localesPath)) {
      this.logDebug(`Added locale file: ${filePath}`);
      this.invalidateCache();
    }
  });

  // 监听文件修改
  watcher.on('change', (filePath: string) => {
    if (filePath.startsWith(localesPath)) {
      this.logDebug(`Changed locale file: ${filePath}`);
      this.invalidateCache();
    }
  });

  // 监听文件删除
  watcher.on('unlink', (filePath: string) => {
    if (filePath.startsWith(localesPath)) {
      this.logDebug(`Removed locale file: ${filePath}`);
      this.invalidateCache();
    }
  });
}
```

#### 缓存失效策略

**实现位置**：`src/context.ts:97-113`

```typescript
private invalidateCache() {
  // 清除内存缓存
  this.cache.clear();

  if (this.server) {
    // 通知 Vite 重新加载虚拟模块
    const module = this.server.moduleGraph.getModuleById(VIRTUAL_MODULE_ID);
    if (module) {
      this.server.reloadModule(module);
      this.logDebug('Hot reloaded locale resources');
    }

    // 重新生成独立的 keys 文件
    if (this.options.keysOutput) {
      this.regenerateKeysFile();
    }
  }
}
```

### 🎨 调试日志系统

#### 优雅的日志输出

**实现位置**：`src/context.ts:46-64`

参考了 `plugin-react-autoroutes` 的设计，提供统一的日志格式：

```typescript
private logDebug(message: string) {
  if (this.options.debug) {
    console.log(
      chalk.gray(this.getTimestamp()),
      chalk.blueBright.bold(`▸ ${this.getPrefix()}`),
      chalk.gray(message)
    );
  }
}

private logSuccess(message: string) {
  if (this.options.debug) {
    console.log(
      chalk.gray(this.getTimestamp()),
      chalk.blueBright.bold(`▸ ${this.getPrefix()}`),
      chalk.green(`✓ ${message}`)
    );
  }
}

private logError(message: string) {
  console.error(
    chalk.gray(this.getTimestamp()),
    chalk.blueBright.bold(`▸ ${this.getPrefix()}`),
    chalk.red(`✗ ${message}`)
  );
}
```

**输出示例**：

```
14:25:30 ▸ [vite-plugin-i18n] 🔍 Scanning locale files...
14:25:30 ▸ [vite-plugin-i18n] Added locale file: /path/to/zh/common.json
14:25:30 ▸ [vite-plugin-i18n] ✓ Generated 2 locales with 8 files
14:25:30 ▸ [vite-plugin-i18n] ✓ Generated keys file: src/i18n-keys.ts (25 keys)
```

## ⚙️ 配置选项

| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `basePath` | `string` | `'src/locales'` | 翻译文件根目录路径 |
| `extensions` | `string[]` | `['.json', '.ts', '.js']` | 支持的文件扩展名 |
| `localePattern` | `'directory' \| 'filename'` | `'directory'` | 语言代码提取模式 |
| `keysOutput` | `string` | `undefined` | 翻译键文件输出路径（`.ts`/`.js`） |
| `hmr` | `boolean` | `true` | 是否启用热模块替换 |
| `deep` | `boolean` | `true` | 是否启用深度目录扫描 |
| `exclude` | `string[]` | `[]` | 排除的文件 / 目录模式 |
| `include` | `string[]` | `[]` | 包含的文件 / 目录模式 |
| `defaultLocale` | `string` | `'en'` | 默认语言代码 |
| `debug` | `boolean` | `false` | 是否启用调试模式 |

## 🎯 高级用法

### 1. 命名空间组织

```
src/locales/
├── zh/
│   ├── default.json        # 默认命名空间（键名：welcome）
│   ├── auth.json          # auth 命名空间（键名：auth.login）
│   └── modules/
│       ├── user.json      # modules.user 命名空间
│       └── admin/
│           └── panel.json # modules.admin.panel 命名空间
```

### 2. 动态翻译文件

```typescript
// src/locales/zh/dynamic.ts
const currentYear = new Date().getFullYear();

export default {
  copyright: `© ${currentYear} 公司版权所有`,
  dynamicMessage: process.env.NODE_ENV === 'development' ? '开发模式' : '生产模式'
};
```

### 3. 类型安全的翻译 Hook

```typescript
import { type I18nKeys } from './src/i18n-keys';
import { resources } from '@gulibs/i18n-locales';

export function useTranslation(locale: string = 'zh') {
  const t = useCallback((key: I18nKeys, params?: Record<string, any>): string => {
    const keys = key.split('.');
    let value = resources[locale];

    for (const k of keys) {
      value = value?.[k];
    }

    if (typeof value !== 'string') {
      console.warn(`Translation key "${key}" not found or not a string`);
      return key;
    }

    // 简单的参数替换
    if (params) {
      return value.replace(/\{\{(\w+)\}\}/g, (match, paramKey) => {
        return params[paramKey] || match;
      });
    }

    return value;
  }, [locale]);

  return { t };
}

// 使用示例
function MyComponent() {
  const { t } = useTranslation('zh');

  return (
    <div>
      <h1>{t('common.welcome')}</h1>
      <button>{t('common.buttons.save')}</button>
      <p>{t('auth.login.title')}</p>
    </div>
  );
}
```

### 4. 开发时的最佳实践

#### 自动检查翻译完整性

```typescript
// scripts/check-translations.ts
import { i18nKeys } from '../src/i18n-keys';
import { resources, supportedLocales } from '@gulibs/i18n-locales';

function checkTranslationCompleteness() {
  const results: Record<string, string[]> = {};

  supportedLocales.forEach(locale => {
    const missingKeys: string[] = [];

    i18nKeys.forEach(key => {
      const keys = key.split('.');
      let value = resources[locale];

      for (const k of keys) {
        value = value?.[k];
      }

      if (value === undefined) {
        missingKeys.push(key);
      }
    });

    if (missingKeys.length > 0) {
      results[locale] = missingKeys;
    }
  });

  if (Object.keys(results).length > 0) {
    console.error('❌ 发现缺失的翻译：', results);
    process.exit(1);
  } else {
    console.log('✅ 所有翻译完整');
  }
}

checkTranslationCompleteness();
```

#### ESLint 规则强制使用类型安全键

```json
// .eslintrc.json
{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "CallExpression[callee.name='t'] Literal[raw=/^['\"][^'\"]*['\"]$/]",
        "message": "请使用 I18nKeys 类型的常量，而不是字符串字面量"
      }
    ]
  }
}
```

## 🔧 故障排除

### 常见问题及解决方案

#### 1. 虚拟模块导入失败

**错误信息**：

```
Failed to resolve import "@gulibs/i18n-locales"
```

**解决方案**：
- 检查 `vite.config.ts` 中插件是否正确配置
- 确认虚拟模块 ID 正确：`@gulibs/i18n-locales`
- 重启开发服务器

#### 2. 翻译键文件未生成

**检查步骤**：

```bash
# 1. 启用调试模式查看详细日志
# vite.config.ts: debug: true

# 2. 检查输出目录权限
ls -la src/

# 3. 手动触发构建
npm run build
```

**常见原因**：
- `keysOutput` 路径配置错误
- 输出目录不存在或无写入权限
- 翻译文件格式错误导致解析失败

#### 3. TypeScript 类型错误

**错误信息**：

```
Type '"invalid.key"' is not assignable to parameter of type 'TranslationKey'
```

**解决方案**：
- 检查翻译键是否存在于生成的文件中
- 重新生成翻译键文件：删除后重启开发服务器
- 确认文件编码为 UTF-8

#### 4. 热更新不工作

**检查步骤**：
- 确认 `hmr: true`（默认开启）
- 检查文件监听权限（macOS 可能需要授权）
- 查看开发服务器控制台是否有错误信息

#### 5. 性能问题

如果翻译文件过多导致构建缓慢：

```typescript
// 优化配置
i18nPlugin({
  exclude: ['**/node_modules/**', '**/dist/**'],  // 排除无关目录
  extensions: ['.json'],                           // 仅扫描 JSON 文件
  deep: false                                     // 禁用深度扫描
})
```

## 📝 更新日志

### v0.0.7 (2024-03-XX)

#### 🚀 新特性

- 优化了日志系统，提供更清晰的调试信息
- 改进了类型生成机制，生成的键名更加语义化
- 增强了错误处理和提示信息

#### 🔧 改进

- 翻译键导出名称从 `translationKeys` 更改为 `i18nKeys`
- 翻译键类型从 `TranslationKey` 更改为 `I18nKeys`
- 日志格式统一，采用 `▸` 符号作为前缀
- 优化了虚拟模块生成逻辑

#### 🐛 修复

- 修复了在某些情况下热更新失效的问题
- 修复了嵌套命名空间解析错误
- 改进了 TypeScript 类型检查

#### ⚠️ 破坏性变更

- 如果你使用了生成的 keys 文件，请将 `translationKeys` 和 `TranslationKey` 更新为 `i18nKeys` 和 `I18nKeys`

#### 🔄 迁移指南

从 v0.0.6 升级到 v0.0.7：

```typescript
// 旧版本 (v0.0.6)
import { translationKeys, type TranslationKey } from './src/i18n-keys';

function t(key: TranslationKey): string {
  // ...
}

// 新版本 (v0.0.7)
import { i18nKeys, type I18nKeys } from './src/i18n-keys';

function t(key: I18nKeys): string {
  // ...
}
```

## 📄 许可证

MIT License - 详见 [LICENSE](./LICENSE) 文件

---

> 💡 **提示**：如果遇到问题，请启用 `debug: true` 查看详细的执行日志，这有助于快速定位问题所在。