npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

引擎运行在浏览器环境(依赖 documentIntersectionObserverMutationObserver 等 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 内部抛错会被引擎捕获并降级为原文,不会中断整页翻译。
  • 引擎会传入 signalAbortSignal)。把它透传给 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-contentwtjs-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 注入按站点命中的规则,命中后会与内置通用规则合并,用于定制分段、排除、样式注入等。命中判定按顺序短路:

  1. matchAll: true —— 显式全站命中(推荐用于「全站默认」规则)
  2. matches —— 按当前页 URL 做 glob 匹配(* 匹配任意字符任意次,其它字符字面量)
  3. selectorMatches —— 按页面是否存在某 CSS 选择器匹配

三者中任一满足即算命中;matchesselectorMatches 独立取或;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-widgetmatchAll 规则被自动推到末位,见下文)。

注意事项:

  • 全站命中一律用 matchAll: true,不要用 matches: '*' / '.*':后者是普通的 glob 字符串,即便碰巧能匹配大多数 URL,也无法覆盖 chrome://newtababout: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 一律靠后。
  • matcheshost 锚定 的:'github.com' 命中 https://github.com/foohttps://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