weblatorjs
v0.0.3
Published
Framework-agnostic web page translation engine: DOM scanning, lazy rendering, caching. You inject the translate function; the engine handles the rest.
Downloads
437
Maintainers
Readme
weblatorjs
一个纯粹、零依赖、轻量的网页文本翻译引擎。它只负责与网页相关的全部工作——DOM 扫描、元素分段、懒加载渲染、双语/仅译文排版、缓存与增量翻译,不内置任何翻译服务。真正把文本翻译成目标语言的能力,由你通过构造函数注入的 translate 函数提供(可以对接任意翻译 API,如 Google、DeepL、自建大模型服务等)。
特性
- 零运行时依赖 · 轻量:不引入任何第三方运行时依赖,仅使用浏览器原生 DOM API 实现;构建产物为单个 ESM 文件(约 43 KB,未压缩)。装它不会给你的依赖树带来任何传递依赖。
- 翻译服务无关:引擎不绑定任何供应商,你注入一个
translate函数即可。 - 懒加载翻译:基于
IntersectionObserver,仅翻译进入视口(含提前 100px 预加载)的段落。 - 双语 / 仅译文两种模式:
dual(原文 + 译文对照)、translation(替换为译文)。 - 智能 DOM 分段:正确处理行内元素、块级元素、
<br>换行、<pre>/ 类 pre 代码块、Shadow DOM、placeholder、<option>等。 - 增量翻译:内置
MutationObserver,页面动态新增的内容会自动被翻译。 - 请求批量合并:跨元素的翻译请求在 100ms 窗口内自动合并、按批发送,减少请求数。
- 缓存:翻译结果按
<源语言>_<目标语言>缓存,默认落地localStorage,可自定义存储适配器与持久化触发策略。 - 事件驱动:
start/progress/complete/error/languageDetected事件,方便驱动进度 UI。 - 站点规则:可注入按 URL / 选择器命中的站点适配规则,覆盖分段与排除逻辑。
- 同语言跳过:可选注入
detectLanguage,当检测到文本已是目标语言时跳过翻译。 - 样式解耦:SDK 只注入结构性最小样式,译文外观(主题、颜色、字体)由调用方经 CSS 变量或
styleOptions完全自定义。
安装
pnpm add weblatorjs
# 或 npm install weblatorjs / yarn add weblatorjs引擎运行在浏览器环境(依赖 document、IntersectionObserver、MutationObserver 等 DOM API)。
快速开始
引擎唯一必填的配置是 translate 函数:引擎把待译文本 texts 发给你,你调用自己的翻译服务,把等长的译文数组返回。
import { TranslationEngine } from 'weblatorjs'
const engine = new TranslationEngine({
// 必填:接入你自己的翻译服务
async translate({ texts, from, to, signal }) {
const res = await fetch('https://your-api.example.com/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ texts, from, to }),
signal, // 透传取消信号,restorePage() 时可中断在途请求
})
const data = await res.json()
return {
texts: data.translations, // 必须与入参 texts 等长;缺失项引擎会按原文降级
}
},
})
// 开始翻译当前页面
await engine.translatePage({
translationMode: 'dual', // 'dual' 双语 | 'translation' 仅译文
sourceLanguage: 'en',
targetLanguage: 'zh-CN',
})
// 恢复原文
engine.restorePage()核心概念:注入的 translate 函数
这是接入的关键。引擎负责决定「翻译什么」,你负责「怎么翻译」。
type TranslateFn = (req: TranslateRequest) => Promise<TranslateResponse>
interface TranslateRequest {
texts: string[] // 引擎收集好的一批待译文本
from: string // 源语言
to: string // 目标语言
signal?: AbortSignal // 可选,取消信号
}
interface TranslateResponse {
texts: string[] // 与入参 texts 等长的译文数组;缺失项引擎按原文降级
tid?: string // 可选,翻译批次 id(用于打点)
errno?: number // 可选,非 0 视为失败
}约定:
- 返回的
texts应与入参texts等长且顺序对应;某一项缺失时,引擎会保留原文。 - 引擎已做批量合并——同一
from/to的多个请求会在 100ms 内合并,单批最多6段文本(maxTextGroupLengthPerRequest)。你的translate每次收到的就是合并后的一批。 translate内部抛错会被引擎捕获并降级为原文,不会中断整页翻译。- 引擎会传入
signal(AbortSignal)。把它透传给fetch后,调用restorePage()会abort()所有在途请求;不透传则请求照常完成,只是其结果不再被渲染。
API
new TranslationEngine(config)
interface TranslationEngineConfig {
/** 必填:调用方实现的翻译函数 */
translate: TranslateFn
/** 可选:站点适配规则,默认 [] */
rules?: SiteRule[]
/** 可选:缓存分桶 key,默认 'default' */
cacheNamespace?: string
/** 可选:语言检测,用于「同目标语言跳过翻译」判断 */
detectLanguage?: (text: string) => Promise<string | undefined>
/** 可选:缓存存储适配器,默认 localStorage */
storage?: StorageAdapter
/** 可选:缓存持久化触发策略,默认 CountBasedFlushPolicy(5)(每 5 次写入落地一次) */
flushPolicy?: FlushPolicy
/** 可选:初始用户设置 */
userSettings?: Partial<UserSettings>
/** 可选:样式定制项,用于自定义译文主题 / 加载指示器 CSS */
styleOptions?: StyleOptions
}实例方法
| 方法 | 说明 |
| --- | --- |
| translatePage(options) | 翻译当前页面(含 <title>)。翻译中重复调用会被忽略。 |
| restorePage(options?) | 恢复原文,清理样式、观察器与标记,并持久化缓存。 |
| on(event, handler) | 订阅引擎事件,返回取消订阅函数。 |
| off(event, handler) | 取消订阅。 |
| updateUserSettings(settings) | 增量合并更新用户设置。 |
| clearCache() | 清除翻译缓存。 |
| observeNewNodes(nodes) | 手动把一批新节点纳入翻译流程(增量翻译)。 |
TranslationOptions(传给 translatePage)
interface TranslationOptions {
translationMode?: 'dual' | 'translation'
targetLanguage?: string
sourceLanguage?: string
from?: string // 触发来源(打点用)
position?: string // 触发位置(打点用)
}未在
options中指定的语言 / 模式,会回退到构造时的userSettings。
事件
interface EngineEvents {
start: { totalElements: number }
progress: { translated: number; total: number }
complete: { success: boolean }
error: { message: string }
languageDetected: { lang: string; confidence: 'high' | 'low' }
}const off = engine.on('progress', ({ translated, total }) => {
console.log(`翻译进度: ${translated}/${total}`)
})
engine.on('complete', ({ success }) => console.log('完成', success))
engine.on('error', ({ message }) => console.error(message))
// off() // 取消订阅翻译模式
通过 TRANSLATION_MODES 常量或字符串字面量使用:
import { TRANSLATION_MODES } from 'weblatorjs'
TRANSLATION_MODES.DUAL // 'dual' 双语对照,保留原文并追加译文
TRANSLATION_MODES.TRANSLATION_ONLY // 'translation' 仅译文,直接替换内容自定义译文样式
SDK 不内置任何主题外观(仅保留结构性样式与一个 none 主题),译文的颜色、字体、主题装饰完全交给调用方。按从轻到重提供三档能力:
1. CSS 变量(推荐 · 零 JS 配置)
最常见的「改颜色 / 改字体」需求无需任何 JS 配置,也无需重新翻译——在你自己的样式表里覆盖 :root 上的变量即可,浏览器即时级联生效:
:root {
--wtjs-translation-color: #2563eb; /* 译文颜色,默认 inherit */
--wtjs-translation-font: Georgia, serif; /* 译文字体,默认 inherit */
}也可用 JS 动态切换(如跟随主题色):
document.documentElement.style.setProperty('--wtjs-translation-color', '#c00')2. styleOptions.themeCss(自定义主题)
需要「带装饰的完整主题」(下划线、高亮、边框等)时,通过 themeCss 注入任意 .wtjs-translated-content-theme-<name> 规则,再用 userSettings.translationTheme 选中该主题:
const engine = new TranslationEngine({
translate,
styleOptions: {
themeCss: `
.wtjs-translated-content-theme-brand {
color: #2563eb;
border-bottom: 2px solid #2563eb;
}
.wtjs-translated-content-theme-highlight {
background: #fde047;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
`,
},
userSettings: {
translationMode: 'dual',
translationTheme: 'brand', // 命中上面定义的 .wtjs-translated-content-theme-brand
},
})引擎会给每个译文元素加上
wtjs-translated-content与wtjs-translated-content-theme-<translationTheme>两个类;你的 CSS 命中后者即可。切换主题只需updateUserSettings({ translationTheme })后重译。
3. styleOptions.loadingIndicatorCss(自定义加载指示器)
翻译进行中,引擎会插入一个命中 .wtjs-loading-indicator 的占位元素。默认是一个旋转 spinner;传入 loadingIndicatorCss 可完全替换它(含 keyframes 自定义责任在调用方):
const engine = new TranslationEngine({
translate,
styleOptions: {
loadingIndicatorCss: `
.wtjs-loading-indicator {
display: inline-block;
width: 12px; height: 12px;
border-radius: 50%;
background: #2563eb;
animation: my-pulse 0.8s ease-in-out infinite;
}
@keyframes my-pulse { 0%,100% { opacity: 0.3 } 50% { opacity: 1 } }
`,
},
})StyleOptions 接口:
interface StyleOptions {
/** 自定义主题 CSS,追加到 SDK 主题 <style>;定义 `.wtjs-translated-content-theme-<name>` 规则后经 translationTheme 选中 */
themeCss?: string
/** 覆盖加载指示器 CSS,命中 `.wtjs-loading-indicator`;不传则用内置默认 spinner */
loadingIndicatorCss?: string
}引擎注入的类名 / CSS 变量约定
| 名称 | 类型 | 说明 |
| --- | --- | --- |
| .wtjs-translated-content | 类 | 每个译文节点都有,作为通用样式挂载点 |
| .wtjs-translated-content-theme-<name> | 类 | 主题类,<name> 来自 translationTheme |
| .wtjs-loading-indicator | 类 | 翻译中占位指示器 |
| --wtjs-translation-color | CSS 变量 | 译文颜色,默认 inherit |
| --wtjs-translation-font | CSS 变量 | 译文字体,默认 inherit |
自定义缓存存储
默认使用 localStorage。可注入自定义存储适配器(如 IndexedDB、Chrome 扩展 chrome.storage 等):
import type { StorageAdapter } from 'weblatorjs'
const storage: StorageAdapter = {
async get(key, defaultValue) {
/* 读取并返回值,无则返回 defaultValue */
return defaultValue
},
async set(key, value) {
/* 持久化 */
},
}
const engine = new TranslationEngine({ translate, storage })自定义缓存持久化策略
内存缓存并非每次写入都落地存储,而是由 FlushPolicy 决定「何时把内存缓存写回 Storage」。默认使用 CountBasedFlushPolicy(5)——每累积 5 次写入触发一次持久化,把频繁的内存写与磁盘 I/O 解耦。
可注入自定义策略(例如调整阈值,或实现按时间 / 按空闲触发的策略):
import { TranslationEngine, CountBasedFlushPolicy } from 'weblatorjs'
import type { FlushPolicy } from 'weblatorjs'
// 调整默认策略的阈值:每 3 次写入落地一次
const engine = new TranslationEngine({
translate,
flushPolicy: new CountBasedFlushPolicy(3),
})
// 或实现完全自定义的策略
const everyWrite: FlushPolicy = {
shouldFlush() {
return true // 每次写入都落地
},
}FlushPolicy 接口:
interface FlushPolicy {
/** 每次写入调用一次;返回 true 表示本次写入后应触发一次持久化 */
shouldFlush(): boolean
}同语言跳过翻译
注入 detectLanguage 后,若某段文本检测到的语言与目标语言一致,引擎会跳过该段,避免「中译中」之类的无效翻译。未注入时不做此判断,保持翻译。
const engine = new TranslationEngine({
translate,
async detectLanguage(text) {
return await myDetector(text) // 返回语言代码,如 'zh-CN'
},
})站点适配规则
通过 rules 注入按站点命中的规则,命中后会与内置通用规则合并,用于定制分段、排除、样式注入等。命中判定按顺序短路:
matchAll: true—— 显式全站命中(推荐用于「全站默认」规则)matches—— 按当前页 URL 做 glob 匹配(*匹配任意字符任意次,其它字符字面量)selectorMatches—— 按页面是否存在某 CSS 选择器匹配
三者中任一满足即算命中;matches 与 selectorMatches 独立取或;matchAll: true 短路,同规则上的 matches / selectorMatches 不再检查。
import type { SiteRule } from 'weblatorjs'
const rules: SiteRule[] = [
{
id: 'example',
matches: ['*.example.com'],
// 在通用规则基础上追加要排除的选择器
'excludeSelectors.add': ['.no-translate'],
// 直接注入作用域样式
globalStyles: { '.foo': 'height:auto!important;' },
// 动态内容额外监听的选择器
mutationAdditionalSelectors: ['.comments'],
},
]
const engine = new TranslationEngine({ translate, rules })选择器类字段支持三种变体:
field—— 覆盖通用规则的该字段field.add—— 在通用规则基础上追加field.remove—— 从最终结果中移除
additionalSelectors 的语义是「始终追加」,因此裸键、.add、.remove 三种写法都是在基线上叠加,不会覆盖通用规则的默认。
完整可用字段见类型定义 SiteRule。
全站生效的自定义规则
若要让一条自定义规则在所有页面都生效(例如统一排除自家 UI 容器、给所有站点追加同一批选择器),使用 matchAll: true。它会被自动推到所有 matches / selectorMatches 命中规则之后合并,作为"最外层兜底"叠加,不会被具体站点规则的裸键冲掉:
const rules: SiteRule[] = [
// 全站默认:所有页面都排除自家 UI 容器
{
id: 'global-defaults',
matchAll: true,
'excludeSelectors.add': ['#my-app-root', '.my-widget'],
},
// 具体站点规则:在 example.com 上再额外排除 .ads
{
id: 'example',
matches: ['*.example.com'],
'excludeSelectors.add': ['.ads'],
},
]在 example.com 上,两条规则都命中,最终 excludeSelectors 会包含通用规则的全部内置项 + .ads + #my-app-root + .my-widget(matchAll 规则被自动推到末位,见下文)。
注意事项:
- 全站命中一律用
matchAll: true,不要用matches: '*'/'.*':后者是普通的 glob 字符串,即便碰巧能匹配大多数 URL,也无法覆盖chrome://newtab、about:blank这类没有典型 host/path 的场景。matchAll是显式 flag,语义无歧义。 matchAll: true规则总是末位合并:无论它在rules数组里声明的位置,命中后都会被自动排到所有matches/selectorMatches命中规则之后合并。这样"全站兜底"永远压在最外层,具体站点规则里的裸键(如excludeSelectors: [...])不会把全站规则的.add冲掉——比如{ matchAll: true, 'excludeSelectors.add': ['#my-panel'] }一定会在站点规则合并完之后追加,#my-panel稳定保留。多条matchAll之间按声明顺序追加。- 优先使用
.add/.remove,避免用裸键:裸键(如excludeSelectors: [...])是整体覆盖语义,会把当前基线(通用规则或上一轮合并结果)里的所有项清空。绝大多数场景应该用excludeSelectors.add。 - 同类规则(都是
matches或都是matchAll)之间,数组声明顺序即合并顺序。跨类不生效——matchAll一律靠后。 matches是 host 锚定 的:'github.com'命中https://github.com/foo与https://www.github.com/foo,但不会误命中https://raw.githubusercontent.com/x/github.com.md这类把 pattern 塞在路径里的 URL。要匹配特定路径请写成'github.com/foo/*'。
完整示例
import { TranslationEngine, TRANSLATION_MODES } from 'weblatorjs'
const engine = new TranslationEngine({
async translate({ texts, from, to, signal }) {
const res = await fetch('/api/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q: texts, source: from, target: to }),
signal,
})
const { data } = await res.json()
return { texts: data }
},
cacheNamespace: 'my-app',
userSettings: {
translationMode: TRANSLATION_MODES.DUAL,
sourceLanguage: 'en',
targetLanguage: 'zh-CN',
},
})
engine.on('progress', ({ translated, total }) =>
console.log(`${translated}/${total}`),
)
// 触发翻译
document.querySelector('#translate-btn')?.addEventListener('click', () => {
engine.translatePage({ from: 'manual' })
})
// 恢复原文
document.querySelector('#restore-btn')?.addEventListener('click', () => {
engine.restorePage()
})导出
import {
TranslationEngine, // 引擎类
TRANSLATION_MODES, // 翻译模式常量
CountBasedFlushPolicy, // 计数式缓存持久化策略(默认阈值 5)
} from 'weblatorjs'
import type {
TranslateRequest,
TranslateResponse,
TranslateFn,
EngineEvents,
StorageAdapter,
FlushPolicy,
SiteRule,
TranslationEngineConfig,
TranslationOptions,
UserSettings,
} from 'weblatorjs'License
MIT
