@gulibs/react-vintl
v0.0.20
Published
Type-safe i18n library for React with Vite plugin and automatic type inference
Maintainers
Readme
@gulibs/react-vintl
🌍 为 React 设计的类型安全 i18n 库,具有自动类型推断功能 - 无需单独的键文件!
✨ 特性
- 🎯 完美的 IDE 自动补全 - 联合类型生成提供无与伦比的自动补全体验(例如,输入
t('common.会显示所有common.*键) - ⚡ 极致 API 设计 -
useTranslation()直接返回函数,无需解构! - 🚀 零配置 - 开箱即用,具有合理的默认设置
- 🔥 热模块替换 - 翻译文件的更改会立即更新类型和 UI
- 📦 Vite 插件 - 与您的 Vite 工作流无缝集成
- 🎨 React Hooks 和组件 - 适用于所有用例的灵活 API
- 🌐 多种文件格式 - 支持 JSON、TypeScript 和 JavaScript
- 💪 完全类型安全 - 无效翻译键的编译时错误
- 🔄 命名空间支持 - 使用点记法的有序分层键结构
- 🔄 自动类型同步 - 添加新字段后编辑器立即识别,无需重启 TypeScript 服务器
- 🌐 远程翻译加载 - 支持从网络接口动态加载翻译并自动合并
📦 安装
npm install @gulibs/react-vintl
# 或
pnpm add @gulibs/react-vintl
# 或
yarn add @gulibs/react-vintl🚀 快速开始
1. 配置 Vite 插件
// 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翻译文件示例:
// src/locales/en/common.json
{
"welcome": "Welcome",
"buttons": {
"save": "Save",
"cancel": "Cancel"
}
}// src/locales/zh/common.json
{
"welcome": "欢迎",
"buttons": {
"save": "保存",
"cancel": "取消"
}
}3. 在您的应用中使用
// 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 参考
插件选项
interface I18nPluginOptions {
basePath?: string // 默认: 'src/locales'
extensions?: string[] // 默认: ['.json', '.ts', '.js']
localePattern?: 'directory' | 'filename' // 默认: 'directory'
hmr?: boolean // 默认: true
debug?: boolean // 默认: false
}钩子
useTranslation(namespace?)
新特性: 直接返回翻译函数 - 无需解构!
// 无命名空间 - 完整键路径
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' })类型签名:
function useTranslation(): TranslationFunction
function useTranslation<N extends I18nNamespaces>(namespace: N): NamespacedTranslationFunction<N>useI18n()
新特性: 从翻译函数中独立获取全局 i18n 状态。
const { locale, supportedLocales, setLocale, resources } = useI18n()
// 现在您可以多次调用 useTranslation 而不会产生冗余状态
const t1 = useTranslation('common')
const t2 = useTranslation('auth')
const { locale } = useI18n() // 只获取状态一次useLocale()
用于语言切换的 useI18n() 简化版本。
const { locale, supportedLocales, setLocale, isSupportedLocale } = useLocale()useTranslationKey(key, params?)
返回带有记忆化的特定翻译。
const { translation, t } = useTranslationKey('common.welcome')useLoadRemoteTranslations(source, options?)
新特性: 从外部接口加载翻译资源并自动合并到现有资源。
支持三种加载方式:
- URL 字符串: 从 HTTP 接口加载
- 函数: 从自定义加载函数获取
- Promise: 从 Promise 获取
// 从 URL 加载
const { loading, error, data, retry } = useLoadRemoteTranslations(
'https://api.example.com/translations/en',
{
namespace: 'remote', // 合并到指定的 namespace
locale: 'en',
merge: true, // 自动合并(默认 true)
cache: true, // 启用缓存(默认 true)
onSuccess: (resources) => console.log('Loaded:', resources),
onError: (error) => console.error('Error:', error),
retry: 3, // 重试次数
retryDelay: 1000 // 重试延迟(毫秒)
}
)
// 从函数加载
const loader = useLoadRemoteTranslations(
() => fetch('/api/translations').then(r => r.json()),
{ namespace: 'remote' }
)
// 从 Promise 加载
const promiseLoader = useLoadRemoteTranslations(
fetch('/api/translations').then(r => r.json()),
{ namespace: 'remote' }
)
// 使用合并后的翻译(立即可用)
const t = useTranslation()
t('remote.title' as any) // 动态字段需要使用类型断言选项说明:
interface LoadRemoteTranslationsOptions {
locale?: string // 目标语言,默认使用当前语言
namespace?: string // 合并到指定的 namespace(可选)
merge?: boolean // 是否自动合并(默认 true)
cache?: boolean // 是否启用缓存(默认 true)
onSuccess?: (resources: I18nResources) => void
onError?: (error: Error) => void
retry?: number // 重试次数(默认 0)
retryDelay?: number // 重试延迟(毫秒,默认 1000)
}返回值:
interface LoadRemoteTranslationsResult {
loading: boolean // 是否正在加载
error: Error | null // 错误信息
data: I18nResources | null // 加载的数据
retry: () => void // 手动重试函数
}特性:
- ✅ 自动合并到现有资源
- ✅ 支持 namespace 级别的合并
- ✅ 支持根级别合并(不指定 namespace)
- ✅ 加载状态管理(loading、error、data)
- ✅ 智能缓存机制避免重复请求
- 不同的
namespace配置使用不同的缓存键,避免冲突 - 支持缓存过期时间(默认 5 分钟)
- 不同的
- ✅ 请求去重(多个组件同时加载相同资源)
- ✅ 错误处理和重试机制
- ✅ 加载后翻译立即可用
- ✅ 资源合并后自动更新翻译函数
完整示例:
查看 test/src/RemoteTranslationDemo.tsx 获取完整的使用示例。
组件
<Translation>
渲染带有可选参数和命名空间支持的翻译。
{/* 无命名空间 - 完整键路径 */}
<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> 组件的别名。
<T keyPath="common.welcome" />
<T namespace="auth" keyPath="logout" /><LocaleSwitcher>
内置语言切换器组件。
<LocaleSwitcher />⚡ 极致 API 设计
react-vintl 遵循现代 React Hooks 约定,通过直接返回函数而不是对象。
对象返回的问题
// ❌ 传统方法 - 需要解构
const { t } = useTranslation()
const { t: tCommon } = useTranslation('common')
const { t: tAuth, locale } = useTranslation('auth')
// 问题:冗长,冗余的状态访问我们的解决方案:直接函数返回
// ✅ 极致 API - 直接函数返回
const t = useTranslation() // 无需解构!
const tCommon = useTranslation('common')
const tAuth = useTranslation('auth')
// ✅ 独立的状态访问
const { locale, setLocale } = useI18n() // 仅在需要时优势
更简洁的代码
const t = useTranslation() // 对比 const { t } = useTranslation()清晰的关注点分离
useTranslation()→ 翻译函数useI18n()→ 全局状态管理useLocale()→ 语言切换
更好的性能
- 使用多个命名空间时不会创建冗余对象
- 减少内存占用
遵循 React 约定
- 像
useRef()- 直接返回 ref - 像
useCallback()- 直接返回函数 - 像
useMemo()- 直接返回值
- 像
实际示例
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 的核心创新是自动类型推断,无需生成单独的类型文件!
工作原理
- 启动时生成 - 插件扫描翻译文件并在
react-vintl-locales.d.ts中生成联合类型 - 联合类型推断 - 所有翻译键都被提取为字符串字面量联合类型
- 热模块替换 - 文件更改会自动更新类型并触发 HMR
- 完美的 IDE 集成 - TypeScript 为所有键提供即时自动补全
自动类型同步
当您添加新的国际化字段时,插件会:
- 自动检测变化 - 监听 locale 文件夹的文件变化
- 更新类型定义 - 自动更新
react-vintl-locales.d.ts文件 - 触发 TypeScript 服务器重新加载 - 通过文件系统事件通知 TypeScript 语言服务器
- 立即生效 - 编辑器通常在几秒内识别新字段,无需手动重启
这意味着:
- ✅ 添加新字段后,编辑器立即提供自动补全
- ✅ 构建时类型检查自动通过,不会因为新字段而失败
- ✅ 无需手动重启 TypeScript 服务器
- ✅ 支持所有主流编辑器(VSCode、WebStorm、Sublime Text 等)
// 插件自动在 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 直接从虚拟模块推断类型:
// ❌ 传统方法 - 需要单独生成
import { I18nKeys } from './i18n-keys' // 单独文件
// ✅ react-vintl - 自动推断
import { type I18nKeys } from '@gulibs/react-vintl-locales' // 来自虚拟模块!🔧 高级用法
远程翻译加载
react-vintl 支持从外部接口动态加载翻译资源,实现文件翻译(基础)+ 接口翻译(动态)的混合模式。
基本用法
import { useLoadRemoteTranslations, useTranslation } from '@gulibs/react-vintl'
function MyComponent() {
// 加载远程翻译
const { loading, error } = useLoadRemoteTranslations(
'https://api.example.com/translations/en',
{
namespace: 'remote', // 合并到 'remote' namespace
merge: true // 自动合并
}
)
const t = useTranslation()
if (loading) return <div>Loading translations...</div>
if (error) return <div>Error: {error.message}</div>
// 远程翻译立即可用
return <div>{t('remote.title' as any)}</div>
}合并策略
Namespace 级别合并:
// 将远程翻译合并到指定的 namespace
useLoadRemoteTranslations(
fetch('/api/translations').then(r => r.json()),
{ namespace: 'remote' }
)
// 结果结构:
// {
// en: {
// common: { ... }, // 文件翻译
// remote: { ... } // 远程翻译合并到这里
// }
// }根级别合并:
// 不指定 namespace,合并到根级别(与现有 namespace 平级)
useLoadRemoteTranslations(
Promise.resolve({
en: {
rootLevel: {
message: "This is merged at root level"
}
},
zh: {
rootLevel: {
message: "这是在根级别合并的"
}
}
}),
{ merge: true } // 不指定 namespace
)
// 结果结构:
// {
// en: {
// common: { ... }, // 文件翻译
// rootLevel: { ... } // 远程翻译(新 namespace,与 common 平级)
// }
// }
// 使用合并后的翻译
const t = useTranslation()
t('rootLevel.message') // ✅ 立即可用重要提示:
- 根级别合并时,远程翻译中的每个顶级键都会作为新的 namespace 合并
- 不同的
namespace配置会使用不同的缓存键,即使source相同也不会冲突 - 资源合并后,翻译函数会在下次渲染时自动更新,确保使用最新资源
手动更新资源
您也可以手动更新资源:
import { useI18nContext } from '@gulibs/react-vintl'
function MyComponent() {
const { updateResources } = useI18nContext()
const handleLoad = async () => {
const remoteTranslations = await fetch('/api/translations').then(r => r.json())
// 合并到指定的 namespace
updateResources(remoteTranslations, {
namespace: 'remote',
merge: true
})
}
return <button onClick={handleLoad}>Load Translations</button>
}工具函数
import { mergeResources } from '@gulibs/react-vintl'
// 手动合并资源
const merged = mergeResources(
baseResources,
remoteResources,
{
namespace: 'remote', // 可选:合并到指定 namespace
locale: 'en' // 可选:只合并指定 locale
}
)完整示例:
查看 test/src/RemoteTranslationDemo.tsx 获取完整的使用示例和所有功能演示。
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使用点记法访问:
t('auth.login.title')
t('dashboard.stats.users')高级参数插值
react-vintl 支持强大的参数插值系统,包括多种格式化选项:
简单插值
{
"greeting": "Hello, {{name}}!",
"user_info": "User {{user.name}} has {{user.count}} messages"
}t('greeting', { name: 'John' })
// 结果: "Hello, John!"
t('user_info', { user: { name: 'Jane', count: 3 } })
// 结果: "User Jane has 3 messages"复数形式
{
"item_count": "{{count, plural, one=1 item, other=# items}}",
"message_count": "{{count, plural, zero=No messages, one=1 message, other=# messages}}"
}t('item_count', { count: 1 }) // "1 item"
t('item_count', { count: 5 }) // "5 items"
t('message_count', { count: 0 }) // "No messages"选择形式
{
"gender_greeting": "{{gender, select, male=He, female=She, other=They}} is here",
"status_message": "{{status, select, active=Online, inactive=Offline, other=Unknown}}"
}t('gender_greeting', { gender: 'male' }) // "He is here"
t('gender_greeting', { gender: 'female' }) // "She is here"
t('status_message', { status: 'active' }) // "Online"数字格式化
{
"price": "Price: {{amount, number}}",
"decimal": "Value: {{value, number, minimumFractionDigits=2}}",
"currency": "Cost: {{amount, currency, currency=USD}}"
}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"日期格式化
{
"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}}"
}t('created_date', { date: new Date() })
// 结果: "Created on 1/15/2024"
t('full_date', { date: new Date('2024-01-15') })
// 结果: "January 15, 2024"文本格式化
{
"text_format": "{{text, uppercase}} - {{text, lowercase}} - {{text, capitalize}}"
}t('text_format', { text: 'hello world' })
// 结果: "HELLO WORLD - hello world - Hello world"嵌套对象支持
{
"user_profile": "{{user.profile.name}} ({{user.profile.age}} years old)"
}t('user_profile', {
user: {
profile: {
name: 'John',
age: 25
}
}
})
// 结果: "John (25 years old)"错误处理和验证
react-vintl 提供了强大的错误处理机制:
// 参数验证
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 实现,无外部依赖
📝 最佳实践
- 使用目录模式进行本地化 - 更容易组织和维护
- 保持翻译文件小巧 - 按功能或模块分割
- 对动态内容使用 TypeScript - 更好的类型安全
- 利用自动补全 - 让您的编辑器指导您
- 合理使用参数插值 - 避免过度复杂的插值表达式
- 参数验证 - 始终验证传入的参数,避免运行时错误
- 格式化一致性 - 在项目中保持数字、日期格式的一致性
- 复数规则 - 根据目标语言正确设置复数规则
- 远程翻译策略 - 使用文件翻译作为基础,远程翻译作为补充
- 缓存管理 - 合理使用缓存机制,避免不必要的网络请求
- 不同的
namespace配置会自动使用不同的缓存键 - 根级别合并和 namespace 合并不会共享缓存
- 不同的
- 错误处理 - 为远程翻译加载提供适当的错误处理和重试机制
- 资源同步 - 资源合并后,翻译函数会在下次渲染时自动更新
- 如果需要在资源合并后立即使用翻译键,可以使用
useI18n()检查资源是否已同步
- 如果需要在资源合并后立即使用翻译键,可以使用
🔧 日志配置
react-vintl 提供了统一的日志系统,允许您控制所有日志输出。所有模块(包括插件)都使用统一的 logger,确保日志行为一致。
日志级别
debug- 调试日志,用于开发时的详细调试信息(如[useLoadRemoteTranslations]、[mergeResources])info- 一般信息日志,用于重要的运行时信息warn- 警告日志,用于潜在问题或非致命错误error- 错误日志,用于错误信息(始终输出,包括生产环境)
默认行为
- 开发环境 (
NODE_ENV=development): 所有日志级别都启用 - 生产环境 (
NODE_ENV=production): 仅error日志启用,debug、info、warn自动禁用
控制日志输出
import { logger } from '@gulibs/react-vintl'
// 禁用调试日志
logger.setConfig({ debug: false })
// 禁用信息日志
logger.setConfig({ info: false })
// 禁用警告日志
logger.setConfig({ warn: false })
// 禁用错误日志(不推荐,仅在特殊情况下使用)
logger.setConfig({ error: false })
// 同时配置多个选项
logger.setConfig({
debug: false,
info: true,
warn: true,
error: true
})日志去重
Logger 默认启用去重机制,防止相同消息在短时间内(默认 5 秒)重复输出:
// 禁用去重
logger.setConfig({ deduplicate: false })
// 自定义去重时间窗口(毫秒)
logger.setConfig({ deduplicationWindow: 10000 }) // 10 秒调试日志和生产日志的区别
- 调试日志 (
debug): 包含详细的执行流程信息,如资源加载、合并过程等,默认在生产环境禁用 - 生产日志: 仅包含错误信息,确保生产环境不会输出过多日志影响性能
插件日志
插件中的日志也通过 logger 统一控制。插件会尊重其 debug 选项,但最终输出由 logger 配置决定:
// vite.config.ts
createReactVintlPlugin({
basePath: 'src/locales',
debug: true // 插件会使用 logger.debug() 输出日志
})即使插件设置了 debug: true,如果 logger 的 debug 配置为 false,日志仍不会输出。
🐛 故障排除
类型在编辑器中不显示?
- 等待自动同步 - 插件会自动更新类型文件并触发 TypeScript 服务器重新加载,通常几秒内生效
- 手动重启 TypeScript 服务器 - 如果类型仍未更新,在编辑器中重启 TypeScript 服务器:
- VSCode:
Cmd/Ctrl + Shift + P→ 输入 "TypeScript: Restart TS Server" - WebStorm:
File→Invalidate Caches...→ 选择 "Invalidate and Restart"
- VSCode:
- 检查插件是否在
vite.config.ts中正确配置 - 确保翻译文件位于正确的位置
- 检查
react-vintl-locales.d.ts文件是否存在且包含最新的类型定义
添加新字段后编辑器仍显示类型错误?
这是类型同步问题。插件会自动处理,但有时需要额外步骤:
- 等待几秒钟 - 插件会在文件变化后自动更新类型并触发 TypeScript 服务器重新加载
- 检查文件是否已更新 - 查看
react-vintl-locales.d.ts文件,确认新字段已包含在类型定义中 - 重启 TypeScript 服务器 - 如果类型文件已更新但编辑器仍显示错误,手动重启 TypeScript 服务器
- 检查构建模式 - 在构建时,类型文件会自动更新,确保构建时类型检查通过
- 查看插件日志 - 如果启用了
debug: true,检查控制台是否有类型更新相关的错误信息
如果问题仍然存在:
- 确保开发服务器正在运行(类型更新需要 HMR 支持)
- 检查文件权限,确保插件可以写入类型文件
- 尝试删除
react-vintl-locales.d.ts文件,让插件重新生成
热重载不工作?
- 检查插件选项中的
hmr: true - 验证文件路径是否正确
- 重启开发服务器
参数插值不工作?
- 检查参数格式 - 确保使用
{{key}}格式 - 验证参数传递 - 确保参数对象正确传递
- 检查控制台警告 - 查看是否有参数验证错误
- 测试简单插值 - 先测试
{{name}}等简单格式 - 检查嵌套对象 - 确保嵌套对象结构正确
格式化不生效?
- 检查格式选项 - 确保格式选项拼写正确
- 验证数据类型 - 确保传入的数据类型正确
- 测试浏览器支持 - 某些格式化功能需要现代浏览器支持
- 查看控制台错误 - 格式化失败时会回退到原始值
📄 许可证
MIT
🤝 贡献
欢迎贡献!请随时提交 Pull Request。
由 gulibs 用 ❤️ 制作
