feat(reader): Word Wise inline vocabulary hints (#4589)

* feat(reader): Word Wise — inline native-language vocabulary hints

Kindle-style Word Wise: a short native-language gloss renders above difficult words
as you read (always-on ruby), gated by a CEFR vocabulary-level slider (A1–C2);
tapping a glossed word opens the existing dictionary.

- Pipeline: CEFR→frequency-rank difficulty, inflection-aware gloss index, pure
  offset-aware planner (EN regex + jieba for CJK).
- Rendering: <ruby cfi-skip>…<rt cfi-inert> injected per occurrence — CFI-transparent
  (verified), so highlights/bookmarks/progress are unaffected; kept out of TTS word
  offsets and find-in-book.
- Delivery: gloss packs are version-controlled in data/wordwise/, mirrored to R2, and
  downloaded on demand into local storage (sha-verified, single-flight) when enabled.
- Settings: a Word Wise sub-page under Settings → Language (enable, level, hint
  language, per-pack download/manage, auto-download toggle).
- Build tooling: scripts/build-wordwise-data.mjs (ECDICT / CC-CEDICT+HSK / WikDict +
  FrequencyWords, with lemmatization) and scripts/sync-wordwise-r2.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* data(wordwise): bundled gloss packs + manifest + attribution

13 frequency-trimmed gloss packs (en↔中文 + es/fr/de/pt/it/ru↔en, ~19 MB) generated
by build-wordwise-data.mjs from ECDICT (MIT), CC-CEDICT + HSK, and WikDict +
FrequencyWords (CC-BY-SA). Source of truth, mirrored to the CDN via `pnpm wordwise:sync`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-15 13:56:00 +08:00
committed by GitHub
parent 51fede1a0d
commit 4908245042
90 changed files with 6573 additions and 125 deletions
@@ -1,5 +1,6 @@
import { DOUBLE_CLICK_INTERVAL_THRESHOLD_MS, LONG_HOLD_THRESHOLD } from '@/services/constants';
import { eventDispatcher } from '@/utils/event';
import { findGlossWord } from '@/app/reader/utils/wordwiseRuby';
let lastClickTime = 0;
let longHoldTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -224,6 +225,15 @@ export const handleClick = (
return;
}
// Word Wise: tapping a glossed word looks it up in the dictionary. Checked
// after the drag/long-hold guards so only a clean single tap triggers it.
const glossWord = findGlossWord(element);
if (glossWord) {
const ruby = element?.closest('ruby.ww-gloss') ?? null;
eventDispatcher.dispatch('wordwise-dictionary', { bookKey, element: ruby, word: glossWord });
return;
}
window.postMessage(
{
type: 'iframe-single-click',