feat(proofread): add an option for whole word replacement, closes #2934 (#2938)

This commit is contained in:
Huang Xin
2026-01-13 15:35:57 +01:00
committed by GitHub
parent aaee04c290
commit 434a44e62c
33 changed files with 217 additions and 67 deletions
@@ -880,5 +880,9 @@
"Exported successfully": "تم التصدير بنجاح",
"Book exported successfully.": "تم تصدير الكتاب بنجاح.",
"Failed to export the book.": "فشل تصدير الكتاب.",
"Export Book": "تصدير الكتاب"
"Export Book": "تصدير الكتاب",
"Whole word:": "كلمة كاملة:",
"Error": "خطأ",
"Unable to load the article. Try searching directly on {{link}}.": "تعذر تحميل المقال. حاول البحث مباشرة على {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "تعذر تحميل الكلمة. حاول البحث مباشرة على {{link}}."
}
@@ -840,5 +840,9 @@
"Exported successfully": "সফলভাবে রপ্তানি হয়েছে",
"Book exported successfully.": "বইটি সফলভাবে রপ্তানি হয়েছে।",
"Failed to export the book.": "বই রপ্তানি করতে ব্যর্থ।",
"Export Book": "বই রপ্তানি করুন"
"Export Book": "বই রপ্তানি করুন",
"Whole word:": "সম্পূর্ণ শব্দ:",
"Error": "ত্রুটি",
"Unable to load the article. Try searching directly on {{link}}.": "নিবন্ধ লোড করতে অক্ষম। সরাসরি {{link}} এ অনুসন্ধান করার চেষ্টা করুন।",
"Unable to load the word. Try searching directly on {{link}}.": "শব্দ লোড করতে অক্ষম। সরাসরি {{link}} এ অনুসন্ধান করার চেষ্টা করুন।"
}
@@ -830,5 +830,9 @@
"Exported successfully": "ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
"Book exported successfully.": "དཔེ་དེབ་ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
"Failed to export the book.": "དཔེ་དེབ་ཕྱིར་འདོན་མི་ཐུབ།",
"Export Book": "དཔེ་དེབ་ཕྱིར་འདོན།"
"Export Book": "དཔེ་དེབ་ཕྱིར་འདོན།",
"Whole word:": "ཚིག་གྲུབ་ཆ་ཚང་:",
"Error": "ནོར་འཁྲུལ།",
"Unable to load the article. Try searching directly on {{link}}.": "རྩོམ་ཡིག་མངོན་མི་ཐུབ། {{link}} ཐད་ཀར་འཚོལ་བ་བྱོས།",
"Unable to load the word. Try searching directly on {{link}}.": "ཚིག་མངོན་མི་ཐུབ། {{link}} ཐད་ཀར་འཚོལ་བ་བྱོས།"
}
@@ -840,5 +840,9 @@
"Exported successfully": "Erfolgreich exportiert",
"Book exported successfully.": "Buch erfolgreich exportiert.",
"Failed to export the book.": "Buch konnte nicht exportiert werden.",
"Export Book": "Buch exportieren"
"Export Book": "Buch exportieren",
"Whole word:": "Ganzes Wort:",
"Error": "Fehler",
"Unable to load the article. Try searching directly on {{link}}.": "Artikel kann nicht geladen werden. Versuchen Sie die direkte Suche auf {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Wort kann nicht geladen werden. Versuchen Sie die direkte Suche auf {{link}}."
}
@@ -840,5 +840,9 @@
"Exported successfully": "Εξαγωγή επιτυχής",
"Book exported successfully.": "Το βιβλίο εξήχθη επιτυχώς.",
"Failed to export the book.": "Αποτυχία εξαγωγής του βιβλίου.",
"Export Book": "Εξαγωγή βιβλίου"
"Export Book": "Εξαγωγή βιβλίου",
"Whole word:": "Ολόκληρη λέξη:",
"Error": "Σφάλμα",
"Unable to load the article. Try searching directly on {{link}}.": "Αδυναμία φόρτωσης του άρθρου. Δοκιμάστε να αναζητήσετε απευθείας στο {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Αδυναμία φόρτωσης της λέξης. Δοκιμάστε να αναζητήσετε απευθείας στο {{link}}."
}
@@ -850,5 +850,9 @@
"Exported successfully": "Exportado con éxito",
"Book exported successfully.": "Libro exportado con éxito.",
"Failed to export the book.": "Error al exportar el libro.",
"Export Book": "Exportar libro"
"Export Book": "Exportar libro",
"Whole word:": "Palabra completa:",
"Error": "Error",
"Unable to load the article. Try searching directly on {{link}}.": "No se puede cargar el artículo. Intenta buscar directamente en {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "No se puede cargar la palabra. Intenta buscar directamente en {{link}}."
}
@@ -840,5 +840,9 @@
"Exported successfully": "صادرات موفق",
"Book exported successfully.": "کتاب با موفقیت صادر شد.",
"Failed to export the book.": "صادر کردن کتاب ناموفق بود.",
"Export Book": "صادر کردن کتاب"
"Export Book": "صادر کردن کتاب",
"Whole word:": "کلمه کامل:",
"Error": "خطا",
"Unable to load the article. Try searching directly on {{link}}.": "بارگذاری مقاله امکان‌پذیر نیست. سعی کنید مستقیماً در {{link}} جستجو کنید.",
"Unable to load the word. Try searching directly on {{link}}.": "بارگذاری کلمه امکان‌پذیر نیست. سعی کنید مستقیماً در {{link}} جستجو کنید."
}
@@ -850,5 +850,9 @@
"Exported successfully": "Exporté avec succès",
"Book exported successfully.": "Livre exporté avec succès.",
"Failed to export the book.": "Échec de l'exportation du livre.",
"Export Book": "Exporter le livre"
"Export Book": "Exporter le livre",
"Whole word:": "Mot entier :",
"Error": "Erreur",
"Unable to load the article. Try searching directly on {{link}}.": "Impossible de charger l'article. Essayez de rechercher directement sur {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Impossible de charger le mot. Essayez de rechercher directement sur {{link}}."
}
@@ -840,5 +840,9 @@
"Exported successfully": "सफलतापूर्वक निर्यात किया गया",
"Book exported successfully.": "पुस्तक सफलतापूर्वक निर्यात की गई।",
"Failed to export the book.": "पुस्तक निर्यात करने में विफल।",
"Export Book": "पुस्तक निर्यात करें"
"Export Book": "पुस्तक निर्यात करें",
"Whole word:": "पूरा शब्द:",
"Error": "त्रुटि",
"Unable to load the article. Try searching directly on {{link}}.": "लेख लोड करने में असमर्थ। सीधे {{link}} पर खोजने का प्रयास करें।",
"Unable to load the word. Try searching directly on {{link}}.": "शब्द लोड करने में असमर्थ। सीधे {{link}} पर खोजने का प्रयास करें।"
}
@@ -830,5 +830,9 @@
"Exported successfully": "Berhasil diekspor",
"Book exported successfully.": "Buku berhasil diekspor.",
"Failed to export the book.": "Gagal mengekspor buku.",
"Export Book": "Ekspor Buku"
"Export Book": "Ekspor Buku",
"Whole word:": "Kata utuh:",
"Error": "Kesalahan",
"Unable to load the article. Try searching directly on {{link}}.": "Tidak dapat memuat artikel. Coba cari langsung di {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Tidak dapat memuat kata. Coba cari langsung di {{link}}."
}
@@ -850,5 +850,9 @@
"Exported successfully": "Esportato con successo",
"Book exported successfully.": "Libro esportato con successo.",
"Failed to export the book.": "Impossibile esportare il libro.",
"Export Book": "Esporta libro"
"Export Book": "Esporta libro",
"Whole word:": "Parola intera:",
"Error": "Errore",
"Unable to load the article. Try searching directly on {{link}}.": "Impossibile caricare l'articolo. Prova a cercare direttamente su {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Impossibile caricare la parola. Prova a cercare direttamente su {{link}}."
}
@@ -830,5 +830,9 @@
"Exported successfully": "エクスポート成功",
"Book exported successfully.": "書籍のエクスポートに成功しました。",
"Failed to export the book.": "書籍のエクスポートに失敗しました。",
"Export Book": "書籍をエクスポート"
"Export Book": "書籍をエクスポート",
"Whole word:": "単語全体:",
"Error": "エラー",
"Unable to load the article. Try searching directly on {{link}}.": "記事を読み込めません。{{link}}で直接検索してみてください。",
"Unable to load the word. Try searching directly on {{link}}.": "単語を読み込めません。{{link}}で直接検索してみてください。"
}
@@ -830,5 +830,9 @@
"Exported successfully": "내보내기 성공",
"Book exported successfully.": "책을 성공적으로 내보냈습니다.",
"Failed to export the book.": "책 내보내기에 실패했습니다.",
"Export Book": "책 내보내기"
"Export Book": "책 내보내기",
"Whole word:": "전체 단어:",
"Error": "오류",
"Unable to load the article. Try searching directly on {{link}}.": "문서를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요.",
"Unable to load the word. Try searching directly on {{link}}.": "단어를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요."
}
@@ -830,5 +830,9 @@
"Exported successfully": "Berjaya dieksport",
"Book exported successfully.": "Buku berjaya dieksport.",
"Failed to export the book.": "Gagal mengeksport buku.",
"Export Book": "Eksport Buku"
"Export Book": "Eksport Buku",
"Whole word:": "Perkataan penuh:",
"Error": "Ralat",
"Unable to load the article. Try searching directly on {{link}}.": "Tidak dapat memuatkan artikel. Cuba cari terus di {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Tidak dapat memuatkan perkataan. Cuba cari terus di {{link}}."
}
@@ -840,5 +840,9 @@
"Exported successfully": "Succesvol geëxporteerd",
"Book exported successfully.": "Boek succesvol geëxporteerd.",
"Failed to export the book.": "Exporteren van het boek mislukt.",
"Export Book": "Boek exporteren"
"Export Book": "Boek exporteren",
"Whole word:": "Heel woord:",
"Error": "Fout",
"Unable to load the article. Try searching directly on {{link}}.": "Kan het artikel niet laden. Probeer direct te zoeken op {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Kan het woord niet laden. Probeer direct te zoeken op {{link}}."
}
@@ -860,5 +860,9 @@
"Exported successfully": "Wyeksportowano pomyślnie",
"Book exported successfully.": "Książka została wyeksportowana pomyślnie.",
"Failed to export the book.": "Nie udało się wyeksportować książki.",
"Export Book": "Eksportuj książkę"
"Export Book": "Eksportuj książkę",
"Whole word:": "Całe słowo:",
"Error": "Błąd",
"Unable to load the article. Try searching directly on {{link}}.": "Nie można załadować artykułu. Spróbuj wyszukać bezpośrednio na {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Nie można załadować słowa. Spróbuj wyszukać bezpośrednio na {{link}}."
}
@@ -850,5 +850,9 @@
"Exported successfully": "Exportado com sucesso",
"Book exported successfully.": "Livro exportado com sucesso.",
"Failed to export the book.": "Falha ao exportar o livro.",
"Export Book": "Exportar livro"
"Export Book": "Exportar livro",
"Whole word:": "Palavra inteira:",
"Error": "Erro",
"Unable to load the article. Try searching directly on {{link}}.": "Não foi possível carregar o artigo. Tente pesquisar diretamente em {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Não foi possível carregar a palavra. Tente pesquisar diretamente em {{link}}."
}
@@ -860,5 +860,9 @@
"Exported successfully": "Успешно экспортировано",
"Book exported successfully.": "Книга успешно экспортирована.",
"Failed to export the book.": "Не удалось экспортировать книгу.",
"Export Book": "Экспорт книги"
"Export Book": "Экспорт книги",
"Whole word:": "Слово целиком:",
"Error": "Ошибка",
"Unable to load the article. Try searching directly on {{link}}.": "Не удалось загрузить статью. Попробуйте искать напрямую на {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Не удалось загрузить слово. Попробуйте искать напрямую на {{link}}."
}
@@ -840,5 +840,9 @@
"Exported successfully": "සාර්ථකව අපනයනය කරන ලදී",
"Book exported successfully.": "පොත සාර්ථකව අපනයනය කරන ලදී.",
"Failed to export the book.": "පොත අපනයනය කිරීමට අසමත් විය.",
"Export Book": "පොත අපනයනය කරන්න"
"Export Book": "පොත අපනයනය කරන්න",
"Whole word:": "සම්පූර්ණ වචනය:",
"Error": "දෝෂය",
"Unable to load the article. Try searching directly on {{link}}.": "ලිපිය පූරණය කළ නොහැක. {{link}} හි කෙලින්ම සෙවීමට උත්සාහ කරන්න.",
"Unable to load the word. Try searching directly on {{link}}.": "වචනය පූරණය කළ නොහැක. {{link}} හි කෙලින්ම සෙවීමට උත්සාහ කරන්න."
}
@@ -840,5 +840,9 @@
"Exported successfully": "Exporterad",
"Book exported successfully.": "Boken har exporterats.",
"Failed to export the book.": "Det gick inte att exportera boken.",
"Export Book": "Exportera bok"
"Export Book": "Exportera bok",
"Whole word:": "Hela ordet:",
"Error": "Fel",
"Unable to load the article. Try searching directly on {{link}}.": "Kan inte ladda artikeln. Försök söka direkt på {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Kan inte ladda ordet. Försök söka direkt på {{link}}."
}
@@ -840,5 +840,9 @@
"Exported successfully": "ஏற்றுமதி வெற்றி",
"Book exported successfully.": "புத்தகம் வெற்றிகரமாக ஏற்றுமதி செய்யப்பட்டது.",
"Failed to export the book.": "புத்தகத்தை ஏற்றுமதி செய்ய இயலவில்லை.",
"Export Book": "புத்தகத்தை ஏற்றுமதி செய்"
"Export Book": "புத்தகத்தை ஏற்றுமதி செய்",
"Whole word:": "முழு சொல்:",
"Error": "பிழை",
"Unable to load the article. Try searching directly on {{link}}.": "கட்டுரையை ஏற்ற முடியவில்லை. நேரடியாக {{link}} இல் தேடவும்.",
"Unable to load the word. Try searching directly on {{link}}.": "சொல்லை ஏற்ற முடியவில்லை. நேரடியாக {{link}} இல் தேடவும்."
}
@@ -830,5 +830,9 @@
"Exported successfully": "ส่งออกสำเร็จ",
"Book exported successfully.": "ส่งออกหนังสือสำเร็จ",
"Failed to export the book.": "ส่งออกหนังสือล้มเหลว",
"Export Book": "ส่งออกหนังสือ"
"Export Book": "ส่งออกหนังสือ",
"Whole word:": "คำทั้งคำ:",
"Error": "ข้อผิดพลาด",
"Unable to load the article. Try searching directly on {{link}}.": "ไม่สามารถโหลดบทความได้ ลองค้นหาโดยตรงที่ {{link}}",
"Unable to load the word. Try searching directly on {{link}}.": "ไม่สามารถโหลดคำได้ ลองค้นหาโดยตรงที่ {{link}}"
}
@@ -840,5 +840,9 @@
"Exported successfully": "Başarıyla dışa aktarıldı",
"Book exported successfully.": "Kitap başarıyla dışa aktarıldı.",
"Failed to export the book.": "Kitap dışa aktarılamadı.",
"Export Book": "Kitabı Dışa Aktar"
"Export Book": "Kitabı Dışa Aktar",
"Whole word:": "Tam kelime:",
"Error": "Hata",
"Unable to load the article. Try searching directly on {{link}}.": "Makale yüklenemiyor. Doğrudan {{link}} üzerinde arama yapmayı deneyin.",
"Unable to load the word. Try searching directly on {{link}}.": "Kelime yüklenemiyor. Doğrudan {{link}} üzerinde arama yapmayı deneyin."
}
@@ -860,5 +860,9 @@
"Exported successfully": "Успішно експортовано",
"Book exported successfully.": "Книгу успішно експортовано.",
"Failed to export the book.": "Не вдалося експортувати книгу.",
"Export Book": "Експортувати книгу"
"Export Book": "Експортувати книгу",
"Whole word:": "Ціле слово:",
"Error": "Помилка",
"Unable to load the article. Try searching directly on {{link}}.": "Не вдалося завантажити статтю. Спробуйте шукати безпосередньо на {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Не вдалося завантажити слово. Спробуйте шукати безпосередньо на {{link}}."
}
@@ -830,5 +830,9 @@
"Exported successfully": "Xuất thành công",
"Book exported successfully.": "Sách đã được xuất thành công.",
"Failed to export the book.": "Xuất sách thất bại.",
"Export Book": "Xuất sách"
"Export Book": "Xuất sách",
"Whole word:": "Toàn bộ từ:",
"Error": "Lỗi",
"Unable to load the article. Try searching directly on {{link}}.": "Không thể tải bài viết. Hãy thử tìm kiếm trực tiếp trên {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Không thể tải từ. Hãy thử tìm kiếm trực tiếp trên {{link}}."
}
@@ -830,5 +830,9 @@
"Exported successfully": "导出成功",
"Book exported successfully.": "书籍导出成功。",
"Failed to export the book.": "书籍导出失败。",
"Export Book": "导出书籍"
"Export Book": "导出书籍",
"Whole word:": "全词匹配:",
"Error": "错误",
"Unable to load the article. Try searching directly on {{link}}.": "无法加载文章。请直接在 {{link}} 上搜索。",
"Unable to load the word. Try searching directly on {{link}}.": "无法加载词汇。请直接在 {{link}} 上搜索。"
}
@@ -830,5 +830,9 @@
"Exported successfully": "匯出成功",
"Book exported successfully.": "書籍匯出成功。",
"Failed to export the book.": "書籍匯出失敗。",
"Export Book": "匯出書籍"
"Export Book": "匯出書籍",
"Whole word:": "全詞匹配:",
"Error": "錯誤",
"Unable to load the article. Try searching directly on {{link}}.": "無法載入文章。請直接在 {{link}} 上搜尋。",
"Unable to load the word. Try searching directly on {{link}}.": "無法載入詞彙。請直接在 {{link}} 上搜尋。"
}
@@ -8,7 +8,7 @@ import { CreateProofreadRuleOptions, useProofreadStore } from '@/store/proofread
import { ProofreadScope } from '@/types/book';
import { eventDispatcher } from '@/utils/event';
import { Position, TextSelection } from '@/utils/sel';
import { isWholeWord } from '@/utils/word';
import { isPunctuationOnly, isWholeWord } from '@/utils/word';
import Select from '@/components/Select';
import Popup from '@/components/Popup';
@@ -41,11 +41,17 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
const [replacementText, setReplacementText] = useState('');
const [caseSensitive, setCaseSensitive] = useState(true);
const [wholeWord, setWholeWord] = useState(!isPunctuationOnly(selection?.text || ''));
const [scope, setScope] = useState<ProofreadScope>('selection');
const inputRef = useRef<HTMLInputElement>(null);
useAutoFocus<HTMLInputElement>({ ref: inputRef });
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
setReplacementText(text);
};
const handleScopeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setScope(event.target.value as ProofreadScope);
};
@@ -58,10 +64,10 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
if (range) {
const isValidWholeWord = isWholeWord(range, selection?.text || '');
if (!isValidWholeWord) {
if (wholeWord && !isValidWholeWord) {
eventDispatcher.dispatch('toast', {
type: 'warning',
message: `Cannot replace "${selection.text}" - please select a complete word. Partial word selections (like "and" in "England" or "errand") are not supported.`,
message: 'Please select a whole word or uncheck the "Whole word" option.',
timeout: 5000,
});
return;
@@ -73,7 +79,7 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
range.insertNode(textNode);
}
const options = {
const options: CreateProofreadRuleOptions = {
scope,
pattern: selection.text,
replacement: replacementText.trim(),
@@ -82,7 +88,7 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
isRegex: false,
enabled: true,
caseSensitive,
wholeWord: true,
wholeWord: wholeWord,
};
onConfirm?.(options);
@@ -111,14 +117,14 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
width={popupWidth}
minHeight={popupHeight}
position={position}
className='flex flex-col justify-between rounded-lg bg-gray-700 text-gray-400'
className='not-eink:text-gray-400 flex flex-col justify-between rounded-lg bg-gray-700'
triangleClassName='text-gray-700'
onDismiss={onDismiss}
>
<div className='flex flex-col gap-6 p-4'>
<div className='flex gap-1 text-xs text-gray-400'>
<div className='not-eink:text-gray-400 flex gap-1 text-xs'>
<span>{_('Selected text:')}</span>
<span className='line-clamp-1 select-text break-words text-yellow-300'>
<span className='not-eink:text-yellow-300 line-clamp-1 select-text break-words font-medium'>
&quot;{selection?.text || ''}&quot;
</span>
</div>
@@ -131,20 +137,23 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
ref={inputRef}
type='text'
value={replacementText}
onChange={(e) => setReplacementText(e.target.value)}
onChange={handleInputChange}
onKeyDown={(e) => {
if (e.key === 'Enter' && replacementText.trim()) {
handleApply();
}
}}
placeholder={_('Enter text...')}
className='w-full flex-1 rounded-md bg-gray-600 p-2 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-0'
className={clsx(
'w-full flex-1 rounded-md p-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-0',
'not-eink:bg-gray-600 not-eink:text-white eink:border eink:border-base-content',
)}
/>
<button
onClick={handleApply}
disabled={!replacementText.trim()}
className={clsx(
'btn btn-sm btn-ghost text-blue-600 disabled:text-gray-600',
'btn btn-sm btn-ghost btn-primary disabled:text-base-content/75 text-blue-600 disabled:opacity-75',
'bg-transparent hover:bg-transparent disabled:bg-transparent',
)}
>
@@ -153,14 +162,14 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
</div>
</div>
<div className='flex items-center justify-between gap-4 p-4'>
<label className='flex max-w-[30%] cursor-pointer items-center gap-2'>
<div className='flex items-center gap-8 p-4'>
<label className='flex cursor-pointer items-center gap-2'>
<span className='line-clamp-1 text-xs' title={_('Case sensitive:')}>
{_('Case sensitive:')}
</span>
<input
type='checkbox'
className='toggle toggle-sm bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
className='toggle toggle-sm not-eink:bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
style={
{
'--tglbg': '#4B5563',
@@ -171,17 +180,33 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
/>
</label>
<div className='flex max-w-[65%] flex-1 items-center justify-between gap-2'>
<label htmlFor='scope-select' className='line-clamp-1 text-xs' title={_('Scope:')}>
{_('Scope:')}
</label>
<Select
className='max-w-[50%] bg-gray-600 text-white'
value={scope}
onChange={handleScopeChange}
options={scopeOptions}
<label className='flex cursor-pointer items-center gap-2'>
<span className='line-clamp-1 text-xs' title={_('Whole word:')}>
{_('Whole word:')}
</span>
<input
type='checkbox'
className='toggle toggle-sm bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
style={
{
'--tglbg': '#4B5563',
} as React.CSSProperties
}
checked={wholeWord}
onChange={(e) => setWholeWord(e.target.checked)}
/>
</div>
</label>
</div>
<div className='flex flex-1 items-center justify-between gap-2 p-4'>
<label htmlFor='scope-select' className='line-clamp-1 text-xs' title={_('Scope:')}>
{_('Scope:')}
</label>
<Select
className='not-eink:bg-gray-600 eink:bg-base-100 not-eink:text-white max-w-[85%]'
value={scope}
onChange={handleScopeChange}
options={scopeOptions}
/>
</div>
</Popup>
</div>
@@ -141,7 +141,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
minHeight={popupHeight}
maxHeight={720}
position={position}
className='grid h-full select-text grid-rows-[1fr,auto,1fr] bg-gray-600 text-white'
className='not-eink:text-white grid h-full select-text grid-rows-[1fr,auto,1fr] bg-gray-600'
triangleClassName='text-gray-600'
onDismiss={onDismiss}
>
@@ -149,7 +149,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
<div className='mb-2 flex items-center justify-between'>
<h1 className='text-sm font-normal'>{_('Original Text')}</h1>
<Select
className='bg-gray-600 text-white/75'
className='not-eink:bg-gray-600 not-eink:text-white eink:bg-base-100'
value={sourceLang}
onChange={handleSourceLangChange}
options={[
@@ -167,7 +167,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
]}
/>
</div>
<p className='text-base text-white/90'>{text}</p>
<p className='not-eink:text-white/90 text-base'>{text}</p>
</div>
<div className='mx-4 flex-shrink-0 border-t border-gray-500/30'></div>
@@ -176,7 +176,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
<div className='mb-2 flex items-center justify-between'>
<h2 className='text-sm font-normal'>{_('Translated Text')}</h2>
<Select
className='bg-gray-600 text-white/75'
className='not-eink:bg-gray-600 not-eink:text-white eink:bg-base-100'
value={targetLang}
onChange={handleTargetLangChange}
options={[
@@ -194,7 +194,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
{error ? (
<p className='text-base text-red-600'>{error}</p>
) : (
<p className='text-base text-white/90'>
<p className='not-eink:text-white/90 text-base'>
{translation || _('No translation available.')}
</p>
)}
@@ -211,7 +211,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
})}
</div>
<Select
className='bg-gray-600 text-white/75'
className='not-eink:bg-gray-600 not-eink:text-white eink:bg-base-100'
value={provider}
onChange={handleProviderChange}
options={providers.map(({ name: value, label }) => ({ value, label }))}
@@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react';
import Popup from '@/components/Popup';
import { Position } from '@/utils/sel';
import { useTranslation } from '@/hooks/useTranslation';
interface WikipediaPopupProps {
text: string;
@@ -21,6 +22,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
popupHeight,
onDismiss,
}) => {
const _ = useTranslation();
const isLoading = useRef(false);
useEffect(() => {
@@ -85,12 +87,14 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
const errorDiv = document.createElement('div');
const h1 = document.createElement('h1');
h1.innerText = 'Error';
h1.innerText = _('Error');
const errorMsg = document.createElement('p');
errorMsg.innerHTML = `Unable to load the article. Try searching directly on <a href="https://${language}.wikipedia.org/w/index.php?search=${encodeURIComponent(
query,
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wikipedia</a>.`;
errorMsg.innerHTML = _('Unable to load the article. Try searching directly on {{link}}.', {
link: `<a href="https://${language}.wikipedia.org/w/index.php?search=${encodeURIComponent(
query,
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wikipedia</a>`,
});
errorDiv.append(h1, errorMsg);
main.appendChild(errorDiv);
@@ -101,7 +105,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
const bookLang = typeof lang === 'string' ? lang : lang?.[0];
const langCode = bookLang ? bookLang.split('-')[0]! : 'en';
fetchSummary(text, langCode);
}, [text, lang]);
}, [_, text, lang]);
return (
<div>
@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { Position } from '@/utils/sel';
import { useTranslation } from '@/hooks/useTranslation';
import Popup from '@/components/Popup';
type Definition = {
@@ -32,6 +33,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
popupHeight,
onDismiss,
}) => {
const _ = useTranslation();
const [lookupWord, setLookupWord] = useState(word);
const isLookingUp = useRef(false);
@@ -141,13 +143,15 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
'flex flex-col items-center justify-center w-full h-full text-center absolute inset-0';
const h1 = document.createElement('h1');
h1.innerText = 'Error';
h1.innerText = _('Error');
h1.className = 'text-lg font-bold';
const p = document.createElement('p');
p.innerHTML = `Unable to load the word. Try searching directly on <a href="https://en.wiktionary.org/w/index.php?search=${encodeURIComponent(
word,
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wiktionary</a>.`;
p.innerHTML = _('Unable to load the word. Try searching directly on {{link}}.', {
link: `<a href="https://en.wiktionary.org/w/index.php?search=${encodeURIComponent(
word,
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wiktionary</a>`,
});
div.append(h1, p);
main.append(div);
@@ -156,7 +160,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
const langCode = typeof lang === 'string' ? lang : lang?.[0];
fetchDefinitions(lookupWord, langCode);
}, [lookupWord, lang]);
}, [_, lookupWord, lang]);
return (
<div>
+4
View File
@@ -595,6 +595,10 @@ foliate-fxl {
background-color: theme('colors.base-100') !important;
}
[data-eink="true"] input[type="checkbox"] {
--tglbg: theme('colors.base-100') !important;
}
.no-transitions * {
transition: none !important;
}
+5
View File
@@ -30,6 +30,11 @@ export const isWholeWord = (range: Range, selectedText: string): boolean => {
}
};
export const isPunctuationOnly = (text: string): boolean => {
const punctuationPattern = /^[\p{P}\p{S}\s]+$/u;
return punctuationPattern.test(text);
};
// Helper to get characters before and after the selection
const getBoundaryChars = (range: Range): { charBefore: string; charAfter: string } => {
let charBefore = '';