@gulibs/vgrove-i18n
v0.0.3
Published
A Vite plugin for automatic i18n locales generation and management
Downloads
6
Maintainers
Readme
@gulibs/vgrove-i18n
一个智能的 Vite 插件,用于自动生成国际化资源和类型安全的翻译键文件。
🎉 Version 0.0.1 - 全新发布!基于文件系统的智能国际化解决方案,提供完整的 TypeScript 支持和优雅的开发体验!
✨ 核心特性
- 🔍 自动文件扫描 - 递归扫描翻译文件,支持嵌套目录结构
- 📦 虚拟模块生成 - 零运行时开销,编译时生成优化的资源对象
- 🎯 类型安全的 Keys - 自动生成翻译键文件,提供完整的 TypeScript 支持
- 🔥 智能热更新 - 文件变化时自动重新生成,支持增量更新
- 🎨 优雅的调试输出 - 带时间戳的彩色日志,便于开发调试
- 🌍 灵活的目录结构 - 支持多种组织方式:目录分离、文件分离
- ⚡ TypeScript 支持 - 原生支持 TypeScript 翻译文件,自动编译和热更新
📦 安装
npm install @gulibs/vgrove-i18n --save-dev
# 或
pnpm add @gulibs/vgrove-i18n -D
# 或
yarn add @gulibs/vgrove-i18n -DTypeScript 支持(可选):
如果您需要使用 TypeScript 翻译文件,请安装 TypeScript:
npm install typescript --save-dev
# 或
pnpm add typescript -D
# 或
yarn add typescript -D🚀 快速开始
1. 配置插件
// vite.config.ts
import { defineConfig } from 'vite';
import i18nPlugin from '@gulibs/vgrove-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示例翻译文件:
// src/locales/zh/common.json
{
"welcome": "欢迎",
"buttons": {
"save": "保存",
"cancel": "取消"
}
}// 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 获得更好的类型推断// src/locales/zh/messages.js
const currentHour = new Date().getHours();
export default {
greeting: currentHour < 12 ? '早上好' : currentHour < 18 ? '下午好' : '晚上好',
notifications: {
count: (num) => `您有 ${num} 条新消息`
}
};3. 使用虚拟模块
// 导入自动生成的资源
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. 使用类型安全的翻译键
// 导入自动生成的翻译键
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 翻译文件的编译过程:
- 自动检测:识别
.ts扩展名的翻译文件 - 智能编译:
- 优先使用项目的 TypeScript 编译器
- 如果 TypeScript 不可用,使用内置的简化编译器
- 临时编译:编译后的代码写入临时
.mjs文件 - 动态导入:导入编译后的模块获取翻译内容
- 自动清理:导入完成后自动删除临时文件
支持的 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;类型安全最佳实践
// 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 类型错误检测
- 增量更新:只重新编译修改的文件
- 缓存管理:智能的编译缓存,避免重复编译
性能优化
// 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
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
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
function createI18nPlugin(options = {}) {
const context = new I18nContext(options);
return [{
name: '@gulibs/vgrove-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
// 递归收集所有翻译键
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 输出示例
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
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
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
参考了 @gulibs/vgrove-autoroutes 的设计,提供统一的日志格式:
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 ▸ [vgrove-i18n] 🔍 Scanning locale files...
14:25:30 ▸ [vgrove-i18n] Added locale file: /path/to/zh/common.json
14:25:30 ▸ [vgrove-i18n] ✓ Generated 2 locales with 8 files
14:25:30 ▸ [vgrove-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. 动态翻译文件
// src/locales/zh/dynamic.ts
const currentYear = new Date().getFullYear();
export default {
copyright: `© ${currentYear} 公司版权所有`,
dynamicMessage: process.env.NODE_ENV === 'development' ? '开发模式' : '生产模式'
};3. 类型安全的翻译 Hook
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. 开发时的最佳实践
自动检查翻译完整性
// 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 规则强制使用类型安全键
// .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. 翻译键文件未生成
检查步骤:
# 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. 性能问题
如果翻译文件过多导致构建缓慢:
// 优化配置
i18nPlugin({
exclude: ['**/node_modules/**', '**/dist/**'], // 排除无关目录
extensions: ['.json'], // 仅扫描 JSON 文件
deep: false // 禁用深度扫描
})📝 更新日志
v0.0.1 (2024-03-XX)
🚀 新特性
- 🎉 首次发布:智能的基于文件系统的国际化解决方案
- 🎉 新功能:支持 JSON、JavaScript 和 TypeScript 翻译文件
- 🎉 新功能:自动生成类型安全的翻译键文件
- 🎉 新功能:虚拟模块系统,零运行时开销
- 🎉 新功能:智能热更新和增量编译
- 🎉 新功能:优雅的调试日志系统
- 🎉 新功能:灵活的命名空间组织
- 🎉 新功能:完整的 TypeScript 支持和类型推断
✨ 核心功能
- 智能文件扫描:使用
fast-glob高效扫描翻译文件 - 类型安全:自动生成
I18nKeys联合类型,提供编译时检查 - 热更新支持:文件变化时自动重新生成资源和类型
- 性能优化:智能缓存机制,避免重复解析
- 开发体验:带时间戳的彩色调试日志
📄 许可证
MIT License - 详见 LICENSE 文件
相关项目
- @gulibs/vgrove-autoroutes - VGrove 自动路由插件
- @gulibs/vgrove-client - VGrove 客户端库
- @gulibs/vgrove - VGrove 核心框架
💡 提示:如果遇到问题,请启用
debug: true查看详细的执行日志,这有助于快速定位问题所在。
