@sv-kit/i18n-hot
v1.0.0
Published
Hot-updatable i18n for Svelte: remote manifest, ETag/version checks, local cache, and Svelte stores.
Downloads
3
Maintainers
Readme
@sv-kit/i18n-hot
Svelte/SvelteKit 的热更新 i18n:远端 Manifest + 304/ETag 校验 + 本地缓存 + Svelte stores。 不绑框架路由、零第三方依赖,支持懒加载、预加载、占位符校验与自动刷新。🈶🔥
- ⚡ 热更新:更新服务器上的 manifest(或变更 ETag)即可增量拉取新词条
- 🧳 本地缓存:
localStorage缓存 manifest ETag、各语言 ETag 与词典,断网也能用 - 🧩 Svelte 原生:
locale/dict/version三个 store;t()翻译函数 - 🧮 占位符校验:同 key 新旧占位符不一致时
console.warn - 🌏 多云适配:默认
HttpJsonAdapter;也可自定义适配器对接 OSS/COS/CDN 等
推荐在 monorepo 根安装
svelte作为 devDependency,而在本包中声明peerDependencies: { svelte: "^4 || ^5" }。
安装
pnpm add @sv-kit/i18n-hot
# or npm i / yarn add快速开始
1) 准备后端文件(示例)
/i18n/manifest.json
{
"version": "2025-09-16T12:00:00Z",
"locales": {
"en": { "url": "/i18n/en.json" },
"zh-CN":{ "url": "/i18n/zh-CN.json" }
}
}/i18n/en.json
{
"hello": "Hello, {name}!",
"save.ok": "Saved."
}/i18n/zh-CN.json
{
"hello": "你好,{name}!",
"save.ok": "已保存。"
}生产环境建议为
manifest.json与每个{locale}.json配置 ETag/Last-Modified,返回 304 提升效率(下方有示例)。
2) 前端使用(任意组件/布局)
<script lang="ts">
import { createI18nHot, HttpJsonAdapter } from "@sv-kit/i18n-hot";
const i18n = createI18nHot({
adapter: new HttpJsonAdapter({ manifestURL: "/i18n/manifest.json" }),
initialLocale: "zh-CN",
fallbackLocale: "en",
preload: ["en"], // 可选:预加载这些语言
autoRefreshMs: 60_000 // 可选:自动每 60s 检查更新
});
// 切换语言
function toEN() { i18n.setLocale("en"); }
function toZH() { i18n.setLocale("zh-CN"); }
</script>
<h1>{i18n.t("hello", { name: "骚哥" })}</h1>
<button on:click={toEN}>EN</button>
<button on:click={toZH}>中文</button>Manifest 与缓存机制
Manifest(上面的
manifest.json)声明版本号和每个语言文件的 URL;本包会将:
- Manifest 的 ETag、
- 各 locale 的 ETag、
- 已拉取的词典
dicts
一并存进
localStorage(默认键前缀i18n:hot),下次启动先用缓存,再按 ETag 差量拉取。
修改
manifest.json的version或响应头ETag→ 触发热更新。 语言文件若返回304 Not Modified→ 不复写本地词典;返回200 OK→ 合并新词条。
API 参考
import { createI18nHot, HttpJsonAdapter } from "@sv-kit/i18n-hot";
import type { Dict, I18nAdapter, I18nHotOptions, Snapshot } from "@sv-kit/i18n-hot";createI18nHot(options: I18nHotOptions)
Options
adapter: I18nAdapter:后端适配器(默认提供HttpJsonAdapter)initialLocale: string:初始语言fallbackLocale?: string:回退语言persistKey?: string:本地缓存键前缀(默认"i18n:hot")preload?: string[]:预加载这些语言autoRefreshMs?: number:自动刷新间隔(ms)warnPlaceholders?: boolean:占位符差异警告(默认true)
返回对象(I18nPublicAPI)
- Stores
locale: Readable<string>dict: Readable<Dict>version: Readable<string | null>
- Ops
t(key: string, params?: Record<string, unknown>): stringsetLocale(locale: string): Promise<void>hasKey(key: string): boolean
- Lifecycle
refresh(): Promise<void>:手动重新检查 manifest 并刷新当前语言startAutoRefresh(ms?: number): void/stopAutoRefresh(): void
- SSR / 持久化
snapshot(): Snapshot:导出当前快照(含版本/ETag/字典)hydrate(snap: Snapshot): void:在客户端复水
默认适配器:HttpJsonAdapter
import { HttpJsonAdapter } from "@sv-kit/i18n-hot";
const adapter = new HttpJsonAdapter({
manifestURL: "/i18n/manifest.json",
headers: { Authorization: "Bearer <token>" } // 可选
});getManifest(etag?):携带If-None-Match请求 manifest;304→changed=falsegetLocale(locale, url, etag?):同理;304→ 返回空 dict,调用方跳过合并
自定义适配器(示意)
对接 OSS/COS/CDN/GraphQL 皆可,形状如下:
export interface I18nAdapter {
name: string;
getManifest(etag?: string | null): Promise<{ changed: boolean; manifest?: Manifest; etag?: string | null }>;
getLocale(locale: string, url: string, etag?: string | null): Promise<{ dict: Dict; etag?: string | null }>;
}SvelteKit SSR 复水(可选)
+layout.server.ts
import { createI18nHot, HttpJsonAdapter } from "@sv-kit/i18n-hot";
export const load = async () => {
const i18n = createI18nHot({
adapter: new HttpJsonAdapter({ manifestURL: "https://example.com/i18n/manifest.json" }),
initialLocale: "zh-CN",
fallbackLocale: "en"
});
// 服务器端可以可选地预拉一次快照(也可以不拉,保持懒加载)
const snap = i18n.snapshot();
return { i18nSnap: snap };
};+layout.svelte
<script lang="ts">
import { createI18nHot, HttpJsonAdapter } from "@sv-kit/i18n-hot";
export let data;
const i18n = createI18nHot({
adapter: new HttpJsonAdapter({ manifestURL: "/i18n/manifest.json" }),
initialLocale: "zh-CN",
fallbackLocale: "en"
});
// 客户端复水
i18n.hydrate(data.i18nSnap);
</script>
<slot />ETag / 304 服务端示例
Node(Fastify)返回 manifest 与 ETag
app.get("/i18n/manifest.json", async (req, reply) => {
const body = JSON.stringify({
version: "2025-09-16T12:00:00Z",
locales: { "en": { url: "/i18n/en.json" }, "zh-CN": { url: "/i18n/zh-CN.json" } }
});
const etag = `"m-${Buffer.from(body).toString("hex").slice(0,16)}"`;
if (req.headers["if-none-match"] === etag) {
return reply.code(304).send();
}
return reply.header("ETag", etag).type("application/json").send(body);
});Cloudflare Workers / 任意 CDN
- 将
manifest.json与{locale}.json上传到对象存储/Pages; - 配置 强制 ETag 与 Cache-Control;
- 前端会用
If-None-Match自动走 304。
占位符最佳实践
- 文本里使用
{name}、{order.id}这类占位符; - 新版词条若与旧版同 key,但占位符集合不同,本包会
console.warn(可通过warnPlaceholders:false关闭); - 参数可嵌套:
i18n.t("helloOrder", { order: { id: 42 }}),匹配{order.id}。
常见问题(FAQ)
Q:首次进入为什么没立即看到翻译?
A:本包采用懒加载策略。createI18nHot 会在 microtask 发起初始化加载;如需“首屏必有”,可在 SSR preload 或入口处调用 await i18n.setLocale('xx') 后再渲染。
Q:如何手动触发热更新?
A:调用 await i18n.refresh();或设置 autoRefreshMs 自动轮询。
Q:多语言切换时闪烁?
A:配置 fallbackLocale 并先加载 fallback 再加载当前(本包已这样做);也可以在 UI 层为缺失 key 提供占位样式。
Q:能把词典放到 GraphQL / 私有 API 吗?
A:可以,写一个自定义 I18nAdapter 就行(上面接口参考)。
版本 & 许可
- Node ≥ 18,Svelte 4/5
- License:MIT
Roadmap:批量预取 + LRU 缓存;基于 Service Worker 的离线包;多命名空间(分模块翻译)支持;占位符类型提示(TS 模板类型)。
