UNPKG

@gulibs/react-vintl

Version:

Type-safe i18n library for React with Vite plugin and automatic type inference

608 lines (459 loc) 15.2 kB
# @gulibs/react-vintl > 🌍 为 React 设计的类型安全 i18n 库,具有自动类型推断功能 - 无需单独的键文件! ## ✨ 特性 - 🎯 **完美的 IDE 自动补全** - 联合类型生成提供无与伦比的自动补全体验(例如,输入 `t('common.` 会显示所有 `common.*` 键) -**极致 API 设计** - `useTranslation()` 直接返回函数,无需解构! - 🚀 **零配置** - 开箱即用,具有合理的默认设置 - 🔥 **热模块替换** - 翻译文件的更改会立即更新类型和 UI - 📦 **Vite 插件** - 与您的 Vite 工作流无缝集成 - 🎨 **React Hooks 和组件** - 适用于所有用例的灵活 API - 🌐 **多种文件格式** - 支持 JSON、TypeScript 和 JavaScript - 💪 **完全类型安全** - 无效翻译键的编译时错误 - 🔄 **命名空间支持** - 使用点记法的有序分层键结构 ## 📦 安装 ```bash npm install @gulibs/react-vintl # 或 pnpm add @gulibs/react-vintl # 或 yarn add @gulibs/react-vintl ``` ## 🚀 快速开始 ### 1. 配置 Vite 插件 ```typescript // vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { createReactVintlPlugin } from '@gulibs/react-vintl' export default defineConfig({ plugins: [ react(), createReactVintlPlugin({ basePath: 'src/locales', debug: true }) ] }) ``` ### 2. 创建翻译文件 ``` src/locales/ ├── en/ │ ├── common.json │ └── auth.json └── zh/ ├── common.json └── auth.json ``` **翻译文件示例:** ```json // src/locales/en/common.json { "welcome": "Welcome", "buttons": { "save": "Save", "cancel": "Cancel" } } ``` ```json // src/locales/zh/common.json { "welcome": "欢迎", "buttons": { "save": "保存", "cancel": "取消" } } ``` ### 3. 在您的应用中使用 ```tsx // src/App.tsx import { I18nProvider, useTranslation, useI18n, Translation } from '@gulibs/react-vintl' import { resources, supportedLocales } from '@gulibs/react-vintl-locales' function ExampleComponent() { // ✅ 极致 API:useTranslation 直接返回翻译函数 const t = useTranslation() // ✅ 用于全局状态的独立钩子 const { locale, setLocale } = useI18n() return ( <div> <h1>{t('common.welcome')}</h1> <button>{t('common.buttons.save')}</button> {/* 使用 Translation 组件 */} <Translation keyPath="common.welcome" /> {/* 语言切换器 */} <select value={locale} onChange={e => setLocale(e.target.value)}> {supportedLocales.map(loc => ( <option key={loc} value={loc}>{loc}</option> ))} </select> </div> ) } export default function App() { return ( <I18nProvider resources={resources} defaultLocale="en" supportedLocales={supportedLocales} > <ExampleComponent /> </I18nProvider> ) } ``` ## 📖 API 参考 ### 插件选项 ```typescript interface I18nPluginOptions { basePath?: string // 默认: 'src/locales' extensions?: string[] // 默认: ['.json', '.ts', '.js'] localePattern?: 'directory' | 'filename' // 默认: 'directory' hmr?: boolean // 默认: true debug?: boolean // 默认: false } ``` ### 钩子 #### `useTranslation(namespace?)` **新特性**: 直接返回翻译函数 - 无需解构! ```typescript // 无命名空间 - 完整键路径 const t = useTranslation() t('common.welcome') // ✅ 完整自动补全 t('auth.login.title') // ✅ 所有键都可用 // 带命名空间 - 相对键路径 const tCommon = useTranslation('common') tCommon('welcome') // ✅ 仅 common.* 键 tCommon('buttons.save') // ✅ 作用域自动补全 const tAuth = useTranslation('auth') tAuth('login.title') // ✅ 仅 auth.* 键 // 带参数 const text = t('messages.greeting', { name: 'World' }) ``` **类型签名:** ```typescript function useTranslation(): TranslationFunction function useTranslation<N extends I18nNamespaces>(namespace: N): NamespacedTranslationFunction<N> ``` #### `useI18n()` **新特性**: 从翻译函数中独立获取全局 i18n 状态。 ```typescript const { locale, supportedLocales, setLocale, resources } = useI18n() // 现在您可以多次调用 useTranslation 而不会产生冗余状态 const t1 = useTranslation('common') const t2 = useTranslation('auth') const { locale } = useI18n() // 只获取状态一次 ``` #### `useLocale()` 用于语言切换的 `useI18n()` 简化版本。 ```typescript const { locale, supportedLocales, setLocale, isSupportedLocale } = useLocale() ``` #### `useTranslationKey(key, params?)` 返回带有记忆化的特定翻译。 ```typescript const { translation, t } = useTranslationKey('common.welcome') ``` ### 组件 #### `<Translation>` 渲染带有可选参数和命名空间支持的翻译。 ```tsx {/* 无命名空间 - 完整键路径 */} <Translation keyPath="common.welcome" /> <Translation keyPath="auth.login.title" params={{ name: 'React' }} /> {/* 带命名空间 - 相对键路径 */} <Translation namespace="common" keyPath="welcome" /> <Translation namespace="auth" keyPath="login.title" /> {/* 自定义渲染 */} <Translation keyPath="common.welcome" fallback="Welcome"> {text => <strong>{text}</strong>} </Translation> ``` #### `<T>` `<Translation>` 组件的别名。 ```tsx <T keyPath="common.welcome" /> <T namespace="auth" keyPath="logout" /> ``` #### `<LocaleSwitcher>` 内置语言切换器组件。 ```tsx <LocaleSwitcher /> ``` ## ⚡ 极致 API 设计 `react-vintl` 遵循现代 React Hooks 约定,通过**直接返回函数**而不是对象。 ### 对象返回的问题 ```typescript // ❌ 传统方法 - 需要解构 const { t } = useTranslation() const { t: tCommon } = useTranslation('common') const { t: tAuth, locale } = useTranslation('auth') // 问题:冗长,冗余的状态访问 ``` ### 我们的解决方案:直接函数返回 ```typescript // ✅ 极致 API - 直接函数返回 const t = useTranslation() // 无需解构! const tCommon = useTranslation('common') const tAuth = useTranslation('auth') // ✅ 独立的状态访问 const { locale, setLocale } = useI18n() // 仅在需要时 ``` ### 优势 1. **更简洁的代码** ```typescript const t = useTranslation() // 对比 const { t } = useTranslation() ``` 2. **清晰的关注点分离** - `useTranslation()` → 翻译函数 - `useI18n()` → 全局状态管理 - `useLocale()` → 语言切换 3. **更好的性能** - 使用多个命名空间时不会创建冗余对象 - 减少内存占用 4. **遵循 React 约定** -`useRef()` - 直接返回 ref -`useCallback()` - 直接返回函数 -`useMemo()` - 直接返回值 ### 实际示例 ```typescript function MyComponent() { // 多个翻译函数 - 无冗余 const tCommon = useTranslation('common') const tAuth = useTranslation('auth') const tProducts = useTranslation('products') // 全局状态 - 只访问一次 const { locale, setLocale } = useI18n() return ( <div> <h1>{tCommon('welcome')}</h1> <p>{tAuth('login.title')}</p> <button>{tProducts('addToCart')}</button> <p>Current: {locale}</p> </div> ) } ``` ## 🎯 类型推断魔法 `react-vintl` 的核心创新是**自动类型推断**,无需生成单独的类型文件! ### 工作原理 1. **启动时生成** - 插件扫描翻译文件并在 `react-vintl-locales.d.ts` 中生成联合类型 2. **联合类型推断** - 所有翻译键都被提取为字符串字面量联合类型 3. **热模块替换** - 文件更改会自动更新类型并触发 HMR 4. **完美的 IDE 集成** - TypeScript 为所有键提供即时自动补全 ```typescript // 插件自动在 react-vintl-locales.d.ts 中生成: export type I18nKeys = | 'common.welcome' | 'common.buttons.save' | 'common.buttons.cancel' | 'auth.login.title' | 'auth.login.username' | ... // 所有您的翻译键作为联合类型! // 您的编辑器获得完美的自动补全! const text = t('common.') // ✅ 显示:welcome, buttons.save, buttons.cancel... const text = t('auth.login.') // ✅ 显示:title, username, password... ``` **为什么使用联合类型?** - ✅ 完美的 IDE 自动补全体验 - ✅ 即使有数千个键也能快速 TypeScript 编译 - ✅ 使用无效键时的清晰类型错误 - ✅ 命名空间感知建议(输入 `common.` 只显示 `common.*` 键) ### 无需单独的键文件! 与需要生成单独类型定义文件的传统 i18n 库不同,`react-vintl` 直接从虚拟模块推断类型: ```typescript // ❌ 传统方法 - 需要单独生成 import { I18nKeys } from './i18n-keys' // 单独文件 // ✅ react-vintl - 自动推断 import { type I18nKeys } from '@gulibs/react-vintl-locales' // 来自虚拟模块! ``` ## 🔧 高级用法 ### TypeScript 翻译文件 您可以使用 TypeScript 进行动态翻译: ```typescript // src/locales/en/dynamic.ts export default { currentYear: new Date().getFullYear(), greeting: (name: string) => `Hello, ${name}!`, environment: process.env.NODE_ENV === 'development' ? 'Dev' : 'Prod' } as const ``` ### 命名空间翻译 按功能组织翻译: ``` src/locales/ ├── en/ │ ├── common.json │ ├── auth/ │ │ ├── login.json │ │ └── register.json │ └── dashboard/ │ └── stats.json ``` 使用点记法访问: ```typescript t('auth.login.title') t('dashboard.stats.users') ``` ### 高级参数插值 `react-vintl` 支持强大的参数插值系统,包括多种格式化选项: #### 简单插值 ```json { "greeting": "Hello, {{name}}!", "user_info": "User {{user.name}} has {{user.count}} messages" } ``` ```tsx t('greeting', { name: 'John' }) // 结果: "Hello, John!" t('user_info', { user: { name: 'Jane', count: 3 } }) // 结果: "User Jane has 3 messages" ``` #### 复数形式 ```json { "item_count": "{{count, plural, one=1 item, other=# items}}", "message_count": "{{count, plural, zero=No messages, one=1 message, other=# messages}}" } ``` ```tsx t('item_count', { count: 1 }) // "1 item" t('item_count', { count: 5 }) // "5 items" t('message_count', { count: 0 }) // "No messages" ``` #### 选择形式 ```json { "gender_greeting": "{{gender, select, male=He, female=She, other=They}} is here", "status_message": "{{status, select, active=Online, inactive=Offline, other=Unknown}}" } ``` ```tsx t('gender_greeting', { gender: 'male' }) // "He is here" t('gender_greeting', { gender: 'female' }) // "She is here" t('status_message', { status: 'active' }) // "Online" ``` #### 数字格式化 ```json { "price": "Price: {{amount, number}}", "decimal": "Value: {{value, number, minimumFractionDigits=2}}", "currency": "Cost: {{amount, currency, currency=USD}}" } ``` ```tsx t('price', { amount: 1234.56 }) // "Price: 1,234.56" t('decimal', { value: 1234.5 }) // "Value: 1,234.50" t('currency', { amount: 99.99 }) // "Cost: $99.99" ``` #### 日期格式化 ```json { "created_date": "Created on {{date, date}}", "full_date": "{{date, date, year=numeric, month=long, day=numeric}}", "time": "Time: {{date, date, hour=numeric, minute=2-digit}}" } ``` ```tsx t('created_date', { date: new Date() }) // 结果: "Created on 1/15/2024" t('full_date', { date: new Date('2024-01-15') }) // 结果: "January 15, 2024" ``` #### 文本格式化 ```json { "text_format": "{{text, uppercase}} - {{text, lowercase}} - {{text, capitalize}}" } ``` ```tsx t('text_format', { text: 'hello world' }) // 结果: "HELLO WORLD - hello world - Hello world" ``` #### 嵌套对象支持 ```json { "user_profile": "{{user.profile.name}} ({{user.profile.age}} years old)" } ``` ```tsx t('user_profile', { user: { profile: { name: 'John', age: 25 } } }) // 结果: "John (25 years old)" ``` #### 错误处理和验证 `react-vintl` 提供了强大的错误处理机制: ```tsx // 参数验证 t('greeting', { name: 'John' }) // ✅ 正常 t('greeting', { invalid: 'data' }) // ⚠️ 警告:参数不匹配 t('greeting', { name: null }) // ✅ 正常:null 值会被转换为字符串 // 循环引用检测 const circular = { name: 'John' } circular.self = circular t('greeting', circular) // ⚠️ 警告:检测到循环引用 // 格式错误处理 t('price', { amount: 'invalid' }) // ✅ 正常:返回原始值 t('date', { date: 'invalid-date' }) // ✅ 正常:返回原始值 ``` ## 🎨 与其他解决方案的对比 ### vs vgrove-i18n - ✅ 相同的类型推断方法 - ✅ React 特定的钩子和组件 - ✅ 无需 `keysOutput` 选项 -**更强大的参数插值系统** - 支持复数、选择、格式化等高级功能 ### vs react-i18next - ✅ 更好的 TypeScript 支持 - ✅ Vite 原生虚拟模块 - ✅ 无运行时开销 - ✅ 自动类型推断 -**内置格式化功能** - 无需额外插件即可使用数字、日期、货币格式化 ### vs formatjs - ✅ 更简单的 API - ✅ 无需 ICU 消息格式 - ✅ 更好的开发者体验与自动补全 -**轻量级实现** - 无需复杂的 ICU 解析器 -**更好的性能** - 原生 JavaScript 实现,无外部依赖 ## 📝 最佳实践 1. **使用目录模式进行本地化** - 更容易组织和维护 2. **保持翻译文件小巧** - 按功能或模块分割 3. **对动态内容使用 TypeScript** - 更好的类型安全 4. **利用自动补全** - 让您的编辑器指导您 5. **合理使用参数插值** - 避免过度复杂的插值表达式 6. **参数验证** - 始终验证传入的参数,避免运行时错误 7. **格式化一致性** - 在项目中保持数字、日期格式的一致性 8. **复数规则** - 根据目标语言正确设置复数规则 ## 🐛 故障排除 ### 类型在编辑器中不显示? 1. 在编辑器中重启 TypeScript 服务器 2. 检查插件是否在 `vite.config.ts` 中正确配置 3. 确保翻译文件位于正确的位置 ### 热重载不工作? 1. 检查插件选项中的 `hmr: true` 2. 验证文件路径是否正确 3. 重启开发服务器 ### 参数插值不工作? 1. **检查参数格式** - 确保使用 `{{key}}` 格式 2. **验证参数传递** - 确保参数对象正确传递 3. **检查控制台警告** - 查看是否有参数验证错误 4. **测试简单插值** - 先测试 `{{name}}` 等简单格式 5. **检查嵌套对象** - 确保嵌套对象结构正确 ### 格式化不生效? 1. **检查格式选项** - 确保格式选项拼写正确 2. **验证数据类型** - 确保传入的数据类型正确 3. **测试浏览器支持** - 某些格式化功能需要现代浏览器支持 4. **查看控制台错误** - 格式化失败时会回退到原始值 ## 📄 许可证 MIT ## 🤝 贡献 欢迎贡献!请随时提交 Pull Request。 --- **由 gulibs 用 ❤️ 制作**