forked from akai/readest
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:
@@ -41,7 +41,7 @@ import {
|
||||
import { applyScrollableStyle, applyTableTouchScroll } from '@/utils/scrollable';
|
||||
import { mountAdditionalFonts, mountCustomFont } from '@/styles/fonts';
|
||||
import { layoutWarichu, relayoutWarichu } from '@/utils/warichu';
|
||||
import { refreshSectionGlosses } from '@/app/reader/utils/wordwiseSection';
|
||||
import { refreshSectionGlosses } from '@/app/reader/utils/wordlensSection';
|
||||
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
|
||||
import { getIndexFromCfi } from '@/utils/cfi';
|
||||
import { useUICSS } from '@/hooks/useUICSS';
|
||||
@@ -411,30 +411,30 @@ const FoliateViewer: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
// Build the Word Wise refresh context: gate silent auto-download on the global
|
||||
// Build the Word Lens refresh context: gate silent auto-download on the global
|
||||
// toggle AND a best-effort metered-connection check, and show a single
|
||||
// "Downloading…" toast on the first progress tick (the per-percent progress
|
||||
// lives in the Word Wise settings panel). `wordWiseToastShownRef` de-dupes the
|
||||
// lives in the Word Lens settings panel). `wordLensToastShownRef` de-dupes the
|
||||
// toast across the multiple section docs a refresh pass touches.
|
||||
const wordWiseToastShownRef = useRef(false);
|
||||
const buildWordWiseCtx = (bookLang?: string | null) => {
|
||||
const wordLensToastShownRef = useRef(false);
|
||||
const buildWordLensCtx = (bookLang?: string | null) => {
|
||||
// Read the live setting (not the first-render `settings` snapshot closed over
|
||||
// by the empty-deps `stabilizedHandler`) so toggling Auto-download mid-session
|
||||
// takes effect on the next section refresh.
|
||||
const liveSettings = useSettingsStore.getState().settings;
|
||||
const allowDownload =
|
||||
(liveSettings.globalReadSettings.wordWiseAutoDownload ?? true) && !isMetered();
|
||||
(liveSettings.globalReadSettings.wordLensAutoDownload ?? true) && !isMetered();
|
||||
return {
|
||||
appService: appService!,
|
||||
bookLang,
|
||||
appLang: getLocale().split('-')[0] || 'en',
|
||||
allowDownload,
|
||||
onProgress: () => {
|
||||
if (wordWiseToastShownRef.current) return;
|
||||
wordWiseToastShownRef.current = true;
|
||||
if (wordLensToastShownRef.current) return;
|
||||
wordLensToastShownRef.current = true;
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Downloading Word Wise data…'),
|
||||
message: _('Downloading Word Lens data…'),
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -447,7 +447,7 @@ const FoliateViewer: React.FC<{
|
||||
const vs = getViewSettings(bookKey);
|
||||
const bookLang = getBookData(bookKey)?.book?.primaryLanguage;
|
||||
// Fixed-layout (pre-paginated) books have no reflow room; injecting ruby
|
||||
// would overflow their fixed boxes, so skip Word Wise glosses there.
|
||||
// would overflow their fixed boxes, so skip Word Lens glosses there.
|
||||
const isFixedLayout = bookDoc.rendition?.layout === 'pre-paginated';
|
||||
for (const { doc } of contents) {
|
||||
if (doc) {
|
||||
@@ -459,7 +459,7 @@ const FoliateViewer: React.FC<{
|
||||
relayoutWarichu(doc);
|
||||
}
|
||||
if (vs && appService && !isFixedLayout) {
|
||||
void refreshSectionGlosses(doc, vs, buildWordWiseCtx(bookLang));
|
||||
void refreshSectionGlosses(doc, vs, buildWordLensCtx(bookLang));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -834,12 +834,12 @@ const FoliateViewer: React.FC<{
|
||||
if (isFixedLayout) return;
|
||||
// A settings change is the moment a fresh download may start; let the
|
||||
// one-time "Downloading…" toast fire again for it.
|
||||
wordWiseToastShownRef.current = false;
|
||||
wordLensToastShownRef.current = false;
|
||||
for (const { doc } of contents) {
|
||||
if (doc) void refreshSectionGlosses(doc, vs, buildWordWiseCtx(bookLang));
|
||||
if (doc) void refreshSectionGlosses(doc, vs, buildWordLensCtx(bookLang));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [viewSettings?.wordWiseEnabled, viewSettings?.wordWiseLevel, viewSettings?.wordWiseHintLang]);
|
||||
}, [viewSettings?.wordLensEnabled, viewSettings?.wordLensLevel, viewSettings?.wordLensHintLang]);
|
||||
|
||||
useEffect(() => {
|
||||
const mountCustomFonts = async () => {
|
||||
|
||||
@@ -174,10 +174,10 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
// (Android long-press selects text via selectionchange before touchend). The
|
||||
// pending action runs on touchend so popups don't open under an active touch.
|
||||
const deferredQuickActionRef = useRef(createDeferredActionState());
|
||||
// Set when a Word Wise gloss tap synthesizes a selection so the
|
||||
// Set when a Word Lens gloss tap synthesizes a selection so the
|
||||
// selection-change effect opens the dictionary popup instead of the
|
||||
// annotation toolbar. Cleared as soon as it's consumed.
|
||||
const pendingWordWiseDictRef = useRef(false);
|
||||
const pendingWordLensDictRef = useRef(false);
|
||||
|
||||
const showingPopup =
|
||||
showAnnotPopup || showDictionaryPopup || showDeepLPopup || showProofreadPopup;
|
||||
@@ -560,12 +560,12 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
|
||||
useFoliateEvents(view, { onLoad, onCreateOverlay, onDrawAnnotation, onShowAnnotation });
|
||||
|
||||
// Word Wise: open the dictionary popup for a tapped glossed word. The tap is
|
||||
// Word Lens: open the dictionary popup for a tapped glossed word. The tap is
|
||||
// detected in the iframe click handler (iframeEventHandlers.ts), which sends
|
||||
// the gloss <ruby> element here. We synthesize a selection over the base word
|
||||
// (excluding the <rt> hint) so the existing dictionary popup positions itself.
|
||||
useEffect(() => {
|
||||
const handleWordWiseDictionary = (event: CustomEvent) => {
|
||||
const handleWordLensDictionary = (event: CustomEvent) => {
|
||||
const { element, word } = event.detail as { element: Element | null; word: string };
|
||||
if (event.detail?.bookKey !== bookKey || !element || !word) return;
|
||||
// Read the view fresh: this handler is registered once (deps [bookKey]) and
|
||||
@@ -584,7 +584,7 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
return;
|
||||
}
|
||||
const text = range.toString().trim() || word;
|
||||
pendingWordWiseDictRef.current = true;
|
||||
pendingWordLensDictRef.current = true;
|
||||
setSelection({
|
||||
key: bookKey,
|
||||
text,
|
||||
@@ -594,9 +594,9 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
page: index + 1,
|
||||
});
|
||||
};
|
||||
eventDispatcher.on('wordwise-dictionary', handleWordWiseDictionary);
|
||||
eventDispatcher.on('wordlens-dictionary', handleWordLensDictionary);
|
||||
return () => {
|
||||
eventDispatcher.off('wordwise-dictionary', handleWordWiseDictionary);
|
||||
eventDispatcher.off('wordlens-dictionary', handleWordLensDictionary);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKey]);
|
||||
@@ -817,11 +817,11 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
useEffect(() => {
|
||||
setHighlightOptionsVisible(!!(selection && selection.annotated));
|
||||
if (selection && selection.text.trim().length > 0) {
|
||||
// Read-and-reset the Word Wise dictionary flag up front so it can never
|
||||
// Read-and-reset the Word Lens dictionary flag up front so it can never
|
||||
// stick to a later selection if an early return below fires (e.g. a gloss
|
||||
// tap whose synthesized range yields an off-frame/zero position).
|
||||
const wantWordWiseDict = pendingWordWiseDictRef.current;
|
||||
pendingWordWiseDictRef.current = false;
|
||||
const wantWordLensDict = pendingWordLensDictRef.current;
|
||||
pendingWordLensDictRef.current = false;
|
||||
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
|
||||
if (!gridFrame) return;
|
||||
const rect = gridFrame.getBoundingClientRect();
|
||||
@@ -866,7 +866,7 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
setTrianglePosition(triangPos);
|
||||
|
||||
const { enableAnnotationQuickActions, annotationQuickAction } = viewSettings;
|
||||
if (wantWordWiseDict) {
|
||||
if (wantWordLensDict) {
|
||||
setShowAnnotPopup(false);
|
||||
setShowDictionaryPopup(true);
|
||||
} else if (enableAnnotationQuickActions && annotationQuickAction && isTextSelected.current) {
|
||||
|
||||
@@ -247,7 +247,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
|
||||
query: term,
|
||||
acceptNode: createRejectFilter({
|
||||
tags: primaryLang.startsWith('ja') ? ['rt'] : [],
|
||||
// Word Wise gloss text (<rt cfi-inert>) is injected, non-book content.
|
||||
// Word Lens gloss text (<rt cfi-inert>) is injected, non-book content.
|
||||
attributes: ['cfi-inert'],
|
||||
}),
|
||||
results: cachedResults,
|
||||
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import type { GlossOccurrence } from '@/services/wordwise/types';
|
||||
import type { GlossOccurrence } from '@/services/wordlens/types';
|
||||
|
||||
const GLOSS_CLASS = 'ww-gloss';
|
||||
|
||||
+11
-11
@@ -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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user