@hd-translate/core
v0.1.5
Published
Framework-agnostic i18n core engine
Readme
@hd-translate/core
通用国际化核心引擎 — 框架无关、API 驱动、内置 axios fetcher 与三级缓存
@hd-translate/core 是 hd-translate SDK 的核心包,提供翻译获取、缓存、复数、插值等纯逻辑能力,不依赖任何前端框架。各框架适配层(Vue / React / Vanilla / AngularJS)均基于此构建。
特性
- 框架无关 — 纯逻辑实现,可在浏览器、Node.js、SSR、小程序等任意 JS 环境运行
- API 驱动 — 翻译从远程接口获取,内置 axios fetcher(超时 + 重试 + 请求去重),支持全量与增量更新
- 三级缓存 — 内存 → localStorage → API,TTL + 版本号双重失效
- 请求去重 — 同一 locale + 命名空间的并发请求自动合并为一次
- 命名空间 — 按模块/路由按需加载翻译,避免首屏拉全量
- 复数规则 — 内置中/英/俄语复数规则,可通过
pluralRules扩展其他语言 - 插值 — 支持
{{name}}与{name}两种风格 - locale 持久化 — 默认把上次语言写入 localStorage(按 appId 隔离),刷新后自动恢复;Node/SSR 自动降级
- TypeScript — 完整类型定义
安装
pnpm add @hd-translate/core
# 或 npm / yarn
npm install @hd-translate/core快速开始
import { createI18n } from '@hd-translate/core'
const i18n = createI18n({
appId: 'my-product',
apiUrl: 'https://i18n.example.com/api/translations',
locale: 'zh-CN',
fallbackLocale: 'en',
namespaces: ['common', 'menu'],
// 内置 axios fetcher 配置(不传走默认:超时 5s、重试 1 次)
fetcherOptions: {
timeout: 10000,
retries: 2,
headers: { 'X-App-Version': '1.0.0' },
},
// 三级缓存:内存 + localStorage,按版本号失效
cache: {
ttl: 24 * 60 * 60 * 1000, // 24h
persistent: true, // 启用 localStorage 持久化
version: '1.0', // 发版改 version 即整体失效
},
})
// 翻译加载是异步的,监听 loaded 后再取值
i18n.on('loaded', ({ locale, namespaces }) => {
console.log(i18n.t('common:hello', { name: 'World' }))
})API
createI18n(options): I18nInstance
创建 i18n 实例。会自动触发首次翻译加载。
I18nOptions
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| appId | string | 是 | - | 产品标识,API 按此隔离翻译与 locale 持久化 |
| apiUrl | string | 是 | - | 翻译 API 地址(注入自定义 fetcher 时可空) |
| locale | string | 是 | - | 当前语言,如 'zh-CN' |
| fallbackLocale | string | 否 | 'en' | 回退语言,当前 locale 缺失 key 时使用 |
| namespaces | string[] | 否 | ['common'] | 初始加载的命名空间 |
| defaultNamespace | string | 否 | 'common' | key 不含 : 时使用的命名空间 |
| interpolationStyle | 'double-brace' \| 'brace' | 否 | 'double-brace' | 插值语法,对应 {{name}} / {name} |
| cache.ttl | number | 否 | 86400000 | 缓存过期时间(ms) |
| cache.persistent | boolean | 否 | true | 是否持久化到 localStorage |
| cache.memory | boolean | 否 | true | 是否启用持久化缓存内部的内存层(仅 persistent: true 时生效);设为 false 后读写直接走 storage,不再维护内存 Map,避免大翻译表占用双份内存 |
| cache.version | string | 否 | '' | 缓存版本号,变更时整体失效 |
| persistLocale | boolean | 否 | true | 是否持久化 locale 到 localStorage(按 appId 隔离),Node/SSR 自动降级 |
| fetcher | Fetcher | 否 | 内置 axios fetcher | 自定义翻译获取器;注入后 fetcherOptions 被忽略 |
| fetcherOptions.timeout | number | 否 | 5000 | 内置 axios fetcher 的请求超时(ms) |
| fetcherOptions.retries | number | 否 | 1 | 内置 axios fetcher 的失败重试次数 |
| fetcherOptions.headers | Record<string, string> | 否 | - | 内置 axios fetcher 的自定义请求头 |
| onLocaleChange | (locale: string) => void | 否 | - | 语言切换回调 |
| onFetched | (payload: FetchedPayload) => void | 否 | - | 接口请求成功回调,拿到 fetcher 返回的完整 payload(含 locale/version/timestamp/data + 本次请求的 namespaces);命中缓存时不触发 |
| onFetchError | (error: Error) => void | 否 | - | 请求失败回调 |
| onMissingKey | (key: string, locale: string) => string | 否 | - | 缺失 key 回调,返回值作为翻译结果(同步调用,非事件) |
I18nInstance
| 方法 | 说明 |
|------|------|
| t(key, params?) | 翻译,key 格式 namespace:path 或 path |
| t(key, locale, params?) | 指定语言翻译(不切换当前 locale) |
| setLocale(locale) | 切换语言,自动加载所需命名空间并持久化 |
| getLocale() | 获取当前语言 |
| loadNamespace(ns) | 按需加载单个命名空间到当前 locale |
| loadNamespaces(ns[]) | 一次加载多个命名空间到当前 locale(单次 fetcher 请求,比循环调 loadNamespace 更高效) |
| reload() | 清空缓存并重新加载当前 locale 全部命名空间 |
| on(event, handler) | 监听 localeChange / loaded / fetched / fetchError,返回取消函数 |
| destroy() | 销毁实例,清空消息表、缓存、进行中请求与事件监听 |
用法
命名空间按需加载
初始只加载 common,进入子路由时再加载对应命名空间:
await i18n.loadNamespace('order')
console.log(i18n.t('order:list_title'))一次加载多个命名空间(单次 fetcher 请求合并拉取,比循环调 loadNamespace 高效):
await i18n.loadNamespaces(['order', 'menu', 'profile'])
console.log(i18n.t('order:list_title'))
console.log(i18n.t('menu:dashboard'))
console.log(i18n.t('profile:name'))复数与插值
翻译 JSON(按 _zero / _one / _other 后缀区分复数类别):
{
"items_zero": "没有项目",
"items_one": "{{count}} 个项目",
"items_other": "{{count}} 个项目"
}_count 触发复数规则(按 locale 自动选 zero/one/other),count 是普通插值参数:
i18n.t('common:items', { _count: 0, count: 0 }) // "没有项目"
i18n.t('common:items', { _count: 1, count: 1 }) // "1 个项目"
i18n.t('common:items', { _count: 5, count: 5 }) // "5 个项目"
// 指定语言翻译(不切换当前 locale)
i18n.t('common:items', 'en', { _count: 5, count: 5 }) // "5 items"内置中/英/俄复数规则,可通过 pluralRules 扩展其他语言:
import { pluralRules } from '@hd-translate/core'
pluralRules['ja'] = (count) => (count === 0 ? 'zero' : 'other')缓存与强制刷新
后端修改翻译后,前端调用 reload() 清空缓存重新加载;发版时改 cache.version 让旧版本缓存整体失效:
await i18n.reload()关闭内存层只走 localStorage
默认 persistent: true 时缓存走「内存 + localStorage」双写:内存层提供热访问速度,storage 层跨刷新复用。翻译表较大、不想同时占用 JS 堆与 storage 时,可设 cache.memory: false,让 get/set/remove/clear 直接落 storage:
const i18n = createI18n({
appId: 'my-product',
apiUrl: '/api/translations',
locale: 'zh-CN',
cache: {
persistent: true, // 走 localStorage
memory: false, // 关闭内存层,只走 storage
version: '1.0',
},
})语言切换
await i18n.setLocale('en') // 触发 localeChange,自动加载 en 翻译并持久化
i18n.getLocale() // 'en'
const off = i18n.on('localeChange', (locale) => {
console.log('语言已切换:', locale)
})
off() // 取消监听接口数据透传
业务侧需要在翻译加载完成后拿到接口返回的原始数据(如做埋点、自定义缓存、写日志),通过 onFetched 钩子或 i18n.on('fetched', ...) 订阅。仅 fetcher 真正请求成功时触发,命中缓存不会触发:
// 方式一:option 形式
const i18n = createI18n({
appId: 'my-product',
apiUrl: '/api/translations',
locale: 'zh-CN',
onFetched: (payload) => {
// payload = { locale, version, timestamp, data, namespaces }
console.log('收到翻译数据:', payload.locale, payload.version, payload.namespaces)
// 业务侧自定义处理:上报、二次缓存、热更新等
},
})
// 方式二:事件订阅形式(适合运行时动态注册)
const off = i18n.on('fetched', (payload) => {
console.log('data keys:', Object.keys(payload.data), 'ns:', payload.namespaces)
})
off() // 取消监听Node.js / SSR
Node 环境无 localStorage,关闭持久化,用内存缓存 + 等待加载完成:
import { createI18n } from '@hd-translate/core'
const i18n = createI18n({
appId: 'my-product',
apiUrl: 'http://internal-i18n/api/translations',
locale: 'zh-CN',
fallbackLocale: 'en',
namespaces: ['common', 'page-home'],
cache: {
ttl: 5 * 60 * 1000, // 5 分钟内存缓存
persistent: false, // Node.js 无 localStorage
},
})
await new Promise(resolve => i18n.on('loaded', resolve))
console.log(i18n.t('common:hello', { name: 'SSR' }))缺失 key 处理
通过 onMissingKey 同步返回占位文案:
const i18n = createI18n({
appId: 'my-product',
apiUrl: '/api/translations',
locale: 'zh-CN',
onMissingKey: (key, locale) => {
console.warn(`缺失翻译: ${key} (${locale})`)
return `[${key}]`
},
})销毁实例
页面卸载或单测结束时清理,避免内存泄漏与事件累积:
i18n.destroy()翻译 API 接口约定
SDK 内置 fetcher 请求格式(后端需实现此接口):
GET /api/translations?appId=demo&locale=zh-CN&ns=common,menu
Response:
{
"locale": "zh-CN",
"version": "1.0.5",
"timestamp": 1718000000000,
"data": {
"common": {
"hello": "你好,{{name}}",
"confirm": "确认"
},
"menu": {
"home": "首页"
}
}
}增量更新通过 since 参数:
GET /api/translations?appId=demo&locale=zh-CN&since=1718000000000自定义 Fetcher
后端接口格式不同时,注入自定义 fetcher:
import { createI18n } from '@hd-translate/core'
const i18n = createI18n({
appId: 'my-product',
apiUrl: '', // 使用自定义 fetcher 时不需要
locale: 'zh-CN',
fetcher: {
async fetch({ appId, locale, namespaces }) {
const res = await fetch(`/api/v2/i18n/${locale}?ns=${namespaces.join(',')}`)
const data = await res.json()
return {
locale,
version: data.version,
timestamp: Date.now(),
data: data.translations,
}
},
},
})工具函数与缓存
除 createI18n 外,core 还导出可独立使用的能力:
import {
interpolate, // 字符串插值
getPluralCategory, // 取复数类别
resolvePluralKey, // 生成复数候选 key 列表
pluralRules, // 复数规则表(可扩展)
resolveKeyPath, // 按 dotted path 取嵌套翻译
createEventEmitter, // 事件发射器
createCache, // 默认缓存(按 options 决定 memory/persistent)
createMemoryCache, // 内存缓存
createPersistentCache, // 持久化缓存(可注入自定义 storage)
} from '@hd-translate/core'createPersistentCache 支持注入自定义 storage,适配小程序等非浏览器环境:
import { createPersistentCache } from '@hd-translate/core'
const cache = createPersistentCache({
ttl: 86400000,
version: '1.0',
storageKey: 'my-app-i18n',
storage: wx.storage, // 任何实现 getItem/setItem/removeItem/clear 的对象
})License
MIT
