refactor(wordlens): rename "Word Wise" to "Word Lens" (#4633)

"Word Wise" is a Kindle trademark, so rename the inline-gloss feature to
"Word Lens" throughout the product.

- User-facing strings → "Word Lens" across all 34 locales; brand translated
  for Chinese (zh-CN 单词透镜, zh-TW 單詞透鏡) and German (Word-Lens-Daten).
- Code identifiers: WordWise→WordLens, wordWise→wordLens, WORD_WISE→WORD_LENS.
- Files/dirs: src/services/wordwise→wordlens, WordWisePanel→WordLensPanel,
  wordwise{Ruby,Section}.ts, build/sync scripts, test dirs/fixtures,
  data/wordwise→data/wordlens.
- Storage paths: CDN base, R2 key, on-device cache dir, WORDLENS_R2_BUCKET env,
  pnpm wordlens:{manifest,sync}. manifest.json is path-agnostic so its
  sha256/bytes stay valid (verified).
- biome.json: point the formatter-ignore at data/wordlens so the generated
  one-line gloss packs aren't pretty-printed on commit.

Migration notes:
- Re-run `pnpm wordlens:sync` to upload packs to cdn.readest.com/wordlens/.
- Persisted view-settings keys renamed (wordWiseEnabled/Level/HintLang and
  wordWiseAutoDownload) — saved values reset to defaults once on upgrade.
- Cached packs under the old Data/wordwise/ orphan (harmless re-download).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-18 01:09:29 +08:00
committed by GitHub
parent 9d0c5dc524
commit c2ac207945
90 changed files with 539 additions and 2716 deletions
@@ -1,6 +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';
import { findGlossWord } from '@/app/reader/utils/wordlensRuby';
let lastClickTime = 0;
let longHoldTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -248,12 +248,12 @@ export const handleClick = (
return;
}
// Word Wise: tapping a glossed word looks it up in the dictionary. Checked
// Word Lens: 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 });
eventDispatcher.dispatch('wordlens-dictionary', { bookKey, element: ruby, word: glossWord });
return;
}
@@ -1,4 +1,4 @@
import type { GlossOccurrence } from '@/services/wordwise/types';
import type { GlossOccurrence } from '@/services/wordlens/types';
const GLOSS_CLASS = 'ww-gloss';
@@ -1,14 +1,14 @@
import type { ViewSettings } from '@/types/book';
import type { AppService } from '@/types/system';
import type { ProgressHandler } from '@/utils/transfer';
import { canTokenizeSource, getRankCutoff } from '@/services/wordwise/difficulty';
import { loadGlossIndex } from '@/services/wordwise/glossPacks';
import { planGlosses } from '@/services/wordwise/planner';
import { buildSectionTextModel, applyGlosses, clearGlosses } from '@/app/reader/utils/wordwiseRuby';
import { canTokenizeSource, getRankCutoff } from '@/services/wordlens/difficulty';
import { loadGlossIndex } from '@/services/wordlens/glossPacks';
import { planGlosses } from '@/services/wordlens/planner';
import { buildSectionTextModel, applyGlosses, clearGlosses } from '@/app/reader/utils/wordlensRuby';
import { cutZh, isJiebaReady, initJieba } from '@/utils/jieba';
/** Normalize a book language tag to its 2-letter base source code, or null. */
export const toWordWiseSource = (lang?: string | null): string | null => {
export const toWordLensSource = (lang?: string | null): string | null => {
if (!lang) return null;
const base = lang.toLowerCase().split('-')[0];
return base || null;
@@ -22,7 +22,7 @@ interface RefreshContext {
/**
* Whether the reader may silently download an uncached pack. Threaded to
* loadGlossIndex ensurePack; when false an uncached pack yields no glosses
* (the user downloads it explicitly from the Word Wise sub-page).
* (the user downloads it explicitly from the Word Lens sub-page).
*/
allowDownload?: boolean;
onProgress?: ProgressHandler;
@@ -49,10 +49,10 @@ export const refreshSectionGlosses = async (
const myGen = (refreshGen.get(doc) ?? 0) + 1;
refreshGen.set(doc, myGen);
clearGlosses(doc);
if (!viewSettings.wordWiseEnabled) return;
const source = toWordWiseSource(ctx.bookLang);
if (!viewSettings.wordLensEnabled) return;
const source = toWordLensSource(ctx.bookLang);
if (!source || !canTokenizeSource(source)) return;
const hint = (viewSettings.wordWiseHintLang || ctx.appLang).toLowerCase().split('-')[0] || '';
const hint = (viewSettings.wordLensHintLang || ctx.appLang).toLowerCase().split('-')[0] || '';
if (!hint || hint === source) return; // no self-gloss
const index = await loadGlossIndex(ctx.appService, source, hint, {
onProgress: ctx.onProgress,
@@ -67,11 +67,11 @@ export const refreshSectionGlosses = async (
const model = buildSectionTextModel(doc);
const occ = planGlosses(model.text, index, {
sourceLang: source,
rankCutoff: getRankCutoff(source, viewSettings.wordWiseLevel),
rankCutoff: getRankCutoff(source, viewSettings.wordLensLevel),
cutZh: source === 'zh' ? cutZh : undefined,
});
if (occ.length) applyGlosses(doc, model, occ);
} catch (err) {
console.warn('[wordwise] refresh failed', err);
console.warn('[wordlens] refresh failed', err);
}
};