diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 063f5626..42518038 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1306,7 +1306,6 @@ "Importing…": "جاري الاستيراد…", "Import Dictionary": "استيراد قاموس", "No dictionaries available.": "لا تتوفر قواميس.", - "Drag the handle on the left to reorder.": "اسحب المقبض الموجود على اليسار لإعادة الترتيب.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "تحتاج حزم StarDict إلى ملفات ‎.ifo و‎.idx و‎.dict.dz (الـ‎.syn اختياري).", "MDict bundles use .mdx files; companion .mdd files are optional.": "حزم MDict تستخدم ملفات ‎.mdx؛ ملفات ‎.mdd المرافقة اختيارية.", "Select all the bundle files together when importing.": "حدد جميع ملفات الحزمة معًا عند الاستيراد.", @@ -1409,5 +1408,18 @@ "Includes your reading progress": "يتضمن تقدّم قراءتك", "Synced {{time}}": "تمت المزامنة {{time}}", "Sync Info": "معلومات المزامنة", - "Last Synced": "آخر مزامنة" + "Last Synced": "آخر مزامنة", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "تحتاج حزم DICT إلى ملف ‎.index‎ وملف ‎.dict.dz‎.", + "Slob bundles need a .slob file.": "تحتاج حزم Slob إلى ملف ‎.slob‎.", + "URL template must start with http(s):// and contain %WORD%.": "يجب أن يبدأ قالب URL بـ http(s):// وأن يحتوي على %WORD%.", + "Web": "ويب", + "Add Web Search": "إضافة بحث ويب", + "Edit Web Search": "تعديل بحث الويب", + "e.g. Google": "مثال: Google", + "URL Template": "قالب URL", + "Use %WORD% where the looked-up word should appear.": "استخدم %WORD% حيث يجب أن تظهر الكلمة المبحوث عنها.", + "Open the search result in your browser:": "افتح نتيجة البحث في متصفحك:", + "Open in {{name}}": "افتح في {{name}}" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index ef42983a..3629cf9f 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "আমদানি হচ্ছে…", "Import Dictionary": "অভিধান আমদানি করুন", "No dictionaries available.": "কোনো অভিধান উপলব্ধ নেই।", - "Drag the handle on the left to reorder.": "পুনঃক্রম করতে বামদিকের হ্যান্ডেলটি টানুন।", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict বান্ডলে .ifo, .idx এবং .dict.dz ফাইল প্রয়োজন (.syn ঐচ্ছিক)।", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict বান্ডলে .mdx ফাইল ব্যবহৃত হয়; সঙ্গী .mdd ফাইল ঐচ্ছিক।", "Select all the bundle files together when importing.": "আমদানির সময় বান্ডলের সব ফাইল একসঙ্গে নির্বাচন করুন।", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "আপনার পড়ার অগ্রগতি অন্তর্ভুক্ত", "Synced {{time}}": "{{time}} সিঙ্ক হয়েছে", "Sync Info": "সিঙ্ক তথ্য", - "Last Synced": "শেষ সিঙ্ক" + "Last Synced": "শেষ সিঙ্ক", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT বান্ডেলের জন্য একটি .index ফাইল এবং একটি .dict.dz ফাইল প্রয়োজন।", + "Slob bundles need a .slob file.": "Slob বান্ডেলের জন্য একটি .slob ফাইল প্রয়োজন।", + "URL template must start with http(s):// and contain %WORD%.": "URL টেমপ্লেটটি অবশ্যই http(s):// দিয়ে শুরু হতে হবে এবং %WORD% থাকতে হবে।", + "Web": "ওয়েব", + "Add Web Search": "ওয়েব অনুসন্ধান যোগ করুন", + "Edit Web Search": "ওয়েব অনুসন্ধান সম্পাদনা করুন", + "e.g. Google": "যেমন Google", + "URL Template": "URL টেমপ্লেট", + "Use %WORD% where the looked-up word should appear.": "যেখানে অনুসন্ধান করা শব্দ উপস্থিত হওয়া উচিত সেখানে %WORD% ব্যবহার করুন।", + "Open the search result in your browser:": "আপনার ব্রাউজারে অনুসন্ধান ফলাফল খুলুন:", + "Open in {{name}}": "{{name}}-এ খুলুন" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index e5dc7bae..235af913 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "ནང་འདྲེན་བྱེད་བཞིན་པ…", "Import Dictionary": "ཚིག་མཛོད་ནང་འདྲེན།", "No dictionaries available.": "ཚིག་མཛོད་གང་ཡང་མི་འདུག།", - "Drag the handle on the left to reorder.": "གོ་རིམ་སྒྱུར་བར་གཡོན་ཕྱོགས་ཀྱི་འཛིན་ཕུང་འཐེན།", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict ཡོ་བྱད་ལ་ .ifo དང་ .idx, .dict.dz ཡིག་ཆ་ཁག་དགོས་པ་ཡིན། (.syn ནི་འདེམས་འཁྱོངས་ཡིན།)", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict ཡོ་བྱད་ཀྱིས་ .mdx ཡིག་ཆ་སྤྱོད། མཉམ་འགྲོགས་ .mdd ཡིག་ཆ་ནི་འདེམས་འཁྱོངས་ཡིན།", "Select all the bundle files together when importing.": "ནང་འདྲེན་སྐབས་ཡོ་བྱད་ཡིག་ཆ་ཡོངས་མཉམ་དུ་འདེམས་རོགས།", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "ཁྱེད་ཀྱི་ཀློག་འདོན་གྱི་ཡར་ཐོན་འདུས།", "Synced {{time}}": "{{time}}མཉམ་སྒྲིག་བྱས་པ", "Sync Info": "མཉམ་སྒྲིག་གྱི་ཆ་འཕྲིན", - "Last Synced": "མཐའ་མའི་མཉམ་སྒྲིག" + "Last Synced": "མཐའ་མའི་མཉམ་སྒྲིག", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT ཕུང་པོར་ .index ཡིག་ཆ་དང་ .dict.dz ཡིག་ཆ་གཉིས་ཀ་དགོས།", + "Slob bundles need a .slob file.": "Slob ཕུང་པོར་ .slob ཡིག་ཆ་ཞིག་དགོས།", + "URL template must start with http(s):// and contain %WORD%.": "URL ཕྱི་མོ་ http(s):// གིས་འགོ་འཛུགས་དགོས་ལ་ %WORD% ཡོད་དགོས།", + "Web": "དྲ་རྒྱ", + "Add Web Search": "དྲ་རྒྱའི་འཚོལ་བཤེར་སྣོན།", + "Edit Web Search": "དྲ་རྒྱའི་འཚོལ་བཤེར་རྩོམ་སྒྲིག", + "e.g. Google": "དཔེར་ན། Google", + "URL Template": "URL ཕྱི་མོ", + "Use %WORD% where the looked-up word should appear.": "འཚོལ་བཤེར་བྱས་པའི་ཚིག་འཆར་ས་གནས་སུ་ %WORD% བཀོལ་སྤྱོད་གནང་རོགས།", + "Open the search result in your browser:": "ཁྱེད་ཀྱི་བརྡ་འཚོལ་མཁོ་ཆས་ལ་འཚོལ་བཤེར་གྲུབ་འབྲས་ཁ་ཕྱེ:", + "Open in {{name}}": "{{name}} ནང་ཁ་ཕྱེ།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 13b10df2..66f4dd11 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "Wird importiert …", "Import Dictionary": "Wörterbuch importieren", "No dictionaries available.": "Keine Wörterbücher verfügbar.", - "Drag the handle on the left to reorder.": "Ziehe am Griff links, um die Reihenfolge zu ändern.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict-Bundles benötigen .ifo-, .idx- und .dict.dz-Dateien (.syn optional).", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict-Bundles verwenden .mdx-Dateien; begleitende .mdd-Dateien sind optional.", "Select all the bundle files together when importing.": "Wähle beim Import alle Bundle-Dateien zusammen aus.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "Enthält deinen Lesefortschritt", "Synced {{time}}": "Synchronisiert {{time}}", "Sync Info": "Synchronisierungs-Info", - "Last Synced": "Zuletzt synchronisiert" + "Last Synced": "Zuletzt synchronisiert", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT-Bundles benötigen eine .index-Datei und eine .dict.dz-Datei.", + "Slob bundles need a .slob file.": "Slob-Bundles benötigen eine .slob-Datei.", + "URL template must start with http(s):// and contain %WORD%.": "URL-Vorlage muss mit http(s):// beginnen und %WORD% enthalten.", + "Web": "Web", + "Add Web Search": "Websuche hinzufügen", + "Edit Web Search": "Websuche bearbeiten", + "e.g. Google": "z. B. Google", + "URL Template": "URL-Vorlage", + "Use %WORD% where the looked-up word should appear.": "Verwenden Sie %WORD%, wo das gesuchte Wort erscheinen soll.", + "Open the search result in your browser:": "Öffne das Suchergebnis in deinem Browser:", + "Open in {{name}}": "In {{name}} öffnen" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 271c1f65..cbdcc068 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "Εισαγωγή…", "Import Dictionary": "Εισαγωγή λεξικού", "No dictionaries available.": "Δεν υπάρχουν διαθέσιμα λεξικά.", - "Drag the handle on the left to reorder.": "Σύρετε τη λαβή αριστερά για επαναταξινόμηση.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Τα πακέτα StarDict χρειάζονται αρχεία .ifo, .idx και .dict.dz (το .syn είναι προαιρετικό).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Τα πακέτα MDict χρησιμοποιούν αρχεία .mdx· τα συνοδευτικά .mdd είναι προαιρετικά.", "Select all the bundle files together when importing.": "Επιλέξτε όλα τα αρχεία του πακέτου μαζί κατά την εισαγωγή.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "Περιλαμβάνει την πρόοδο ανάγνωσής σας", "Synced {{time}}": "Συγχρονίστηκε {{time}}", "Sync Info": "Πληροφορίες συγχρονισμού", - "Last Synced": "Τελευταίος συγχρονισμός" + "Last Synced": "Τελευταίος συγχρονισμός", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Τα πακέτα DICT χρειάζονται ένα αρχείο .index και ένα αρχείο .dict.dz.", + "Slob bundles need a .slob file.": "Τα πακέτα Slob χρειάζονται ένα αρχείο .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Το πρότυπο URL πρέπει να ξεκινά με http(s):// και να περιέχει %WORD%.", + "Web": "Διαδίκτυο", + "Add Web Search": "Προσθήκη αναζήτησης ιστού", + "Edit Web Search": "Επεξεργασία αναζήτησης ιστού", + "e.g. Google": "π.χ. Google", + "URL Template": "Πρότυπο URL", + "Use %WORD% where the looked-up word should appear.": "Χρησιμοποιήστε %WORD% όπου πρέπει να εμφανίζεται η λέξη αναζήτησης.", + "Open the search result in your browser:": "Ανοίξτε το αποτέλεσμα αναζήτησης στο πρόγραμμα περιήγησής σας:", + "Open in {{name}}": "Άνοιγμα σε {{name}}" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 30185f0d..f992884b 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1252,7 +1252,6 @@ "Importing…": "Importando…", "Import Dictionary": "Importar diccionario", "No dictionaries available.": "No hay diccionarios disponibles.", - "Drag the handle on the left to reorder.": "Arrastra el asa de la izquierda para reordenar.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Los paquetes StarDict requieren archivos .ifo, .idx y .dict.dz (.syn opcional).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Los paquetes MDict usan archivos .mdx; los .mdd asociados son opcionales.", "Select all the bundle files together when importing.": "Selecciona todos los archivos del paquete juntos al importar.", @@ -1340,5 +1339,18 @@ "Includes your reading progress": "Incluye tu progreso de lectura", "Synced {{time}}": "Sincronizado {{time}}", "Sync Info": "Información de sincronización", - "Last Synced": "Última sincronización" + "Last Synced": "Última sincronización", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Los paquetes DICT necesitan un archivo .index y un archivo .dict.dz.", + "Slob bundles need a .slob file.": "Los paquetes Slob necesitan un archivo .slob.", + "URL template must start with http(s):// and contain %WORD%.": "La plantilla de URL debe empezar con http(s):// y contener %WORD%.", + "Web": "Web", + "Add Web Search": "Añadir búsqueda web", + "Edit Web Search": "Editar búsqueda web", + "e.g. Google": "p. ej. Google", + "URL Template": "Plantilla de URL", + "Use %WORD% where the looked-up word should appear.": "Use %WORD% donde debe aparecer la palabra buscada.", + "Open the search result in your browser:": "Abre el resultado de búsqueda en tu navegador:", + "Open in {{name}}": "Abrir en {{name}}" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index d990a298..46e5d203 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "در حال وارد کردن…", "Import Dictionary": "وارد کردن فرهنگ‌لغت", "No dictionaries available.": "هیچ فرهنگ‌لغتی در دسترس نیست.", - "Drag the handle on the left to reorder.": "برای تغییر ترتیب، دستگیرهٔ سمت چپ را بکشید.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "بسته‌های StarDict به فایل‌های .ifo، .idx و .dict.dz نیاز دارند (.syn اختیاری).", "MDict bundles use .mdx files; companion .mdd files are optional.": "بسته‌های MDict از فایل‌های .mdx استفاده می‌کنند؛ فایل‌های همراه .mdd اختیاری‌اند.", "Select all the bundle files together when importing.": "هنگام وارد کردن، همه فایل‌های بسته را با هم انتخاب کنید.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "شامل پیشرفت مطالعهٔ شماست", "Synced {{time}}": "{{time}} همگام‌سازی شد", "Sync Info": "اطلاعات همگام‌سازی", - "Last Synced": "آخرین همگام‌سازی" + "Last Synced": "آخرین همگام‌سازی", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "بسته‌های DICT به یک فایل ‎.index‎ و یک فایل ‎.dict.dz‎ نیاز دارند.", + "Slob bundles need a .slob file.": "بسته‌های Slob به یک فایل ‎.slob‎ نیاز دارند.", + "URL template must start with http(s):// and contain %WORD%.": "الگوی URL باید با http(s):// شروع شود و حاوی %WORD% باشد.", + "Web": "وب", + "Add Web Search": "افزودن جست‌وجوی وب", + "Edit Web Search": "ویرایش جست‌وجوی وب", + "e.g. Google": "مثلاً Google", + "URL Template": "الگوی URL", + "Use %WORD% where the looked-up word should appear.": "از %WORD% در جایی که کلمه‌ی مورد جست‌وجو باید ظاهر شود، استفاده کنید.", + "Open the search result in your browser:": "نتیجه‌ی جست‌وجو را در مرورگر باز کنید:", + "Open in {{name}}": "بازکردن در {{name}}" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 8c89bd3c..b1155e6f 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1252,7 +1252,6 @@ "Importing…": "Importation…", "Import Dictionary": "Importer un dictionnaire", "No dictionaries available.": "Aucun dictionnaire disponible.", - "Drag the handle on the left to reorder.": "Glissez la poignée à gauche pour réorganiser.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Les paquets StarDict nécessitent des fichiers .ifo, .idx et .dict.dz (.syn optionnel).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Les paquets MDict utilisent des fichiers .mdx ; les .mdd associés sont optionnels.", "Select all the bundle files together when importing.": "Sélectionnez tous les fichiers du paquet ensemble lors de l’import.", @@ -1340,5 +1339,18 @@ "Includes your reading progress": "Inclut votre progression de lecture", "Synced {{time}}": "Synchronisé {{time}}", "Sync Info": "Infos de synchronisation", - "Last Synced": "Dernière synchronisation" + "Last Synced": "Dernière synchronisation", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Les paquets DICT nécessitent un fichier .index et un fichier .dict.dz.", + "Slob bundles need a .slob file.": "Les paquets Slob nécessitent un fichier .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Le modèle d'URL doit commencer par http(s):// et contenir %WORD%.", + "Web": "Web", + "Add Web Search": "Ajouter une recherche web", + "Edit Web Search": "Modifier la recherche web", + "e.g. Google": "ex. Google", + "URL Template": "Modèle d'URL", + "Use %WORD% where the looked-up word should appear.": "Utilisez %WORD% à l'endroit où le mot recherché doit apparaître.", + "Open the search result in your browser:": "Ouvrir le résultat de la recherche dans votre navigateur :", + "Open in {{name}}": "Ouvrir dans {{name}}" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index acbd4522..70da179d 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1252,7 +1252,6 @@ "Importing…": "מייבא…", "Import Dictionary": "ייבא מילון", "No dictionaries available.": "אין מילונים זמינים.", - "Drag the handle on the left to reorder.": "גררו את הידית משמאל כדי לסדר מחדש.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "חבילות StarDict דורשות קבצי .ifo,‏ .idx ו-.dict.dz (.syn אופציונלי).", "MDict bundles use .mdx files; companion .mdd files are optional.": "חבילות MDict משתמשות בקבצי .mdx; קבצי .mdd נלווים הם אופציונליים.", "Select all the bundle files together when importing.": "בחרו את כל קבצי החבילה יחד בעת הייבוא.", @@ -1340,5 +1339,18 @@ "Includes your reading progress": "כולל את התקדמות הקריאה שלך", "Synced {{time}}": "סונכרן {{time}}", "Sync Info": "פרטי סנכרון", - "Last Synced": "סונכרן לאחרונה" + "Last Synced": "סונכרן לאחרונה", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "חבילות DICT דורשות קובץ ‎.index‎ וקובץ ‎.dict.dz‎.", + "Slob bundles need a .slob file.": "חבילות Slob דורשות קובץ ‎.slob‎.", + "URL template must start with http(s):// and contain %WORD%.": "תבנית כתובת ה-URL חייבת להתחיל ב-http(s):// ולהכיל ‎%WORD%‎.", + "Web": "אינטרנט", + "Add Web Search": "הוסף חיפוש באינטרנט", + "Edit Web Search": "ערוך חיפוש באינטרנט", + "e.g. Google": "לדוגמה: Google", + "URL Template": "תבנית URL", + "Use %WORD% where the looked-up word should appear.": "השתמש ב-%WORD% היכן שהמילה המבוקשת צריכה להופיע.", + "Open the search result in your browser:": "פתח את תוצאת החיפוש בדפדפן שלך:", + "Open in {{name}}": "פתח ב-{{name}}" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 7026e9b6..bd388fc9 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "आयात हो रहा है…", "Import Dictionary": "शब्दकोश आयात करें", "No dictionaries available.": "कोई शब्दकोश उपलब्ध नहीं है।", - "Drag the handle on the left to reorder.": "पुनः क्रमित करने के लिए बायीं ओर के हैंडल को खींचें।", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict बंडल को .ifo, .idx और .dict.dz फ़ाइलें चाहिए (.syn वैकल्पिक)।", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict बंडल .mdx फ़ाइलों का उपयोग करते हैं; साथी .mdd फ़ाइलें वैकल्पिक हैं।", "Select all the bundle files together when importing.": "आयात करते समय बंडल की सभी फ़ाइलें एक साथ चुनें।", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "आपकी पढ़ने की प्रगति शामिल है", "Synced {{time}}": "{{time}} सिंक हुआ", "Sync Info": "सिंक जानकारी", - "Last Synced": "अंतिम सिंक" + "Last Synced": "अंतिम सिंक", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT बंडल को एक .index फ़ाइल और एक .dict.dz फ़ाइल की आवश्यकता होती है।", + "Slob bundles need a .slob file.": "Slob बंडल को एक .slob फ़ाइल की आवश्यकता होती है।", + "URL template must start with http(s):// and contain %WORD%.": "URL टेम्पलेट http(s):// से शुरू होना चाहिए और उसमें %WORD% होना चाहिए।", + "Web": "वेब", + "Add Web Search": "वेब खोज जोड़ें", + "Edit Web Search": "वेब खोज संपादित करें", + "e.g. Google": "उदा. Google", + "URL Template": "URL टेम्पलेट", + "Use %WORD% where the looked-up word should appear.": "%WORD% का उपयोग वहां करें जहां खोजा गया शब्द दिखाई देना चाहिए।", + "Open the search result in your browser:": "अपने ब्राउज़र में खोज परिणाम खोलें:", + "Open in {{name}}": "{{name}} में खोलें" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 1661babe..0c0de602 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "Importálás…", "Import Dictionary": "Szótár importálása", "No dictionaries available.": "Nincsenek elérhető szótárak.", - "Drag the handle on the left to reorder.": "A bal oldali fogantyúval rendezheted át.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "A StarDict csomagokhoz .ifo, .idx és .dict.dz fájlok szükségesek (.syn opcionális).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Az MDict csomagok .mdx fájlokat használnak; a kísérő .mdd fájlok opcionálisak.", "Select all the bundle files together when importing.": "Importáláskor jelöld ki egyszerre a csomag összes fájlját.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "Tartalmazza az olvasási folyamatot", "Synced {{time}}": "Szinkronizálva {{time}}", "Sync Info": "Szinkronizálási információk", - "Last Synced": "Utolsó szinkronizálás" + "Last Synced": "Utolsó szinkronizálás", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "A DICT csomagokhoz egy .index fájl és egy .dict.dz fájl szükséges.", + "Slob bundles need a .slob file.": "A Slob csomagokhoz egy .slob fájl szükséges.", + "URL template must start with http(s):// and contain %WORD%.": "Az URL-sablonnak http(s):// kezdetűnek kell lennie, és tartalmaznia kell a %WORD% részt.", + "Web": "Web", + "Add Web Search": "Webes keresés hozzáadása", + "Edit Web Search": "Webes keresés szerkesztése", + "e.g. Google": "pl. Google", + "URL Template": "URL-sablon", + "Use %WORD% where the looked-up word should appear.": "Használja a %WORD%-t ott, ahol a keresett szónak meg kell jelennie.", + "Open the search result in your browser:": "Nyissa meg a találatot a böngészőben:", + "Open in {{name}}": "Megnyitás itt: {{name}}" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 270c3768..990d51fa 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "Mengimpor…", "Import Dictionary": "Impor Kamus", "No dictionaries available.": "Tidak ada kamus yang tersedia.", - "Drag the handle on the left to reorder.": "Seret pegangan di sebelah kiri untuk mengurutkan ulang.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Paket StarDict memerlukan file .ifo, .idx, dan .dict.dz (.syn opsional).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Paket MDict menggunakan file .mdx; file .mdd pendamping bersifat opsional.", "Select all the bundle files together when importing.": "Pilih semua file paket sekaligus saat mengimpor.", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "Termasuk progres baca Anda", "Synced {{time}}": "Disinkronkan {{time}}", "Sync Info": "Info sinkronisasi", - "Last Synced": "Sinkronisasi terakhir" + "Last Synced": "Sinkronisasi terakhir", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Bundel DICT memerlukan berkas .index dan berkas .dict.dz.", + "Slob bundles need a .slob file.": "Bundel Slob memerlukan berkas .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Templat URL harus diawali http(s):// dan berisi %WORD%.", + "Web": "Web", + "Add Web Search": "Tambah Pencarian Web", + "Edit Web Search": "Edit Pencarian Web", + "e.g. Google": "mis. Google", + "URL Template": "Templat URL", + "Use %WORD% where the looked-up word should appear.": "Gunakan %WORD% di tempat kata yang dicari harus muncul.", + "Open the search result in your browser:": "Buka hasil pencarian di peramban Anda:", + "Open in {{name}}": "Buka di {{name}}" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 56133ec8..fef1d814 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1252,7 +1252,6 @@ "Importing…": "Importazione…", "Import Dictionary": "Importa dizionario", "No dictionaries available.": "Nessun dizionario disponibile.", - "Drag the handle on the left to reorder.": "Trascina la maniglia a sinistra per riordinare.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "I pacchetti StarDict richiedono file .ifo, .idx e .dict.dz (.syn opzionale).", "MDict bundles use .mdx files; companion .mdd files are optional.": "I pacchetti MDict usano file .mdx; i .mdd associati sono opzionali.", "Select all the bundle files together when importing.": "Seleziona tutti i file del pacchetto insieme durante l’importazione.", @@ -1340,5 +1339,18 @@ "Includes your reading progress": "Include i tuoi progressi di lettura", "Synced {{time}}": "Sincronizzato {{time}}", "Sync Info": "Info di sincronizzazione", - "Last Synced": "Ultima sincronizzazione" + "Last Synced": "Ultima sincronizzazione", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "I bundle DICT richiedono un file .index e un file .dict.dz.", + "Slob bundles need a .slob file.": "I bundle Slob richiedono un file .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Il modello URL deve iniziare con http(s):// e contenere %WORD%.", + "Web": "Web", + "Add Web Search": "Aggiungi ricerca web", + "Edit Web Search": "Modifica ricerca web", + "e.g. Google": "es. Google", + "URL Template": "Modello URL", + "Use %WORD% where the looked-up word should appear.": "Usa %WORD% dove dovrebbe apparire la parola cercata.", + "Open the search result in your browser:": "Apri il risultato della ricerca nel tuo browser:", + "Open in {{name}}": "Apri in {{name}}" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index c4ac668a..b2e7a438 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "インポート中…", "Import Dictionary": "辞書をインポート", "No dictionaries available.": "利用可能な辞書はありません。", - "Drag the handle on the left to reorder.": "左のハンドルをドラッグして並べ替えます。", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict バンドルには .ifo、.idx、.dict.dz ファイルが必要です(.syn は任意)。", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict バンドルは .mdx ファイルを使用します。付随する .mdd ファイルは任意です。", "Select all the bundle files together when importing.": "インポート時はバンドルのファイルをまとめて選択してください。", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "読書の進捗を含みます", "Synced {{time}}": "{{time}}に同期", "Sync Info": "同期情報", - "Last Synced": "最終同期" + "Last Synced": "最終同期", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT バンドルには .index ファイルと .dict.dz ファイルが必要です。", + "Slob bundles need a .slob file.": "Slob バンドルには .slob ファイルが必要です。", + "URL template must start with http(s):// and contain %WORD%.": "URL テンプレートは http(s):// で始まり、%WORD% を含む必要があります。", + "Web": "ウェブ", + "Add Web Search": "ウェブ検索を追加", + "Edit Web Search": "ウェブ検索を編集", + "e.g. Google": "例: Google", + "URL Template": "URL テンプレート", + "Use %WORD% where the looked-up word should appear.": "調べる単語が表示される場所に %WORD% を使用してください。", + "Open the search result in your browser:": "検索結果をブラウザで開く:", + "Open in {{name}}": "{{name}} で開く" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 4a02dbe7..2c800d94 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "가져오는 중…", "Import Dictionary": "사전 가져오기", "No dictionaries available.": "사용 가능한 사전이 없습니다.", - "Drag the handle on the left to reorder.": "왼쪽 핸들을 드래그하여 순서를 변경하세요.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict 번들은 .ifo, .idx, .dict.dz 파일이 필요합니다(.syn은 선택).", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict 번들은 .mdx 파일을 사용하며, 동반되는 .mdd 파일은 선택입니다.", "Select all the bundle files together when importing.": "가져올 때 번들의 모든 파일을 함께 선택하세요.", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "읽기 진행률 포함됨", "Synced {{time}}": "{{time}} 동기화됨", "Sync Info": "동기화 정보", - "Last Synced": "마지막 동기화" + "Last Synced": "마지막 동기화", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT 번들에는 .index 파일과 .dict.dz 파일이 필요합니다.", + "Slob bundles need a .slob file.": "Slob 번들에는 .slob 파일이 필요합니다.", + "URL template must start with http(s):// and contain %WORD%.": "URL 템플릿은 http(s):// 로 시작하고 %WORD%를 포함해야 합니다.", + "Web": "웹", + "Add Web Search": "웹 검색 추가", + "Edit Web Search": "웹 검색 편집", + "e.g. Google": "예: Google", + "URL Template": "URL 템플릿", + "Use %WORD% where the looked-up word should appear.": "검색어가 표시될 위치에 %WORD%를 사용하세요.", + "Open the search result in your browser:": "브라우저에서 검색 결과 열기:", + "Open in {{name}}": "{{name}}에서 열기" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 10a4995b..7d5e713a 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "Mengimport…", "Import Dictionary": "Import Kamus", "No dictionaries available.": "Tiada kamus tersedia.", - "Drag the handle on the left to reorder.": "Seret pemegang di sebelah kiri untuk susun semula.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Bundle StarDict memerlukan fail .ifo, .idx dan .dict.dz (.syn pilihan).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Bundle MDict menggunakan fail .mdx; fail .mdd pengiring adalah pilihan.", "Select all the bundle files together when importing.": "Pilih semua fail bundle sekali gus semasa mengimport.", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "Termasuk kemajuan bacaan anda", "Synced {{time}}": "Disegerakkan {{time}}", "Sync Info": "Maklumat penyegerakan", - "Last Synced": "Penyegerakan terakhir" + "Last Synced": "Penyegerakan terakhir", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Pakej DICT memerlukan fail .index dan fail .dict.dz.", + "Slob bundles need a .slob file.": "Pakej Slob memerlukan fail .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Templat URL mesti bermula dengan http(s):// dan mengandungi %WORD%.", + "Web": "Web", + "Add Web Search": "Tambah Carian Web", + "Edit Web Search": "Edit Carian Web", + "e.g. Google": "cth. Google", + "URL Template": "Templat URL", + "Use %WORD% where the looked-up word should appear.": "Gunakan %WORD% di mana perkataan yang dicari harus muncul.", + "Open the search result in your browser:": "Buka hasil carian dalam pelayar anda:", + "Open in {{name}}": "Buka dalam {{name}}" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 3a911c6c..70634b43 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "Bezig met importeren…", "Import Dictionary": "Woordenboek importeren", "No dictionaries available.": "Geen woordenboeken beschikbaar.", - "Drag the handle on the left to reorder.": "Sleep de greep links om opnieuw te ordenen.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict-bundels hebben .ifo-, .idx- en .dict.dz-bestanden nodig (.syn optioneel).", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict-bundels gebruiken .mdx-bestanden; bijbehorende .mdd-bestanden zijn optioneel.", "Select all the bundle files together when importing.": "Selecteer bij het importeren alle bundelbestanden tegelijk.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "Inclusief je leesvoortgang", "Synced {{time}}": "Gesynchroniseerd {{time}}", "Sync Info": "Synchronisatie-info", - "Last Synced": "Laatst gesynchroniseerd" + "Last Synced": "Laatst gesynchroniseerd", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT-bundels hebben een .index-bestand en een .dict.dz-bestand nodig.", + "Slob bundles need a .slob file.": "Slob-bundels hebben een .slob-bestand nodig.", + "URL template must start with http(s):// and contain %WORD%.": "URL-sjabloon moet beginnen met http(s):// en %WORD% bevatten.", + "Web": "Web", + "Add Web Search": "Webzoekopdracht toevoegen", + "Edit Web Search": "Webzoekopdracht bewerken", + "e.g. Google": "bijv. Google", + "URL Template": "URL-sjabloon", + "Use %WORD% where the looked-up word should appear.": "Gebruik %WORD% op de plek waar het opgezochte woord moet verschijnen.", + "Open the search result in your browser:": "Open het zoekresultaat in je browser:", + "Open in {{name}}": "Openen in {{name}}" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 5e9a07a9..f086d393 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1270,7 +1270,6 @@ "Importing…": "Importowanie…", "Import Dictionary": "Importuj słownik", "No dictionaries available.": "Brak dostępnych słowników.", - "Drag the handle on the left to reorder.": "Przeciągnij uchwyt po lewej, aby zmienić kolejność.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Pakiety StarDict wymagają plików .ifo, .idx i .dict.dz (.syn opcjonalny).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Pakiety MDict używają plików .mdx; towarzyszące .mdd są opcjonalne.", "Select all the bundle files together when importing.": "Podczas importu wybierz wszystkie pliki pakietu jednocześnie.", @@ -1363,5 +1362,18 @@ "Includes your reading progress": "Zawiera Twój postęp czytania", "Synced {{time}}": "Zsynchronizowano {{time}}", "Sync Info": "Informacje o synchronizacji", - "Last Synced": "Ostatnia synchronizacja" + "Last Synced": "Ostatnia synchronizacja", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Pakiety DICT wymagają pliku .index i pliku .dict.dz.", + "Slob bundles need a .slob file.": "Pakiety Slob wymagają pliku .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Szablon URL musi zaczynać się od http(s):// i zawierać %WORD%.", + "Web": "Web", + "Add Web Search": "Dodaj wyszukiwanie internetowe", + "Edit Web Search": "Edytuj wyszukiwanie internetowe", + "e.g. Google": "np. Google", + "URL Template": "Szablon URL", + "Use %WORD% where the looked-up word should appear.": "Użyj %WORD% w miejscu, w którym ma się pojawić wyszukiwane słowo.", + "Open the search result in your browser:": "Otwórz wynik wyszukiwania w przeglądarce:", + "Open in {{name}}": "Otwórz w {{name}}" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 45ad7ec9..929870b3 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1252,7 +1252,6 @@ "Importing…": "Importando…", "Import Dictionary": "Importar dicionário", "No dictionaries available.": "Nenhum dicionário disponível.", - "Drag the handle on the left to reorder.": "Arraste o punho à esquerda para reordenar.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Pacotes StarDict precisam dos arquivos .ifo, .idx e .dict.dz (.syn opcional).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Pacotes MDict usam arquivos .mdx; arquivos .mdd companheiros são opcionais.", "Select all the bundle files together when importing.": "Selecione todos os arquivos do pacote juntos ao importar.", @@ -1340,5 +1339,18 @@ "Includes your reading progress": "Inclui seu progresso de leitura", "Synced {{time}}": "Sincronizado {{time}}", "Sync Info": "Informações de sincronização", - "Last Synced": "Última sincronização" + "Last Synced": "Última sincronização", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Os pacotes DICT precisam de um arquivo .index e um arquivo .dict.dz.", + "Slob bundles need a .slob file.": "Os pacotes Slob precisam de um arquivo .slob.", + "URL template must start with http(s):// and contain %WORD%.": "O modelo de URL deve começar com http(s):// e conter %WORD%.", + "Web": "Web", + "Add Web Search": "Adicionar pesquisa web", + "Edit Web Search": "Editar pesquisa web", + "e.g. Google": "por exemplo: Google", + "URL Template": "Modelo de URL", + "Use %WORD% where the looked-up word should appear.": "Use %WORD% onde a palavra pesquisada deve aparecer.", + "Open the search result in your browser:": "Abrir o resultado da pesquisa no seu navegador:", + "Open in {{name}}": "Abrir em {{name}}" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index a8e1bffb..016ca539 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1252,7 +1252,6 @@ "Importing…": "Se importă…", "Import Dictionary": "Importă dicționar", "No dictionaries available.": "Niciun dicționar disponibil.", - "Drag the handle on the left to reorder.": "Trage de mâner din stânga pentru a reordona.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Pachetele StarDict au nevoie de fișierele .ifo, .idx și .dict.dz (.syn opțional).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Pachetele MDict folosesc fișiere .mdx; fișierele .mdd însoțitoare sunt opționale.", "Select all the bundle files together when importing.": "Selectează împreună toate fișierele pachetului la import.", @@ -1340,5 +1339,18 @@ "Includes your reading progress": "Include progresul tău de lectură", "Synced {{time}}": "Sincronizat {{time}}", "Sync Info": "Informații sincronizare", - "Last Synced": "Ultima sincronizare" + "Last Synced": "Ultima sincronizare", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Pachetele DICT necesită un fișier .index și un fișier .dict.dz.", + "Slob bundles need a .slob file.": "Pachetele Slob necesită un fișier .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Șablonul URL trebuie să înceapă cu http(s):// și să conțină %WORD%.", + "Web": "Web", + "Add Web Search": "Adaugă căutare web", + "Edit Web Search": "Editează căutarea web", + "e.g. Google": "ex. Google", + "URL Template": "Șablon URL", + "Use %WORD% where the looked-up word should appear.": "Folosiți %WORD% acolo unde ar trebui să apară cuvântul căutat.", + "Open the search result in your browser:": "Deschideți rezultatul căutării în browser:", + "Open in {{name}}": "Deschide în {{name}}" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index aeae8e33..2a17e489 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1270,7 +1270,6 @@ "Importing…": "Импорт…", "Import Dictionary": "Импорт словаря", "No dictionaries available.": "Нет доступных словарей.", - "Drag the handle on the left to reorder.": "Перетащите ручку слева для изменения порядка.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Пакеты StarDict требуют файлов .ifo, .idx и .dict.dz (.syn — опционально).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Пакеты MDict используют файлы .mdx; сопутствующие .mdd — опционально.", "Select all the bundle files together when importing.": "При импорте выбирайте все файлы пакета вместе.", @@ -1363,5 +1362,18 @@ "Includes your reading progress": "Включает ваш прогресс чтения", "Synced {{time}}": "Синхронизировано {{time}}", "Sync Info": "Информация о синхронизации", - "Last Synced": "Последняя синхронизация" + "Last Synced": "Последняя синхронизация", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Пакетам DICT нужны файл .index и файл .dict.dz.", + "Slob bundles need a .slob file.": "Пакетам Slob нужен файл .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Шаблон URL должен начинаться с http(s):// и содержать %WORD%.", + "Web": "Веб", + "Add Web Search": "Добавить веб-поиск", + "Edit Web Search": "Изменить веб-поиск", + "e.g. Google": "Например, Google", + "URL Template": "Шаблон URL", + "Use %WORD% where the looked-up word should appear.": "Используйте %WORD% там, где должно появиться искомое слово.", + "Open the search result in your browser:": "Открыть результат поиска в браузере:", + "Open in {{name}}": "Открыть в {{name}}" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 05f2a5bb..d358291e 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "ආයාත වෙමින්…", "Import Dictionary": "ශබ්දකෝෂයක් ආයාත කරන්න", "No dictionaries available.": "ලබා ගත හැකි ශබ්දකෝෂ නැත.", - "Drag the handle on the left to reorder.": "නැවත සකස් කිරීමට වම් පස ඇති පතන අදින්න.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict බණ්ඩලවලට .ifo, .idx සහ .dict.dz ගොනු අවශ්‍යය (.syn විකල්ප).", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict බණ්ඩල .mdx ගොනු භාවිතා කරයි; සහායක .mdd ගොනු විකල්ප වේ.", "Select all the bundle files together when importing.": "ආයාත කරන විට බණ්ඩලේ සියලු ගොනු එකවර තෝරන්න.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "ඔබේ කියවීමේ ප්‍රගතිය ඇතුළත්", "Synced {{time}}": "{{time}} සමමුහුර්ත කළේ", "Sync Info": "සමමුහුර්ත තොරතුරු", - "Last Synced": "අවසන් සමමුහුර්තය" + "Last Synced": "අවසන් සමමුහුර්තය", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT පැකේජ සඳහා .index ගොනුවක් සහ .dict.dz ගොනුවක් අවශ්‍ය වේ.", + "Slob bundles need a .slob file.": "Slob පැකේජ සඳහා .slob ගොනුවක් අවශ්‍ය වේ.", + "URL template must start with http(s):// and contain %WORD%.": "URL අච්චුව http(s):// වලින් ආරම්භ විය යුතු අතර %WORD% අඩංගු විය යුතුය.", + "Web": "වෙබ්", + "Add Web Search": "වෙබ් සෙවුම එක් කරන්න", + "Edit Web Search": "වෙබ් සෙවුම සංස්කරණය කරන්න", + "e.g. Google": "උදා. Google", + "URL Template": "URL අච්චුව", + "Use %WORD% where the looked-up word should appear.": "සොයන වචනය දිස්විය යුතු තැන %WORD% භාවිතා කරන්න.", + "Open the search result in your browser:": "ඔබේ බ්‍රවුසරයේ සෙවුම් ප්‍රතිඵලය විවෘත කරන්න:", + "Open in {{name}}": "{{name}} වල විවෘත කරන්න" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 44db04c7..f73a480b 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1270,7 +1270,6 @@ "Importing…": "Uvažanje …", "Import Dictionary": "Uvozi slovar", "No dictionaries available.": "Ni razpoložljivih slovarjev.", - "Drag the handle on the left to reorder.": "Povleci ročaj levo za prerazporeditev.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Svežnji StarDict potrebujejo datoteke .ifo, .idx in .dict.dz (.syn neobvezno).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Svežnji MDict uporabljajo datoteke .mdx; spremljajoče .mdd so neobvezne.", "Select all the bundle files together when importing.": "Pri uvozu izberi vse datoteke svežnja skupaj.", @@ -1363,5 +1362,18 @@ "Includes your reading progress": "Vključuje vaš napredek branja", "Synced {{time}}": "Sinhronizirano {{time}}", "Sync Info": "Informacije o sinhronizaciji", - "Last Synced": "Zadnja sinhronizacija" + "Last Synced": "Zadnja sinhronizacija", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Paketi DICT potrebujejo datoteko .index in datoteko .dict.dz.", + "Slob bundles need a .slob file.": "Paketi Slob potrebujejo datoteko .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Predloga URL se mora začeti z http(s):// in vsebovati %WORD%.", + "Web": "Splet", + "Add Web Search": "Dodaj spletno iskanje", + "Edit Web Search": "Uredi spletno iskanje", + "e.g. Google": "npr. Google", + "URL Template": "Predloga URL", + "Use %WORD% where the looked-up word should appear.": "Uporabite %WORD% tam, kjer naj se pojavi iskana beseda.", + "Open the search result in your browser:": "Odprite rezultat iskanja v brskalniku:", + "Open in {{name}}": "Odpri v {{name}}" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index ecc5e310..f026996e 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "Importerar…", "Import Dictionary": "Importera ordbok", "No dictionaries available.": "Inga ordböcker tillgängliga.", - "Drag the handle on the left to reorder.": "Dra i handtaget till vänster för att ändra ordning.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict-paket kräver .ifo-, .idx- och .dict.dz-filer (.syn valfri).", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict-paket använder .mdx-filer; tillhörande .mdd-filer är valfria.", "Select all the bundle files together when importing.": "Markera alla paketfiler tillsammans när du importerar.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "Inkluderar ditt läsförlopp", "Synced {{time}}": "Synkroniserades {{time}}", "Sync Info": "Synkroniseringsinformation", - "Last Synced": "Senast synkroniserad" + "Last Synced": "Senast synkroniserad", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT-paket kräver en .index-fil och en .dict.dz-fil.", + "Slob bundles need a .slob file.": "Slob-paket kräver en .slob-fil.", + "URL template must start with http(s):// and contain %WORD%.": "URL-mallen måste börja med http(s):// och innehålla %WORD%.", + "Web": "Webb", + "Add Web Search": "Lägg till webbsökning", + "Edit Web Search": "Redigera webbsökning", + "e.g. Google": "t.ex. Google", + "URL Template": "URL-mall", + "Use %WORD% where the looked-up word should appear.": "Använd %WORD% där det uppslagna ordet ska visas.", + "Open the search result in your browser:": "Öppna sökresultatet i din webbläsare:", + "Open in {{name}}": "Öppna i {{name}}" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 151a575d..0ed66b0b 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "இறக்குமதி செய்கிறது…", "Import Dictionary": "அகராதியை இறக்குமதி செய்", "No dictionaries available.": "அகராதிகள் கிடைக்கவில்லை.", - "Drag the handle on the left to reorder.": "மறு வரிசைப்படுத்த இடதுபுறம் உள்ள கைப்பிடியை இழுக்கவும்.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict தொகுப்புகளுக்கு .ifo, .idx, .dict.dz கோப்புகள் தேவை (.syn விருப்பம்).", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict தொகுப்புகள் .mdx கோப்புகளைப் பயன்படுத்தும்; தொடர்புடைய .mdd கோப்புகள் விருப்பம்.", "Select all the bundle files together when importing.": "இறக்குமதி செய்யும்போது தொகுப்பின் அனைத்துக் கோப்புகளையும் ஒரே நேரத்தில் தேர்ந்தெடுக்கவும்.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "உங்கள் வாசிப்பு முன்னேற்றத்தை உள்ளடக்கியது", "Synced {{time}}": "{{time}} ஒத்திசைக்கப்பட்டது", "Sync Info": "ஒத்திசைவு தகவல்", - "Last Synced": "கடைசி ஒத்திசைவு" + "Last Synced": "கடைசி ஒத்திசைவு", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT தொகுப்புகளுக்கு ஒரு .index கோப்பு மற்றும் ஒரு .dict.dz கோப்பு தேவை.", + "Slob bundles need a .slob file.": "Slob தொகுப்புகளுக்கு ஒரு .slob கோப்பு தேவை.", + "URL template must start with http(s):// and contain %WORD%.": "URL வார்ப்புரு http(s):// உடன் தொடங்க வேண்டும் மற்றும் %WORD% இருக்க வேண்டும்.", + "Web": "வலை", + "Add Web Search": "வலைத் தேடல் சேர்", + "Edit Web Search": "வலைத் தேடலைத் திருத்து", + "e.g. Google": "எ.கா. Google", + "URL Template": "URL வார்ப்புரு", + "Use %WORD% where the looked-up word should appear.": "தேடப்பட்ட சொல் தோன்ற வேண்டிய இடத்தில் %WORD% ஐ பயன்படுத்தவும்.", + "Open the search result in your browser:": "உங்கள் உலாவியில் தேடல் முடிவைத் திற:", + "Open in {{name}}": "{{name}} இல் திற" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 6778478e..f55abad0 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "กำลังนำเข้า…", "Import Dictionary": "นำเข้าพจนานุกรม", "No dictionaries available.": "ไม่มีพจนานุกรม", - "Drag the handle on the left to reorder.": "ลากที่จับทางซ้ายเพื่อจัดลำดับใหม่", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "บันเดิล StarDict ต้องมีไฟล์ .ifo, .idx และ .dict.dz (.syn ไม่บังคับ)", "MDict bundles use .mdx files; companion .mdd files are optional.": "บันเดิล MDict ใช้ไฟล์ .mdx ส่วนไฟล์ .mdd ที่มาด้วยเป็นทางเลือก", "Select all the bundle files together when importing.": "เลือกไฟล์ทั้งหมดของบันเดิลพร้อมกันเมื่อทำการนำเข้า", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "รวมถึงความคืบหน้าการอ่านของคุณ", "Synced {{time}}": "ซิงค์เมื่อ {{time}}", "Sync Info": "ข้อมูลการซิงค์", - "Last Synced": "ซิงค์ล่าสุด" + "Last Synced": "ซิงค์ล่าสุด", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "ชุด DICT ต้องการไฟล์ .index และไฟล์ .dict.dz", + "Slob bundles need a .slob file.": "ชุด Slob ต้องการไฟล์ .slob", + "URL template must start with http(s):// and contain %WORD%.": "เทมเพลต URL ต้องขึ้นต้นด้วย http(s):// และมี %WORD%", + "Web": "เว็บ", + "Add Web Search": "เพิ่มการค้นหาบนเว็บ", + "Edit Web Search": "แก้ไขการค้นหาบนเว็บ", + "e.g. Google": "เช่น Google", + "URL Template": "เทมเพลต URL", + "Use %WORD% where the looked-up word should appear.": "ใช้ %WORD% ในตำแหน่งที่คำค้นหาควรปรากฏ", + "Open the search result in your browser:": "เปิดผลลัพธ์การค้นหาในเบราว์เซอร์ของคุณ:", + "Open in {{name}}": "เปิดใน {{name}}" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 3374a41f..41bcf033 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1234,7 +1234,6 @@ "Importing…": "İçe aktarılıyor…", "Import Dictionary": "Sözlük içe aktar", "No dictionaries available.": "Kullanılabilir sözlük yok.", - "Drag the handle on the left to reorder.": "Yeniden sıralamak için soldaki tutamacı sürükleyin.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict paketleri .ifo, .idx ve .dict.dz dosyalarını gerektirir (.syn isteğe bağlı).", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict paketleri .mdx dosyalarını kullanır; eşlik eden .mdd dosyaları isteğe bağlıdır.", "Select all the bundle files together when importing.": "İçe aktarırken paketin tüm dosyalarını birlikte seçin.", @@ -1317,5 +1316,18 @@ "Includes your reading progress": "Okuma ilerlemenizi içerir", "Synced {{time}}": "{{time}} senkronize edildi", "Sync Info": "Senkronizasyon bilgisi", - "Last Synced": "Son senkronizasyon" + "Last Synced": "Son senkronizasyon", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT paketleri bir .index dosyası ve bir .dict.dz dosyası gerektirir.", + "Slob bundles need a .slob file.": "Slob paketleri bir .slob dosyası gerektirir.", + "URL template must start with http(s):// and contain %WORD%.": "URL şablonu http(s):// ile başlamalı ve %WORD% içermelidir.", + "Web": "Web", + "Add Web Search": "Web Araması Ekle", + "Edit Web Search": "Web Aramasını Düzenle", + "e.g. Google": "örn. Google", + "URL Template": "URL Şablonu", + "Use %WORD% where the looked-up word should appear.": "Aranan kelimenin görünmesi gereken yerde %WORD% kullanın.", + "Open the search result in your browser:": "Arama sonucunu tarayıcınızda açın:", + "Open in {{name}}": "{{name}} ile aç" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index c9260b82..80991a30 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1270,7 +1270,6 @@ "Importing…": "Імпорт…", "Import Dictionary": "Імпорт словника", "No dictionaries available.": "Немає доступних словників.", - "Drag the handle on the left to reorder.": "Перетягуйте маркер ліворуч, щоб змінити порядок.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Пакети StarDict потребують файлів .ifo, .idx і .dict.dz (.syn — необов’язково).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Пакети MDict використовують файли .mdx; супутні .mdd — необов’язково.", "Select all the bundle files together when importing.": "Під час імпорту вибирайте всі файли пакета разом.", @@ -1363,5 +1362,18 @@ "Includes your reading progress": "Містить ваш прогрес читання", "Synced {{time}}": "Синхронізовано {{time}}", "Sync Info": "Інформація про синхронізацію", - "Last Synced": "Остання синхронізація" + "Last Synced": "Остання синхронізація", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Пакети DICT потребують файл .index і файл .dict.dz.", + "Slob bundles need a .slob file.": "Пакети Slob потребують файл .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Шаблон URL має починатися з http(s):// та містити %WORD%.", + "Web": "Веб", + "Add Web Search": "Додати веб-пошук", + "Edit Web Search": "Редагувати веб-пошук", + "e.g. Google": "напр. Google", + "URL Template": "Шаблон URL", + "Use %WORD% where the looked-up word should appear.": "Використовуйте %WORD% там, де має з'явитися шукане слово.", + "Open the search result in your browser:": "Відкрийте результат пошуку в браузері:", + "Open in {{name}}": "Відкрити в {{name}}" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 5e966d60..1ccd34d1 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "Đang nhập…", "Import Dictionary": "Nhập từ điển", "No dictionaries available.": "Không có từ điển.", - "Drag the handle on the left to reorder.": "Kéo tay cầm bên trái để sắp xếp lại.", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "Gói StarDict cần các tệp .ifo, .idx và .dict.dz (.syn tuỳ chọn).", "MDict bundles use .mdx files; companion .mdd files are optional.": "Gói MDict dùng tệp .mdx; tệp .mdd đi kèm là tuỳ chọn.", "Select all the bundle files together when importing.": "Chọn tất cả các tệp của gói cùng lúc khi nhập.", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "Bao gồm tiến độ đọc của bạn", "Synced {{time}}": "Đã đồng bộ {{time}}", "Sync Info": "Thông tin đồng bộ", - "Last Synced": "Đồng bộ lần cuối" + "Last Synced": "Đồng bộ lần cuối", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "Gói DICT cần một tệp .index và một tệp .dict.dz.", + "Slob bundles need a .slob file.": "Gói Slob cần một tệp .slob.", + "URL template must start with http(s):// and contain %WORD%.": "Mẫu URL phải bắt đầu bằng http(s):// và chứa %WORD%.", + "Web": "Web", + "Add Web Search": "Thêm tìm kiếm web", + "Edit Web Search": "Sửa tìm kiếm web", + "e.g. Google": "vd. Google", + "URL Template": "Mẫu URL", + "Use %WORD% where the looked-up word should appear.": "Dùng %WORD% tại vị trí mà từ tra cứu sẽ xuất hiện.", + "Open the search result in your browser:": "Mở kết quả tìm kiếm trong trình duyệt của bạn:", + "Open in {{name}}": "Mở trong {{name}}" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 21c72660..59b6a16a 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "正在导入…", "Import Dictionary": "导入词典", "No dictionaries available.": "没有可用的词典。", - "Drag the handle on the left to reorder.": "拖动左侧手柄以重新排序。", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict 词典包需要 .ifo、.idx 和 .dict.dz 文件(.syn 可选)。", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict 词典包使用 .mdx 文件;配套的 .mdd 文件为可选。", "Select all the bundle files together when importing.": "导入时请同时选择词典包内的所有文件。", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "包含你的阅读进度", "Synced {{time}}": "{{time}}已同步", "Sync Info": "同步信息", - "Last Synced": "上一次同步" + "Last Synced": "上一次同步", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT 词典包需要一个 .index 文件和一个 .dict.dz 文件。", + "Slob bundles need a .slob file.": "Slob 词典包需要一个 .slob 文件。", + "URL template must start with http(s):// and contain %WORD%.": "URL 模板必须以 http(s):// 开头并包含 %WORD%。", + "Web": "网络", + "Add Web Search": "添加网络搜索", + "Edit Web Search": "编辑网络搜索", + "e.g. Google": "例如 Google", + "URL Template": "URL 模板", + "Use %WORD% where the looked-up word should appear.": "在查询词应出现的位置使用 %WORD%。", + "Open the search result in your browser:": "在浏览器中打开搜索结果:", + "Open in {{name}}": "在 {{name}} 中打开" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 3c893cb1..153b1ca1 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1216,7 +1216,6 @@ "Importing…": "正在匯入…", "Import Dictionary": "匯入詞典", "No dictionaries available.": "沒有可用的詞典。", - "Drag the handle on the left to reorder.": "拖曳左側握把以重新排序。", "StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).": "StarDict 詞典套件需要 .ifo、.idx 和 .dict.dz 檔案(.syn 選用)。", "MDict bundles use .mdx files; companion .mdd files are optional.": "MDict 詞典套件使用 .mdx 檔案;搭配的 .mdd 檔案為選用。", "Select all the bundle files together when importing.": "匯入時請同時選擇詞典套件內的所有檔案。", @@ -1294,5 +1293,18 @@ "Includes your reading progress": "包含你的閱讀進度", "Synced {{time}}": "{{time}}已同步", "Sync Info": "同步資訊", - "Last Synced": "上一次同步" + "Last Synced": "上一次同步", + "DICT": "DICT", + "Slob": "Slob", + "DICT bundles need a .index file and a .dict.dz file.": "DICT 字典套件需要一個 .index 檔案和一個 .dict.dz 檔案。", + "Slob bundles need a .slob file.": "Slob 字典套件需要一個 .slob 檔案。", + "URL template must start with http(s):// and contain %WORD%.": "URL 範本必須以 http(s):// 開頭並包含 %WORD%。", + "Web": "網路", + "Add Web Search": "新增網路搜尋", + "Edit Web Search": "編輯網路搜尋", + "e.g. Google": "例如 Google", + "URL Template": "URL 範本", + "Use %WORD% where the looked-up word should appear.": "在查詢字詞應出現的位置使用 %WORD%。", + "Open the search result in your browser:": "在瀏覽器中開啟搜尋結果:", + "Open in {{name}}": "在 {{name}} 中開啟" } diff --git a/apps/readest-app/src/__tests__/fixtures/data/dicts/eng-nld.slob b/apps/readest-app/src/__tests__/fixtures/data/dicts/eng-nld.slob new file mode 100644 index 00000000..88ce91cf Binary files /dev/null and b/apps/readest-app/src/__tests__/fixtures/data/dicts/eng-nld.slob differ diff --git a/apps/readest-app/src/__tests__/fixtures/data/dicts/freedict-eng-nld.dict.dz b/apps/readest-app/src/__tests__/fixtures/data/dicts/freedict-eng-nld.dict.dz new file mode 100644 index 00000000..701115aa Binary files /dev/null and b/apps/readest-app/src/__tests__/fixtures/data/dicts/freedict-eng-nld.dict.dz differ diff --git a/apps/readest-app/src/__tests__/fixtures/data/dicts/freedict-eng-nld.index b/apps/readest-app/src/__tests__/fixtures/data/dicts/freedict-eng-nld.index new file mode 100644 index 00000000..bf618191 --- /dev/null +++ b/apps/readest-app/src/__tests__/fixtures/data/dicts/freedict-eng-nld.index @@ -0,0 +1,7720 @@ +00databasealphabet Bn0B m +00databasedictfmt1121 B b +00databaseinfo c eR +00databaseshort et r +00databaseurl fY V +00databaseutf8 A B +a hI L +a shop where drugs are sold Bns6 BV +able hT l +about h4 k +above ic Q +across is W +after jC T +again jV Q +against jl T +age j4 N +ago kF N +air kS N +all kf J +almost ko S +alone k6 Q +along lK R +already lb X +also ly P +always mB p +american ft h +among mq R +an m7 K +and nF M +another nR W +are nn N +area n0 R +around oF S +as oX K +ask oh M +at ot K +auxiliary verb for passive voice BnpY B0 +auxiliary verb for past tense BnrM Bu +away o3 Q +back pH N +barricade of trees or branches BnuP By +be pU J +become pd U +before px S +behind qD U +believe qX u +best rF N +better rS R +between rj U +body r3 Q +book sH M +both sT O +boy sh M +bring st P +british gO Y +business s8 U +but tQ M +by tc L +call tn P +can t2 M +car uC O +certain uQ T +change uj U +child u3 R +city vI P +close vX P +come vm N +common vz S +country wF V +course wa R +cut wr M +day w3 L +different xC W +do xY K +door xi P +down xx N +during x+ U +each yS P +early yh S +either or yz c +emphasizing particle BnwB Br +enough zP Q +even zf O +ever zt O +every z7 Q +everything 0L Y +example 0j W +eye 05 L +face 1E N +fact 1R O +family 1f V +far 10 M +father 2A V +feel 2V O +felt 2j N +few 2w N +find 29 O +first 3L R +five 3c N +follow 3p R +food 36 O +foot 4I M +for 4U M +form 4g P +found 4v P +four 4+ P +full 5N M +get 5Z L +girl 5k P +give 5z M +go 5/ K +good 6J M +great 6V P +group 6k Bl +group member 8J n +grouser 8w 8 +grow 9s DI +grow more beautiful BE1 BF +grow sour BF6 t +grow stronger BGn 9 +growl BA0 0 +grown over BCt i +grownup BBo BF +growth BDP Bm +grub BHk Bs +grudge BJQ BU +gruel BKk h +gruesome BLF DG +gruff BOL BO +grumble BPZ 4 +grumbler BQR / +guarantee BRQ CK +guarantee of a bill BTa BA +guard BUa DQ +guardian angel BXq BL +guelder rose BY1 m +guess BZb Bq +guesswork BbF e +guest Bbj l +guide BcI Cr +guidebook Bez BT +guilder BgG Z +guile Bgf l +guileless BhE d +guilt Bhh U +guiltless Bh1 BC +guilty Bi3 a +guineapig BjR 4 +guitar BkJ Z +gulden Bki Y +gulf Bk6 BH +gully BmB a +gulp down Bmb s +gum BnH m +gun Boa 1 +guncarriage Bnt t +gunner BpP d +gunpowder Bps o +gunsight BqU w +gurgle BrE BJ +gush BsN s +gusto Bs5 BQ +guy BuJ 2 +gymnastics Bu/ m +gypsy Bvl b +gyroscope BwA k +habit Bwk BI +habitation Bxs k +hack ByQ j +hackneyed Byz h +haddock BzU b +haemorhoids Bzv h +haemorrhoid B0Q g +hafnium B0w c +haggle B1M w +hahnium B18 c +hail B2Y S +hail from B3Y BD +hailstone B2q u +hair B75 f +hair of the head B+3 7 +hair ribbon B/y k +hairdo B4b k +hairdresser B8Y w +hairpiece B9I d +hairs B9l b +hairsplitter B4/ Bh +hairsplitting B6g BZ +hairy B+A 3 +halberd CAW e +halcyon CA0 d +half CBR b +half CEI f +halfbrother CBs 8 +halfsister CCo u +halftone CDW y +halibut CEn c +haliotis CFD h +hall CFk S +halo CF2 Bq +halt CHg CK +halyard CJq d +ham CKH P +hamburger CKW k +hamburger meat CK6 r +hamlet CLl r +hammer CNJ l +hammerhead shark CMQ 5 +hammering CNu f +hammock CON a +hamper COn Ba +hand CRK Cw +hand over CeV B+ +handbag CQB BJ +handbook CT6 4 +handful CUy n +handicap CVZ 5 +handicraft CWS 7 +handiwork CXN i +handkerchief CXv l +handle CYU C5 +handlebars CbN m +handshake Cbz m +handsome CcZ 5 +handy CdS BD +hang ChE t +hang down Ciy f +hang onto CjR 2 +hangglider CgT x +hanging lamp Chx o +hangman CiZ Z +happen CkH B3 +happiness Cl+ c +happy Cma BD +harass Cnd a +harbor Cn3 Z +harbour CoQ a +hard CpX BW +harden Cqt q +hardly CrX Bc +hardness Csz p +hardworked Coq t +hardworking Ctc Bc +hardy Cu4 h +hare CvZ T +harem Cvs Z +haricot CwF / +harm CxE CF +harmful CzJ r +harmonica Cz0 BN +harmonious C1B z +harmonize C10 BC +harmony C22 3 +harness C3t Bs +harp C5Z T +harpy C5s w +harrier C6c e +harrow C66 W +harry C7Q n +harsh C73 DC +harshness C+5 y +harvest C/r 1 +hash DAg a +hashish DA6 k +haste DBe m +hastily DCE y +hat DC2 Q +hatch DDG l +hatch out DDr h +hate DEM d +hated DEp W +haul DE/ W +haunt DFV w +have DGF BQ +have a bath DLK x +have a fever DL7 s +have a good time DMn BN +have a tendency DN0 r +have an accident DHV 5 +have an erection DIO 4 +have an interest in DJG 6 +have at ones disposal DKA BK +have bad luck DOf 5 +have compassion on DPY Bd +have confidence in DQ1 BX +have diarrhoea DSM u +have effect DS6 BO +have enough DUI y +have faith DU6 BM +have faith in DWG Bl +have good luck DXr BI +have got DYz q +have got hold of DZd v +have sex DaM 9 +have sexual intercourse DbJ BN +have the right to DcW 6 +have to DdQ 5 +hawk DeJ U +hawksbeard DfA u +hawkweed Ded j +hay Dfu P +haystack Df9 u +hazard Dgr CS +hazardous Di9 BD +haze DkA BI +hazel Dl0 X +hazelnut DmL g +hazeltree DlI s +he DnU Z +head Dnt Bg +head for Dt8 g +head of a department Duc 7 +head of a school DvX BT +head of cattle Dwq 0 +head of department Dxe 3 +head of state DyV r +head up DzA u +headache DpN c +header Dpp f +heading DqI x +headlamp Dq5 c +headlight DrV x +headmaster DsG BL +headquarters DtR r +heal Dzu s +health D0a a +health resort D1b n +healthy D00 n +heap D2C Dt +hear D5v q +hearing D6Z a +hearing aid D6z p +hearsay D7c BK +heart D8m c +heart D9C U +heartburn D9W BG +hearth D+c Bh +hearty D/9 k +heat EAh Bg +heater ECB l +heath ECm 3 +heather EDd l +heating EEC e +heaven EEg b +heavenly EE7 o +heavenly body EFj z +heavy EGW f +hectolitre EG1 s +hectometre EHh l +hedge EJA h +hedge clippers EJh q +hedge shears EKL n +hedgesparrow EIG 6 +hedonism EKy k +heel ELW c +hegoat Dmr p +height ELy h +height of the barometer EM6 / +heighten EMT n +heir EN5 i +helium EOb a +helix EO1 BE +hell EP5 P +hellebore EQI g +hello EQo s +hello ERU W +helm ERq Y +helmet ESC V +help ESX CQ +help down EX8 e +help off EYa c +helper EUn 1 +helpful EVc r +helpfulness EWH 7 +helpless EXC 6 +hemi EY2 a +hemisphere EZQ s +hemlock EZ8 f +hemorrhoid Eab f +hemp Ebo T +hempnettle Ea6 u +hen Eb7 a +hence EcV j +hens egg Ec4 c +hepatica EdU k +hepatitis Ed4 i +her Eea Bk +herald Ef+ 0 +heraldry Egy t +herb Ehf U +herd Ehz Be +here EjR a +here and there Enl r +here are EoQ BA +here is EpQ 8 +hereafter Ejr n +hereditary EkS w +heredity ElC 1 +heresy El3 c +heretic EmT a +heretical Emt e +herewith EnL a +hermit EqM n +hero Eqz b +heroic ErO n +heroin Er1 a +heroine EsP Z +heroism Eso w +heron EtY W +herpes Etu W +herring EuE j +hers Eun g +herself EvH p +hesitant Evw f +hesitate EwP B0 +hesitation EyD BR +hexadecimal EzU 1 +hexagon E0J d +hey E0m v +hey there E1V a +hibiscus E1v e +hiccup E2N n +hidden E20 BN +hide E43 Ce +hide oneself E8c BL +hideandseek E4B 2 +hideous E7V BH +high E/2 l +high school FBy j +high society FCV x +high tide FDG p +highflown E9n BI +highhandedness E+v BH +highway FAb BX +hill FDv i +hill up FFT b +hillside FER BC +him FFu BF +himself FGz n +hind FHa 1 +hind axle FNA g +hind part FNg j +hind wheel FOD j +hinder FIP Co +hindmost FK3 1 +hindrance FLs BU +hinge FOm Z +hinny FO/ Y +hint FPX g +hinterland FP3 k +hip FQb P +hippo FQq Y +hippopotamus FRC p +hire FRr Bq +hired house FTV i +hired out FT3 f +hirsute FUW e +his FU0 3 +hiss FVr b +historian FWG 4 +history FW+ z +hit FXx B/ +hitch on FZw 5 +hither Fap g +hoar FbJ n +hoarse Fbw i +hoax FcS / +hobby FdR p +hock Fd6 V +hockey FeP Y +hod Fen V +hoe Fe8 T +hog FfP Z +hogweed Ffo e +hoist FgG j +hoist up Fgp p +hold FhS BQ +hold a postmortem Fki BE +hold against FjN BV +hold back Flm Bi +hold responsible FnI BV +holder Fii r +hole Fod BH +holiday Fpk BB +holiness Fql g +hollow Frv y +holloweyed FrF q +holly Fsh W +holmium Fs3 a +holy FtR y +holy object FuD t +homage Fuw BL +home Fv7 2 +homesickness Fwx j +homosexual FxU BH +homy Fyb j +honest Fy+ 9 +honey Fz7 X +honeycomb F0S h +honeymoon F0z m +honeysuckle F1Z m +honor F1/ BD +honorary F3C Bp +honour F4r BE +hood F5v Ci +hoodlum F8R d +hoof F8u S +hook F9A Bh +hook on GAc 2 +hooker F+h BR +hooks and eyes F/y q +hoop GBS U +hoopoo GBm X +hoot after GCw l +hooter GB9 z +hop GDV 3 +hope GEM 0 +hope for GFg BN +hopefully GFA g +hops GGt e +horde GHL c +horizon GHn w +horizontal GIX 8 +horn GJT p +hornbeam GJ8 g +horned viper GKc p +hornet GLF o +horoscope GLt i +horrible GMP Ew +horror GQ/ B2 +horse GS1 q +horse GWW 2 +horsechestnut GTf BD +horsefly GUi 5 +horsepower GXM n +horseradish GVb 7 +horseradish GXz y +horseshoe GYl i +horsewoman GZH i +hospitable GZp v +hospital GaY 2 +hospitality GbO r +host Gb5 + +hostage Gc3 p +hostel Gdg i +hostess GeC q +hostile Ges p +hot GgB v +hot season Ghr l +hotchpotch Ggw m +hotel GhW V +hotheaded GfV s +hour GiQ R +houri Gih e +hours Gi/ V +hours GjU X +house Gjr b +house Gk1 Y +house charge Gn3 t +house of correction Gok 6 +house of ill fame Gpe BI +house of worship Gqm / +house to let Grl j +housed GlN 3 +housefly GmE a +housekeeper Gme o +houseowner GkG v +housewife GnG x +hovel GsI s +hover Gs0 X +how GtL 5 +how are you Gv7 7 +how do you do Gw2 + +how far Gx0 Z +how long Gy8 e +how long GyN v +how many Gza u +how much G0I g +however GuE y +howl Gu2 BF +hub G1S Y +hubcap G0o q +huddle G1q i +hue G2M 0 +hug G3A Bl +huge G4l BW +hull G57 s +hum G6n BN +human G70 c +human being G9W k +human race G96 t +humane G8Q l +humanist G81 h +humble G+n v +humble oneself G/W w +humid HAG Z +humiliating HAf 7 +humility HBa 3 +hummingbird HCR l +humor HC2 Y +humour HDO 6 +humourous HEI m +hump HEu a +humus HFI j +hunchback HGX u +hunchbacked HFr s +hundred HHF c +hundred thousand HHh z +hunger HIU a +hungry HIu d +hunt HJL s +hunt after HKP v +hunt for HK+ r +hunter HJ3 Y +hurdygurdy HLp 7 +hurray HMk W +hurricane HM6 d +hurried HNX i +hurriedly HN5 0 +hurry HOt CT +hurt HRA B5 +husband HS5 s +husk HTl o +husked rice HUN n +hustler HU0 1 +hut HVp W +hyaena HV/ Z +hybrid HWY p +hydrogen bomb HXB 4 +hygiene HX5 e +hygienic HYX u +hymn HZF d +hypnosis HZi d +hypnotic trance HZ/ s +hypnotist Har j +hypnotize HbO w +hypocrite Hb+ 2 +hypocritical Hc0 BM +hypothesis HeA BG +hypothesize HfG t +hypothetical Hfz q +hysteria Hgd g +hysterical Hg9 i +ibex Hhf X +ibis Hh2 S +ice HiI x +iceberg Hi5 c +icecream HjV 8 +icing HkR W +icon Hkn Y +iconoclasm Hk/ r +iconoclast Hlq r +icy HmV k +idea Hm5 BL +ideal HoE X +idealist Hob f +identical Ho6 f +identify HpZ 0 +identity HqN i +ideology Hqv l +idiocy HrU m +idiom Hr6 x +idiot Hsr h +idiotic HtM a +idol Htm r +idol worship Hv/ 5 +idolator HuR 5 +idolatry HvK 1 +idyll Hw4 U +if HxM n +if the occasion arises Hxz Bn +ignition Hza 9 +ignore H0X BH +iguana H1e c +il H16 i +ill H3W U +ill fate H9i f +ill humor H+B w +ill humour H+x x +illfated H2c 6 +illiterate H3q BE +illness H4u Y +illuminate H5G BZ +illusion H6f BH +illustrate H7m BF +illustration H8r 3 +im H/i i +image IAE BY +imagination IBc 3 +imagine ICT BA +imitate IDT u +imitation IEB BF +immaterial IFG v +immediate IF1 u +immediately IGj Cl +immense IJI BF +immensely rich IKN s +immerse IK5 BC +immigrate IL7 g +immune IMb u +imp INJ p +impale INy Z +impartial IOL BI +impatient IPT z +impeach of IQG r +impede IQx BD +impediment IR0 Ba +impel ITO B8 +impenetrable IVK s +imperialism IV2 v +imperialist IWl p +impertinence IXO BF +impertinent IYT + +impetus IZR Cg +implicate Ibx BF +implore Ic2 d +imply IdT / +import IeS n +important Ie5 BP +impose IgI k +impose upon Igs q +imposing IhW v +impossible IiF / +impossibly IjE 9 +imposter IkB BK +impotent IlL f +impress Ilq v +impressible ImZ BJ +impression Ini BD +impressionism Iol v +impressionist IpU s +imprint IqA l +imprison Iql u +improve IrT p +impulse Ir8 BI +impulsive ItE r +imputation Itv m +in Iu3 h +in IuV i +in a hurry M96 2 +in a way M+w v +in abeyance M00 BA +in accordance with M10 Bg +in action M3U Bk +in addition M44 B0 +in all M6s u +in all respects M7a o +in another way M8C + +in any event M9A 6 +in arabic MzA 5 +in case M/f i +in cash NAB r +in charge NAs m +in command NBS n +in consequence of NB5 BB +in detail NC6 h +in distress NDb 5 +in easy circumstances NEU Ba +in english Mz5 7 +in every respect NFu t +in every way NGb BK +in exchange for NHl BV +in fact NI6 Bj +in front of NKd f +in good condition NK8 + +in my opinion NL6 u +in my view NMo n +in no way NNP BE +in ones absence NOT v +in order to NPC BG +in principle NQI 0 +in question NQ8 BI +in rows NSE x +in short supply NS1 BE +in spite of NT5 Ba +in that way NVT BY +in the absence of NXf BM +in the abstract NYr 5 +in the beginning NZk + +in the circumstances Nai BW +in the cuban way NWr 0 +in the daytime Nb4 l +in the evening Ncd o +in the final analysis NdF 0 +in the meantime Nd5 BY +in the middle of NfR BZ +in the morning Ngq 3 +in the presence of Nhh 4 +in the rear NiZ x +in this manner NjK BM +in this way NkW BC +in time NlY r +in turn NmD BW +in vain NnZ c +in what way Nn1 9 +in which Noy i +in working order NpU 1 +inaccessible IvY 5 +inaugural IwR l +inaugurate Iw2 BB +inauguration Ix3 4 +inborn Iyv r +inbred Iza p +incandescent I0D n +incarnation I0q 3 +incendiary I1h q +incendiary bomb I2L t +incense I24 Z +incensory I3R k +inception I31 d +incest I4S l +inch I43 n +incidental I5e BK +incinerate I6o u +incite I7W DZ +incitement I+v BK +inclination I/5 8 +incline JA1 BX +inclined JCM t +inclined to travel JC5 0 +include JDt BL +including JE4 BY +inclusive JGQ h +inclusively JGx Ba +income JIL Bd +incomplete JJo x +inconceivable JKZ BB +inconsistent JLa r +inconvenient JMF + +incorrect JND z +increase JN2 EN +increasingly JSD BN +incredible JTQ 0 +incredibly insolent JUE BH +incubate JVL 0 +incubus JV/ BC +incus JXB BF +indecision JYG 6 +indecisive JZA BQ +indeed JaQ CW +indent Jcm c +independant JdC BM +indescribably JeO y +index JfA Bv +indicate Jgv Cz +indication Jji Ba +indicative Jk8 0 +indicative mood Jlw + +indict for JoF p +indictment Jmu BX +indifference Jou h +indifferent JpP t +indigence Jp8 y +indigenous Jqu l +indigestion JrT BP +indignant Jsi m +indispensable JtI 3 +indistinct Jt/ 9 +indium Ju8 Y +individual JvU BT +indivisible Jwn l +induce JxM BN +inducement JyZ m +indulge Jy/ BR +indulge in J0y s +indulgence J0Q i +industrial J1e 0 +industrial branch J2+ 3 +industrialist J2S s +industrious J31 + +industry J4z BO +inebriety J6B BP +inert J7Q x +inexpensive J8B j +inexperienced J8k y +inexplicable J9W s +inexpressibly J+C x +infant J+z W +infant in arms J/1 r +infantry J/J s +infect KAg 8 +infected KBc BO +infection KCq v +infectious KDZ BC +infer KEb BM +inference KFn x +inferior KGY BF +infernal KHd j +infertile KIA 6 +infest KI6 f +infestation KJZ i +infirm KJ7 m +infirmity of purpose KKh BS +inflammation KLz p +inflammatory KMc n +inflate KND 4 +inflated KN7 i +inflation KOd h +influence KO+ BP +influenza KQN o +inform KQ1 CH +information KS8 Bf +infuse KUb z +infusion KVO o +ingenious KV2 i +ingredient KWY BE +inhabitant KXc 6 +inhale KYW i +inherit KY4 i +inheritance KZa BT +inhibit Kat Bj +initial KcQ Bf +initial cost Kdv p +initial costs KeY r +initial deposit KfD t +initial stage Kfw t +initial velocity Kgd BG +initiate Khj BC +inject Kil o +injection KjN 3 +injure KkE Bt +injured Klx a +injury KmL 6 +ink Kn0 Q +inkpot KoE l +inkwell KnF v +inlet Kop U +inn Ko9 t +innate Kpq o +inner KqS 3 +inner tube KrJ k +innkeeper Krt 5 +innocence Ksm + +innocent Ktk BA +inoculate Kuk x +inoffensive KvV i +inordinate Kv3 BH +inorganic Kw+ l +input Kxj V +inquiry Ky2 g +inquiryoffice Kx4 + +inquisitive KzW BC +inquisitively K0Y BG +insane K1e BQ +insane asylum K2u r +insanity K3Z B2 +insatiable K5P j +inscription K5y m +insect K6Y X +insect repellent K6v / +insert K7u Bd +inside K9L C0 +insidious K// g +insignia LAf t +insignificant LBM CB +insinuate LDN i +insist LDv b +insistence LEK h +inspect LEr BF +inspection LFw BP +inspector LG/ w +inspiration LHv BC +inspire LIx Bt +inspiring LKe j +install LLB z +installment payment LL0 2 +instalment LMq BL +instant LN1 BH +instantly LO8 Bd +instead LQZ j +instead of LQ8 z +instigate LRv BJ +instigator LS4 Bb +instinct LUT o +institute LU7 BE +institution LV/ p +instruct LWo z +instruction LXb Bn +instructor LZC BK +instrument LaM u +instruments of murder La6 7 +insubordinate Lb1 Bh +insubstantial LdW l +insulate Ld7 q +insulation Lel v +insulator LfU i +insult Lf2 Bs +insulted Lhi n +insulting LiJ t +insurance Li2 x +insurance policy Ljn w +insure LkX 4 +insurer LlP BA +integer LmP i +integral Lmx 6 +integrated circuit Lnr BJ +intellect Lo0 x +intellectual Lpl BQ +intelligence Lq1 BZ +intelligent LsO BE +intelligibility LtS BJ +intend Lub CQ +intense Lwr v +intensive Lxa z +intention LyN BQ +intentional Lzd 7 +inter L0Y t +intercede L1F j +intercessor L1o t +interchange L2V Bc +interest L3x B2 +interested L5n 4 +interesting L6f Bj +interface L8C g +interfere with L9I BM +interference L8i m +interior L+U s +interjection L/A t +interment L/t BK +intermission MA3 o +intermittent MBf y +internal MCR + +internal combustion engine MDP Bd +international MEs u +international express MFa 4 +international express train MGS BD +interpret MHV BP +interpretation MIk BK +interpreter MJu / +interpunction MKt 6 +interrogate MLn Bq +interrupt MNR 8 +interruption MON BD +interval MPQ s +intervene MP8 BE +interview MRA BB +intestine MSB a +intestines MSb i +intimate MS9 BB +intimidation MT+ q +into MUo m +into the heights MZC t +intolerant MVO q +intone MV4 t +intoxicate MWl k +intoxicated MXJ 7 +intoxicating liquor MYE + +intractable MZv l +intrepid MaU BR +intrigue Mbl 1 +introduce Mca BZ +introduction Mdz 2 +intrude Mep BM +invade Mf1 r +invasion Mgg j +invective MhD u +invent Mhx Bl +invention MjW w +inventor MkG f +inventory Mkl r +inverted comma MlQ y +inverted commas MmC 1 +invest Mm3 x +investigate Mno BY +investigation MpA B5 +investment Mq5 w +investor Mrp s +invincible MsV 3 +invitation MtM x +invite Mt9 1 +invite tenders for Muy z +invoice Mvl Bu +invoke MxT + +inwards MyR v +iodine NqJ a +iota Nqj U +iridium Nq3 b +iris NrS Z +iron Nrr Bc +iron clothes NvJ z +iron wire Nv8 j +ironic NtH b +ironing board Nti u +ironwork NuQ e +irony Nuu b +irradiate Nwf g +irradiation Nw/ n +irresistible Nxm r +irresolute NyR m +irresolution Ny3 BC +irrigate Nz5 BO +irritating N1H 4 +island N1/ Y +isnt it N2X y +isnt it N3J z +isnt that so N38 9 +isolate N45 p +isolated N5i x +isolation N6T BL +issue N7e Bq +it N9I BU +it doesnt matter OCb Bp +it is rumoured that OEE 2 +it is understood that OE6 6 +itch N+c BA +item N/c / +its OAb d +its a matter of OBd + +itself OA4 l +ivory OF0 z +ivy OGn U +jack OG7 BG +jack off OJF 9 +jackal OIB d +jacket OIe n +jade OKC BX +jaguaro OLZ d +jail OL2 BH +jam OM9 i +jargon ONf 0 +javelin OOT w +jaw OPD d +jawbone OPg l +jaws OQF x +jay OQ2 f +jealous ORV BE +jealousy OSZ p +jeer OTC k +jeer at OTm BK +jellyfish OUw w +jennyass OVg k +jerk OWE m +jest OWq t +jester OXX 0 +jet plane OYL s +jewel OY3 BJ +jeweller OaA h +jingle Oah BL +job Obs d +job OcJ CC +jocular OeL m +jog Oex + +join Ofv EE +joining Ojz y +joint Okl B0 +joke OmZ B7 +jolt OoU Y +jot Oos S +journal Oo+ s +journalist Opq Bj +journey OrN u +joy Or7 BJ +joyful OtE BD +joystick OuH q +jubilant Oux i +jubilation OvT m +jubilee Ov5 y +judge Owr Bi +judgement OyN h +judgment Oyu 8 +jug Ozq Bb +juggle O1F b +juggler O1g e +juice O1+ U +jumbled up O3M i +jumblesale O2S 6 +jump O3u l +jump off O4T k +junction O43 2 +june bug O5t g +jungle O6N r +junior O64 q +junto O7i t +jurisprudence O8P o +jurist O83 b +jury O9S X +just O9p D1 +just now PCf BA +just one PDf x +justice PBe BB +jute PEQ U +kangaroo PEk i +kappa PFG V +karma PFb X +kea PFy P +keel PGB S +keen PGT BO +keep PHh E0 +keep an eye on PPh BC +keep at a distance PQj 8 +keep at bay PRf u +keep back PSN j +keep burning POm 7 +keep house PSw 1 +keep in PTl b +keep on PUA BJ +keep ones hands off PVJ BG +keeper of the records PMV 6 +keepsake PNP BX +kennel PWP f +kerchief PWu g +kernel PXO b +kerosene PXp f +kettle PYI 8 +key PZE BA +keyboard PaE r +kibbutz Pav b +kick Pby v +kick down Pch e +kick it Pc/ Z +kick off PdY p +kickoff PbK o +kid PeB p +kidney Peq W +kill PfA n +kill oneself Pfn BI +kiln Pgv Z +kilo PhI e +kilobyte Phm c +kilometre PiC i +kilt Pik e +kimono PjC Z +kind Pjb DU +kindle Pmv BI +kindness Pn3 / +king Po2 q +kingdom Ppg e +kingfisher Pp+ i +kingship Pqg g +kip PrA O +kirk PrO f +kiss Prt q +kiss and be friends again PsX 6 +kitchen PtR q +kite Pt7 i +kitten Pud U +klaxon Pux n +knapsack PvY k +knapwood Pv8 d +knead PwZ V +knee Pwu R +kneel Pw/ x +knife Pxw R +knight PyB h +knit Pyi S +knitting needle Py0 y +knob Pzm 1 +knock P1S BY +knock at the door P2q v +knock off P3Z BD +knockkneed legs P0b 3 +knot P4c BV +know P5x BK +know how P8L + +knowledge P67 BQ +kopje P9J W +krypton P9f b +label P96 c +labor agreement QC6 6 +labor dispute QD0 x +labor exchange QEl BG +labor market QFr s +labor movement QGX 1 +laboratory P/V p +laboratory assistent P/+ 7 +laborer QA5 BM +laborers QCF 1 +laborintensive P+W / +labour QJV u +labour agreement QMC 7 +labour contract QM9 4 +labour dispute QN1 y +labour exchange QOn BH +labour market QPu t +labour movement QQb 2 +labour pains QRR n +labourer QKD BL +labourers QLO 0 +labourintensive QHM BA +labourunion QIM BJ +lace QR4 R +lack QSJ Bj +lackey QTs k +lacquer QUQ u +lad QU+ Z +ladder QVX Y +laddie QVv f +ladies and gentlemen QWO BR +lady QYW 1 +lady help Qcw y +ladybug QZL BD +ladyinwaiting QXf 3 +ladylike QaO 5 +ladys bag QbH u +ladys mantle Qb1 7 +laggard Qdi g +lagoon QeC j +laid up Qel d +lake QfC i +lamb Qfk r +lambda QgP X +lame Qgm j +lament QhJ y +lamp Qio S +lampshade Qh7 t +lance Qi6 x +land Qjr CC +land tortoise Qqy u +landing QnN j +landingstage Qlt Bg +landlady Qnw g +landlord QoQ 5 +landmark QpJ g +landscape Qpp h +landslide QqK o +lane Qrg S +language Qry c +lanky QsO m +lantern Qs0 c +lanthanum QtQ i +lap Qty 6 +lapse Qus a +lapsed QvG BK +larch QwQ p +larder Qw5 g +large QxZ X +large spoon Qxw m +lark QyW Y +larkspur Qyu l +larva QzT X +laryngitis Qzq r +larynx Q0V g +lass Q01 Z +lasso Q1O V +last Q1j EC +last days of autumn Q7v 2 +last night Q8l BU +last week Q95 0 +lasting Q5l CK +late Q+t i +late at night RBj t +late in the afternoon RCQ BA +late in the morning RDQ 7 +lately Q/P x +later RAA W +later on RAW 3 +latex RBN W +lath REL T +lathe REe n +lather RFF h +latrine RFm c +laud RGC BJ +laugh RHL h +laugh at RIe f +laughter RHs y +launch RI9 Bb +laundress RKY g +laundry RK4 f +laundry mark RLX r +laurel RMC h +lava RMj j +lavender RNG g +law RNm c +law of nature RSW s +lawless ROC 4 +lawn RO6 6 +lawrencium RP0 m +lawsuit RQa BJ +lawyer RRj z +laxative RTC r +lay RUk / +lay a distress upon RYl 6 +lay before RZf o +lay down RaH BG +lay hold of RbN BE +lay on top of RcR u +lay out Rc/ y +layby RTt 3 +layer RVj Bg +layer of earth RXD 6 +layman RX9 o +laziness Rdx d +lazy ReO T +lead Reh Bo +lead away RjO h +leader RgJ Bj +leading Rhs 0 +leading article Rig u +leaf Rjv X +leaf through RlK + +leaflet RkG BE +league RmI u +lean Rm2 Bv +lean against Rol s +leap Rp8 j +leap down Rqf h +leap upon RrA BJ +leapyear RpR r +learn RsJ f +learn of Rtw l +learned Rso s +learnt RtU c +lease RuV BZ +least Rvu s +leasurely Rwa BC +leather Rxc BF +leather goods Ryh y +leave RzT Do +leave behind R3O z +leave in abeyance R4B y +leave out R4z BG +leave out of account R55 CY +leaven R27 T +leaving certificate R8+ 2 +leaving examination R90 5 +leavings R8R t +lectern R+t m +lector R/T Y +lecture R/r CA +lecturer SBr f +leech SCK c +leek SCm S +left SC4 Bs +left out SEk e +left over SFC x +left side SFz h +leg SGU l +legal SG5 0 +legation SHt e +legend SIL u +legendary SI5 n +legging SJg Z +legible SJ5 d +legion SKW a +legitimize SKw u +leisurely SLe z +leitmotif SMR l +lemming SM2 j +lemon SOH X +lemon juice SO+ n +lemonade SOe g +lemontree SNZ u +lend SPl 6 +length SQf p +lengthen SRI BW +lengthwise SSe w +leniency STO BZ +lenient SUn 7 +lens SVi R +lentil SVz V +leopard SWI i +leper SWq u +leprosy SXY o +less SYA X +less and less Sa6 7 +lessen SYX k +lesser bindweed SZ6 u +lesserthan sign SY7 / +lesson Sao S +let Sb1 BR +let know Skq By +let off Smc m +lethargy SdG BW +lets start Sj2 0 +letter Sec BN +letter of credit SgQ v +letter of introduction Sg/ BE +letter of recommendation SiD BH +letterbox Sfp n +lettuce SjK s +level SoG EM +level crossing SsS BE +levelcrossing SnC BE +lever StW BH +lewd Sud 3 +lewdly SvU b +liability insurance Svv BO +liberal Sw9 n +librarian Sxk q +library SyO s +licence Sy6 2 +license Szw BO +licentious S0+ 9 +lick S17 S +lid S2N q +lie S23 1 +lie down S4Q BI +lie encamped S5Y u +lieutenant S3s k +life S7f a +lifebelt S6G t +lifebelt S75 j +lifeboat S6z s +lifeless S8c k +lift S9A Ch +ligament S/h b +ligature S/8 r +light TBo De +light up TKR 3 +lighten TFG u +lighter TF0 n +lighthouse TGb 2 +lighting THR e +lightly THv BO +lightning TI9 BU +lightswitch TAn BB +likable TLI q +like TLy Cp +like that TQe BC +like this TRg B5 +likely TOb h +likewise TO8 Bi +lilac TTZ X +lily TU7 U +lilyofthevalley TTw BL +limb TVP Y +lime TVn m +lime TW9 1 +limetree TWN w +limit TXy BZ +limitation TZL w +limited TZ7 s +limited liability Tan / +limited liability company Tbm BV +limp Tc7 BM +lindentree TeH 1 +line Te8 CU +line up TjR BD +lineage ThQ u +linen Th+ q +lineseed oil Tio p +lingo TkU Z +linguist Tkt 6 +linguistic Tln BB +lining Tmo Z +link TnB 0 +link together ToO w +link up To+ j +linker Tn1 Z +linoleum Tph q +lion TqL U +lip Tqf O +liquefied petroleum gas Tqt / +liqueur Trs a +liquid TsG v +liquid manure Tu+ w +liquidate Ts1 CJ +liquor Tvu BB +lira Twv U +lisp TxD V +list TxY BF +listen Tyd BH +listen in T0O f +listener Tzk q +litany T0t b +liter T1I V +literal T1d r +literary T2I 6 +literature T3C / +lithium T4B c +litigate T4d f +litre T48 X +litter T5T g +little T5z 9 +little bell T6w s +little bird T7c i +little boat T7+ e +little brother T8c o +little by little T9E BL +little owl T+P o +little present T+3 1 +little stick T/s o +liturgy UAU f +live UAz BB +live in lodgings UFH 0 +live on UF7 f +live through UGa 4 +live to see UIN BG +live together UHS 7 +live upon UJT j +lively UB0 Ba +liver UDO V +liver sausage UDj q +livestock UEN 6 +living alone UK5 p +living room ULi k +livingroom UJ2 BD +lizard UMG Y +load UMe BU +loaded with UNy w +loaf UOi X +loafer UO5 t +loan UPm r +loathe UQR / +lobe of the ear URQ n +lobster UR3 BE +local US7 i +locate UTd 5 +location UUW / +loch UVV u +lock UWD Bi +lock up UYq + +locked UXl j +locksmith UYI i +locomotive UZo k +locust UaM d +lofty Uap g +log in Ucz Z +log out UdM c +logic UbJ Y +logical Ubh c +logical reasoning Ub9 2 +loin Udo a +lonely UeC a +long Uec 0 +long for UiS BW +longan UfQ a +longboat Ufq c +longing UgG Bb +longitude Uhh x +look Ujo D3 +look after UoM CM +look askance UqY 9 +look at UrV Bn +look down Us8 0 +look for Utw BL +look forward Uu7 r +look forward to Uvm 0 +lookingglass Unf t +loom Uwa Y +loop Uwy r +loopy Uxd BJ +loose Uym 9 +loose cover Uzj g +lord U0D x +lorry U00 v +lose U1j Bx +lose flesh U3U t +lose in weight U4B y +lose ones way U4z z +loss U5m Bf +lost U7F BG +lot U8L a +lottery U8l o +lotus U9N W +loud U9j X +loudly U96 f +lounge U+Z d +lounge suite U+2 m +louse U/c S +lovable U/u BD +love VAx Cy +love of ones neighbour VL9 BQ +loved VDj e +loved one VEB BP +lovely VFQ Ep +lover VJ5 CE +loving VNN d +low VNq BA +low relief VWF m +low tide VWr W +lower VPq Br +lower case letter VR0 9 +lower course VSx o +lower middleclass VTZ 3 +lower part VUQ 5 +lower part of the body VVJ 8 +lowerarm VOq BA +lowering VRV f +loyal VXB 0 +luck VX1 Bq +luggage VaO a +luggage ticket Vao s +luggagevan VZf v +lukewarm VbU i +lull Vb2 T +lullaby VcJ p +lump Vcy Bz +lump of earth Vel p +lunacy VfO B0 +lunar VhC k +lunatic Vhm x +lunch ViX BD +lung Vja S +lungwort Vjs f +lupine VkL Z +lure Vkk U +lurid Vk4 4 +lust Vlw Bd +lute VnN S +lutetium Vnf g +luxurious Vn/ t +luxury Vos Bi +lyceum VqO b +lye Vqp Q +lyingin room Vq5 1 +lyingin woman Vru 3 +lynx Vsl Y +lyre Vs9 T +lyric VtQ U +macabre Vtk BF +macaroni Vup i +macaw VvL o +mach Vvz T +machine VwG c +machine of government VxM BA +machinery Vwi q +mackerel VyM c +mad Vyo Bi +madam V0K Y +madness V0i By +maestro V2U z +magazine V3H BT +magic V4a BK +magical V5k p +magician V6N BF +magnanimous V7S 0 +magnesium V8G x +magnet V83 Z +magnetic V9Q g +magnetism V9w k +magnetize V+U k +magnificent V+4 BF +magnifying glass V/9 6 +magpie WA3 Y +maid WBP BF +maidenhair WCU 4 +mail WDM d +mailbox WDp k +mailman WEN p +main WE2 l +mainland WFb 2 +mainly WGR / +maintain WHQ B6 +maintenance WJK BN +maize WKX Z +majestic WKw BC +majesty WLy BY +major WNK 1 +major road WP4 BB +majoram WN/ r +majority WOq BO +make WRy CJ +make a bad decision WYs BX +make a contract WaD BE +make a deposit WbH 1 +make a down payment Wb8 + +make a draft Wc6 x +make a list Wdr 0 +make a mistake Wef BW +make a night of it Wf1 x +make a noise Wgm BP +make a note Wh1 BJ +make a project of Wi+ BE +make a stroke WkC 5 +make a uvular r Wk7 BD +make a will Wl+ q +make acquaintance WT7 6 +make an appeal to WU1 5 +make an appointment WVu BJ +make an offer of WW3 BB +make available WX4 0 +make bitter Wmo l +make clean WnN BJ +make eyes at WoW h +make fast Wo3 BX +make for WqO 1 +make fun of WrD 0 +make glad Wr3 s +make love Wsj By +make money WuV o +make music Wu9 y +make no account of Wvv 0 +make smooth Wwj BP +make the sign of the cross Wxy BB +make up Wyz Br +make up ones mind W0e 9 +make use of W1b BZ +make war W20 q +make war against W3e t +make war on W4L l +makeup WQ5 5 +making W4w BS +malaria W6C f +male W6h 6 +male cat W7b a +male cook W71 Y +malfunction W8N Bb +malicious W9o BX +mallet W+/ v +mallow W/u s +malt XAa U +mam XAu k +mammal XBS Y +mammy XBq n +man XCR 5 +man of genius XcE p +mana XDK T +manage XDd Bt +manageability XFK BK +manageable XGU 0 +management XHI BV +manager XId Bw +mandate XKN w +maneuver XK9 e +manganese XLb i +manger XL9 BB +mango XN4 e +mangosteen XOW n +mangotree XM+ 6 +manicure XO9 h +manifacture XPe BY +manifest XQ2 BA +manipulate XR2 8 +mankind XSy m +manna XTY V +manner XTt m +manoeuvre XUT / +mansion XVS e +mantel XVw i +mantelpiece XWS s +mantle XW+ c +manufacture XXa B0 +manure XZO Y +manuring XZm i +manuscript XaI 5 +many XbB g +manœuvre Xbh j +map Xct h +maple XdO h +maple Xdv z +marble Xei z +march XfV s +march off XgB BB +march on XhC v +margarine Xhx m +margin XiX m +marguerite Xi9 k +marigold Xjh h +marijuana XkC k +marionette Xkm h +maritime XlH m +mark Xlt C1 +mark down Xrr h +mark of cain XsM p +mark out Xs1 f +mark with a cross XtU x +market Xoi Bg +market trader Xq9 u +marketplace XqC 7 +marmelade XuF u +marriage Xuz BJ +married Xv8 i +married people Xwe 0 +marry XxS Ca +marsh Xzs u +martagon lily X0a w +marten X1K Y +martial X1i w +martyr X2S 8 +marvel X3O z +marvel at X4B 5 +mascara X46 f +masculine X5Z h +mashed potatoes X56 5 +mask X6z Bk +masked ball X8X 4 +mason X9P Y +masonic X9n m +masonry X+N h +masquerade X+u 1 +mass X/j Bn +mass produce YDs / +massacre YBK Bf +massage YCp q +massive YDT Z +mast YEr U +master YE/ Cc +master builder YH7 8 +masterful YHb g +masturbate YI3 l +mat YJc a +match YLx 4 +matchmake YJ2 o +matchmaker YKe BT +mate YMp r +material YNU Ba +materialize YOu + +materials YPs o +maternal aunt YQU + +maternal grandfather YRS Bt +maternal grandmother YS/ BD +maternal grandparents YUC BP +maternal uncle YVR 7 +mathematical YWM 2 +mathematician YXC 7 +mathematics YX9 z +matrimony YYw BM +matrix YZ8 s +matron Yao d +matter YbF B/ +mattress YdE b +mature Ydf r +matzo YeK g +maw Yeq o +maxim YfS t +maximum Yf/ 4 +may Yg3 n +maybe Yhe BC +mayhap Yig BF +mayor Yjl BB +me Ykm 2 +mead Ylc X +meadow Ylz BF +meager Ym4 Bt +meal Yol BW +mealies Yp7 e +mean YqZ C2 +meander YtP p +meaning Yt4 Bm +meaningless Yve BI +means Ywm BH +meanwhile Yxt Bi +measles YzP a +measurable Yzp k +measure Y0N Bh +measure out Y4T q +measured Y1u BK +measurement Y24 Bb +meat Y5w T +meatfly Y49 z +mechanic Y6D z +mechanical Y62 z +mechanics Y7p y +medal Y8b f +meddler Y86 5 +meddlesome Y9z y +meddlesomeness Y+l 8 +mediate Y/h p +mediation ZAK 4 +medical ZBC BA +medicine ZCC BE +medick ZDG a +meditate ZDg 5 +meditative ZEZ i +medly ZE7 b +meekness ZFW BV +meercat ZGr b +meet ZHG C3 +meet each other ZLM BJ +meet one another ZMV BL +meet with ZNg BW +meeting ZJ9 BP +melancholy ZO2 CN +melilot ZRD g +mellowness ZRj 1 +melodrama ZSY l +melodramatic ZS9 u +melody ZTr 4 +melon ZUj W +melt ZU5 CE +melting ZW9 X +member ZXU BH +member of parliament ZZA Bh +membership ZYb l +memento Zah BV +memorable Zb2 v +memorize Zcl BN +memory Zdy Bi +menace ZfU BK +menacing Zge t +mend ZhL BD +mendelevium ZiO l +mendicant friar Ziz w +mendicant order Zjj v +mental ZkS j +mentally deficient Zk1 3 +mention Zls BZ +menu ZnF u +merchandise Znz 9 +merchant Zow 1 +merciful Zpl z +mercifulness ZqY 7 +mercury ZrT o +mercy Zr7 u +mere Zsp i +merit ZtL BN +merlin ZuY a +merry ZwF Bv +merrygoround Zuy BT +mesh Zx0 q +mess Zye b +message Zy5 Bj +messenger Z0c v +metal Z1L i +metalled Z1t 0 +metallic Z2h c +metamorphosis Z29 Bp +metaphor Z4m y +meteoric stone Z5Y BD +meteorite Z6b / +meteorology Z7a 7 +method Z8V Z +method of preparation Z9a BB +methodical Z8u s +methylated spirit Z+b 0 +metre Z/P p +metric Z/4 Y +metropolis aAQ Bl +mica aB1 c +microbe aCR b +microfilm aCs i +micrometre aDO m +microphone aD0 j +microscope aEX l +mid aE8 a +midday aFW d +midday meal aFz 4 +middle aHn BS +middleaged aGr 8 +middleclass aI5 m +middleclass citizen aJf BI +middy aKn Z +midgebite aLA q +midget aLq 0 +midnight aMe f +midnight mass aM9 l +midshipsman aNi l +midwife aOH v +migrant aO2 2 +migrate aPs + +mild aQq BG +mildness aRw BV +mile aTF R +milepost aTW e +milestone aT0 f +military aUT 7 +military division aVO 8 +milk aW3 e +milkpowder aWK t +mill aXV m +millenium aX7 4 +millennium aYz i +miller aZV h +millet aZ2 W +millibar aaM f +millimetre aar l +milliner abQ q +million ab6 a +millionaire acU l +mince adn Y +minced meat ad/ g +mincepie ac5 u +mind aef Bj +mine agC 1 +mine sweeper ajD p +minefield ag3 i +minelayer ahZ k +miner ah9 a +mineral aiX s +mingle ajs Bm +minimum alS y +mining anD a +miningengineer amE / +minister and q +ministerial aoH q +ministry aox k +minority apV BV +mint arX 1 +mintmaster aqq t +minuend asM c +minus aso b +minus sign atD i +minute atl W +minutes at7 BA +miracle au7 i +miraculous avd Bb +mirage aw4 x +mirror axp q +miscalculate ayT BT +miscarriage azm s +miscarry a0S CB +miscellaneous a2T 4 +mischievous a3L CF +miser a5Q 2 +miserable a6G GY +miserably bAe h +miserliness bA/ BQ +miserly bCP BO +misery bDd BJ +misfeasance bEm l +misleading bFL v +miss bF6 r +mission bGl s +missionary bHR w +mist bIB m +mistake bIn z +mistaken bJa 3 +mistletoe bKR n +mistress bK4 p +misty bLh v +misunderstand bMQ + +misunderstanding bNO y +mite bOA R +miter bOR X +mitre bOo X +mitten bO/ Z +mix bPY Bh +mixture bQ5 BA +mizzle bR5 Z +mnemonic bSS f +moan bSx c +mobile bTN z +mobilize bUA h +mock bUh Bv +mockery bWQ BS +mode bXi 5 +model bYb Bh +modem bZ8 V +moderate baR Bo +moderation bb5 m +modern bcf v +modernize bdO k +modest bdy BI +modestly be6 2 +modesty bfw B5 +modify bhp o +moist biR q +moisture bi7 o +mold bjj BY +mole bk7 Q +molecule blL g +molybdenum blr l +mom bmQ i +moment bmy BG +momentarily bn4 BY +momentary bpQ l +mommy bp1 n +monastery bqc 0 +monetary system brQ w +monetary union bsA + +monetary unit bs+ u +money bvO b +money box bvp u +money order bwX p +moneybox bts z +moneygrubber buf v +mongoose bxA 0 +mongrel bx0 t +monitor byh g +monk bzB r +monkey b1S X +monkey puzzle b1p BQ +monkeylike bzs s +monkeywrench b0Y 6 +monkshood b25 r +monocle b3k k +monogamy b4I j +monologue b4r u +monopolize b5Z w +monopoly b6J x +monotonous b66 3 +monsignor b7x k +monster b8V CA +monstrance b+V j +monstrosity b+4 w +monstrous b/o BB +montbretia cAp o +month cBR V +monthly cBm u +monthly nurse cCU n +monument cC7 t +monumental cDo l +mood cEN BZ +moon cFm a +moon cGA S +moor cGS Ba +moorage cHs 2 +moorhen cIi c +mooring cI+ z +moose cJx U +moped cKF Y +moraine cKd j +moral cLA BS +moralist cMS 7 +morality cNN 9 +morals cOK w +moratorium cO6 4 +mordant cPy b +more cQN g +more and more cT+ BP +more or less cVN o +more precisely cV1 r +more than cWg 0 +morel cQt a +morello cRH Z +moreover cRg CB +mores cTh d +morganatic cXU p +morganatical cX9 s +morning cYp k +morning star cZN r +morning time cZ4 p +morose cah 8 +morpheme cbd e +morphia cb7 c +morphine ccX e +morphine addict cc1 t +morphinomaniac cdi w +morphology ceS q +morsel ce8 h +mortal cfd c +mortar cf5 j +morter cgc 3 +mortgage chT g +mortician chz x +mosaic cik b +mosaic work ci/ m +moselle cjl c +mosque ckB W +mosquito cmn f +mosquito net cnG BW +mosquito repellant coc BH +mosquitonet ckX BE +mosquitonetting clb BM +moss cqN Q +mossgreen cpj q +mossy cqd X +most cq0 b +most charming csG v +most important cs1 8 +most of ctx 8 +mostly crP 3 +motel cut V +motet cvC V +moth cv/ x +motheaten cvX o +mother cxp q +motherinlaw cww 5 +mothproof storage bag cyT 9 +motion czQ j +motion picture c1E y +motionless czz BR +motivation c12 l +motive c2b BG +motor c7A W +motor road c/s BO +motor tyre dA6 k +motor vehicle dBe 7 +motorboat c3h t +motorcycle c7W r +motorcycle policeman c4O BK +motorcyclist c5Y 2 +motorial c8B j +motoring c8k k +motorist c9I 4 +motorize c+A i +motorlaunch c6O y +motorway c+i BK +motto dCZ BT +mould dDs BZ +mouldy dFF BH +mouldy smell dGM o +mount dG0 w +mountable dHk h +mountain dKE X +mountain air dNv l +mountain chain dOU 3 +mountain climber dPL + +mountain hut dQJ i +mountain lion dQr j +mountain pass dRO w +mountain pasture dR+ 2 +mountain path dS0 y +mountain peak dTm k +mountain range dUK p +mountain ridge dUz Bi +mountain slope dWV p +mountain top dW+ i +mountainash dIF BC +mountainash berry dJH 9 +mountaineer dKb 1 +mountaineering dLQ BG +mountaineering boot dMW 1 +mountainous dNL k +mounted dXg a +mounting dX6 BA +mourn dY6 7 +mournful dZ1 BF +mourning da6 a +mouse dbU S +mousepad dbm e +mousetrap dcE f +moustache dcj l +mouth ddI 8 +mouthful deE l +move dep EK +movement diz v +movie dji + +movie theatre dkg z +moving dlT Bb +mow dmu a +mozaic floor dnI t +mr gm Q +mrs g2 S +mu dn1 M +much doB T +muckwater doU x +mucous dpF o +mucus dpt Y +mud dqw l +muddy drV a +mudguard drv 2 +mudstained dqF r +muezzin dsl n +mug dtM s +mulatto dt4 r +mulberrytree duj BA +mule dvj x +muleteer dwU p +mullein dw9 m +multi dxj c +multicoloured dx/ w +multinational dyv v +multiple dze d +multiply dz7 BM +multitasking d1H t +multitude d10 BZ +mummification d3N t +mummy d36 X +munition d4R r +muntjac d48 o +mural d5k z +mural painting d6X BE +murder d7b z +murderer d8O w +murderess d8+ k +murderous weapon d9i x +murk d+T q +murmur d+9 CH +muscatel eBE i +muscle eBm V +muscular eB7 g +muse eCb T +muser eCu k +museum eDS b +mush eDt d +mushroom eEK 2 +music eIz Y +musical eJ9 2 +musical box eLe o +musical club eMG v +musical evening eM1 w +musical instruction eNl 9 +musical instrument eOi BG +musical performance ePo + +musical society eQm 5 +musical time eRf 3 +musicalbox eJL y +musicality eKz r +musicbook eFA t +musicbox eFt t +musician eSW BE +musiclesson eGa x +musiclover eHL 2 +musicology eTa y +musicstand eIB y +musk eWI U +muskdeer eUM o +musket eWc Y +musketeer eW0 i +muskox eU0 k +muskrat eVY w +musky smell eXW n +muslin eX9 m +musquash eYj q +mussel ean W +musselbank eZN u +musselbed eZ7 s +must ea9 / +mustard ecp b +mustard pot edE m +mustard sauce edq r +mustard seed eeV p +mustardgas eb8 t +musterroll ee+ v +musty eft o +mutation egV h +mute eg2 s +mutineer ehi y +mutiny eiU x +mutter ejF BH +mutton ekM b +mutual ekn 4 +mutual understanding emG BO +mutually elf n +muzzle enU BA +my eoU X +myriad eor Z +myrrh epE V +myself epZ h +mysterious ep6 BC +mysteriousness eq8 0 +mystery erw z +mystic esj n +mystical etK b +mysticism etl j +mystification euI BD +mystify evL BF +myth ewQ S +mythical ewi c +mythologist ew+ o +mythology exm n +myxomatosis eyN p +nabob ey2 W +nadir ezM h +nag ezt P +naiad ez8 h +nail e39 BA +nail clipper e49 o +nail clippers e5l p +nailbed e0d l +nailbrush e1C u +nailfile e1w r +nailscissors e2b z +nailvarnish e3O v +naked e6O y +nakedness e7A h +name e8w m +nameday e7h k +nameless e9W m +namely e98 u +nameplate e8F r +namesake e+q f +nandu e/J Y +nanometre e/h j +nap fAE m +nape fAq Q +naphthalene fA6 m +naphthaline fBg o +naphthol fCI c +napkin fCk Y +narcissus fC8 f +narrate fDb x +narrative fEM 9 +narrow fFJ x +narrow space fF6 v +nasal fGp e +nasturtium fHH k +nasty fHr Gz +nation fOe h +national fO/ r +nationality fPq t +nationalization fQX x +nationalize fRI 1 +native fR9 By +natural fTv g +natural gas fVL l +natural law fVw o +naturally fUP 8 +nature fWY BE +naught fXc b +naughty boy fX3 BQ +nausea fZH BK +nauseate faR BM +nauseous fbd BS +naval cadet fcv m +nave fdV Z +navel fdu W +navigate feE r +navigation fev BB +navy ffw g +navy blue fgQ l +naïf fg1 5 +naïve fhu 8 +near fjr Bn +near to frD 0 +nearby flS Cv +nearer foB n +nearest foo m +nearly fpO BX +nearness fql e +nearsighted fiq BB +neat fr3 s +necessaries fsj o +necessary ftL BI +necessity fuT y +neck fvF V +necklace fva 9 +necktie fwX f +nectar fw2 Y +need fxO Bm +needle fy0 5 +needless fzt q +needlewoman f0X k +needlework f07 BR +needy f2M x +negative f29 3 +negatively f30 l +neglect f4Z Ca +neglectful f6z 8 +negligence f7v BX +negligent f9G BJ +negotiate f+P 8 +neigh f/L V +neighborhood f/g l +neighbour gAF BJ +neighbour on the lower storey gDp BK +neighbourhood gBO BQ +neighbouring gCe BL +neither gEz BO +neither gGB j +neither nor gGk t +nematode gHR p +neodymium gH6 i +neologism gIc p +neon gJF i +nephew gJn h +neptunium gKI k +nerve gKs V +nervous gLB 2 +nest gL3 w +net gMn 1 +nettle gNc g +network gN8 h +neutral gOd 4 +never gPV d +never mind gQ+ BK +nevertheless gPy BM +new gSI c +newness gSk e +news gTC BC +newspaper gUE 9 +next gVB Ch +next of kin gXi 8 +next to gYe BM +next week gZq 5 +nibble gaj j +nice gbG C8 +nice guy geC n +nice smell gep l +niche gfO S +nickel gfg V +nickname gf1 b +niece ggQ U +niggard ggk 4 +nigger ghc 2 +niggler giS BP +night gvN T +night club g0y h +night flight g1T l +night is falling g14 x +night prayers g2p n +night rate g3Q i +night spot g3y h +night tariff g4T m +nightattire gjh w +nightblind gkR t +nightblindness gk+ 5 +nightdress gl3 s +nightduty gmj w +nightfrost gnT t +nightgown goA r +nightie gvg d +nightingale gv9 l +nightjar gwi y +nightlamp gor 4 +nightmare gxU BI +nightmarish gyc u +nightmoth gpj 5 +nights rest g0P j +nightschool gqc u +nightservice grK y +nightshade gzK j +nightshift gr8 + +nightshirt gs6 7 +nighttrain gt1 t +nightwatch gui r +nightwatch gzt i +nimbus g45 m +nine g5f S +nineteen g5x f +nineteenth g6Q l +ninety g61 b +ninth g7Q X +niobium g7n b +nip g8C c +nipple g8e g +nipplewort g8+ j +nitpicker g9h Bc +nitrogen g+9 i +no hBG e +no doubt iAP w +no longer iA/ 1 +nobelium hBk f +nobility hCD / +noble hDC a +nobleman hDc d +nobleness hD5 f +nobody hEY 7 +nocturnal hFT j +nocturnal animal hF2 x +nod hGn f +node hHG 2 +noise hH8 z +noiseless hIv 9 +noisy hJs BB +nomad hKt i +nomadic hLP o +nominate hL3 e +nomination hMV BS +nonalcoholic hNn 2 +nonattendance hOd 0 +noncom hS/ g +noncommissioned officer hPR BP +none hTf z +nonmilitary hQg 6 +nonsense hUS B1 +nonskid hRa r +nonskid chain hSF 6 +noodles hWH Z +noon hWg a +noone g/f BB +noones hAg m +nor hW6 Z +norm hXT p +normal hX8 q +north hYm q +northern hZQ p +northward hZ5 h +northwards haa j +nose ha9 R +nostalgia hbO i +nostril hbw u +not hce Q +not an ace ht2 6 +not at all huw B7 +not easily hwr z +not fitting hxe u +not on any account hyM Ba +not quite hzm k +not well h0K BN +not yet h1X b +notary hcu c +notch hdK s +note hd2 Cs +notebook hgi Be +notepad hiA d +noteworthy hid 1 +nothing hjS x +notice hk1 D5 +noticeboard hkD y +notification hou BV +notify hqD BJ +notion hrM BF +notwithstanding hsR Bl +nougat h1y Y +nought h2K g +noun h2q v +nourish h3Z Z +nourishment h3y BQ +novel h5C s +novelist h5u l +novelty h6T 0 +novice h7H j +now h7q BU +now and then h+W Bk +nowadays h8+ j +nowhere h9h 1 +nozzle h/6 V +nu iB0 O +nuance iCC 0 +nuclear iC2 o +nucleus iDe u +nude iEM 1 +nudge iFB z +nudism iF0 3 +nudist iGr m +nudity iHR f +null iHw X +numbed iIH Y +number iIf BO +number of eight iKi p +number of the house iLL z +number sign iL+ o +numberplate iJt 1 +numeral iMm 5 +numismatics iNf 1 +numismatist iOU Bb +nun iPv f +nurse iQO Do +nursery school iT2 z +nut iUp q +nutmeg iVT z +nuts iWG CS +nutty iYY BM +nyctalopia iZk q +nylon iaO l +nymph iaz X +négligé ibK BG +oak idE 8 +oaken ieA r +oakwood icQ 0 +oar ier m +oasis ifR V +oath ifm b +oath of office igB n +oats igo S +obedient ig6 BW +obeisance iiQ 9 +obey ijN Z +obituary ijm l +object ikL BQ +objection ilb BB +objectionable imc BL +obligation inn w +obliging ioX CG +oblique iqd i +oboe iq/ S +obscene irR t +obscure ir+ y +observation isw Bv +observe iuf Cs +obsess ixL l +obsession ixw g +obstacle iyQ Bm +obstetrician iz2 7 +obstinate i0x Bp +obstruct i2a B5 +obstruction i4T BC +obtain i5V 8 +obtainable i6R m +obtuse i63 BJ +obvious i8A BX +obviously i9X / +occasion i+W Bc +occasional i/y 0 +occiput jAm f +occult jBF Y +occupant jBd g +occupation jB9 Bg +occupy jDd BS +occur jEv BP +occur intermittently jHZ BG +occurence jF+ Bb +ocean jIf i +oceanic jJB f +oclock nbN W +octagon jJg e +octagonal jJ+ j +octal jKh g +octopus jLB d +octuple jLe g +oculist jL+ c +odd jMa Bb +ode jN1 O +odor jOD r +odour jOu q +odourous jPY e +of jP2 BH +of a battle jro l +of a beaver jsN l +of a beet jsy h +of a bird jtT n +of a cedar jt6 x +of a deer jur r +of a duck jvW q +of a horse jwA v +of a limetree jwv + +of a maple jxt j +of a pig jyQ r +of a spider jy7 y +of a star jzt 5 +of a tree j0m f +of a wild boar j1F t +of actium jnB j +of all sorts jov 1 +of an alder jpk 3 +of an ass jqb f +of an elephant jq6 u +of course j1y BK +of fantasy j28 / +of good repute j37 x +of high stature j4s 3 +of ill repute j5j u +of lamb j6R a +of long standing j6r BH +of noble birth j7y w +of ones own accord j8i 9 +of paramount importance j9f BD +of sand j+i k +of silk j/G Z +of steal j/f c +of the j/7 f +of the body kAa y +of the coast kBM i +of the hague jnk f +of the same age kBu BB +of the sea kCv o +of the sun kDX c +of the winter kDz 1 +of the year kEo f +of to joD s +of tomorrow kFH 8 +of two colours kGD s +of use kGv p +of water kHY u +of which kIG BJ +of yesterday kJP q +offal jQ9 Z +offence jRW 2 +offend jSM BN +offended jTZ m +offensive jT/ BS +offer jVR Da +offer a toast jYr BA +offer of marriage jZr BA +offer of parriage jar BA +office jbr BZ +officer jdE BG +official jeK BU +official residence jg5 y +official secret jhr 8 +officialdom jfe z +officially jgR o +offset jin e +offset printing jjF o +offspring jjt CL +often jl4 BJ +ogle kJ5 T +oh kKM h +oh dear kK7 g +ohm kKt O +oil kLb Q +oil of olives kMl o +oilcloth kLr 6 +ointment kNN l +okay kNy B6 +old kRM p +old man kSL BB +old materials kTM q +old peoples home kT2 BJ +old wives tale kU/ r +old woman kVq w +older kR1 W +oldfashioned kPs Bg +olive kXu U +oliveoil kWa p +olivetree kXD r +olympic kYC d +omega kYf W +omelette kY1 h +omen kZW z +omicron kaJ c +omit kal Br +omitted kcQ d +omnibus kct v +omnipotence kdc k +omnipotent keA l +omnipresent kel s +omniscient kfR i +on kfz Cz +on account k0A n +on account of k0n B6 +on board k2h g +on end k3B Bq +on every side k4r v +on foot k5a a +on occasion k50 BD +on ones back k63 p +on purpose k7g / +on that account k8f BW +on the analogy of k91 + +on the contrary k+z BQ +on the left lAD f +on the occasion of lAi BJ +on the other hand lBr Bc +on the other side of lDH Bf +on the recommendation of lEm BI +on the right lFu BK +on the whole lG4 BV +on time lIN BO +on top lJb r +on top of lKG g +onager kim b +once kjB 5 +once more kj6 Bk +one kmf BN +one and a half kop s +one another kpV 2 +one at a time kqL BF +one hundred krQ k +one more kr0 3 +one more time ksr t +one only ktY x +one person kuJ o +one thousand kux j +one time kvU u +onerous kns 9 +oneway traffic kle BB +onion kwC a +only kwc Bb +only just kx3 BF +only one ky8 x +onyx kzt T +opal lKm S +open lLt 1 +open ones mouth wide lPD BD +open up lQG w +opening lMi z +openly lNV BR +openness lOm d +openwork lK4 1 +opera lQ2 X +operate lRN Bh +operating system lSu 5 +operation lTn 3 +operative lUe BN +ophtalmologist lVr t +opiate lWY Y +opine lWw 4 +opinion lXo BL +opium lYz W +opossum lZJ p +opponent lZy w +opportune lai BN +opportunist lbv z +opportunity lci Bh +oppose leD BB +opposed to lfE BI +opposite lgM Cr +opposite side li3 x +opposition ljo BX +oppress lk/ 8 +oppressed ll7 c +oppressing lmX i +oppression lm5 w +oppressive lnp BH +opprobrious low x +optician lph f +optimism lqA h +optimist lqh e +optimistic lq/ m +option lrl r +or lsQ M +or not mUZ w +or not mVJ x +orache lsc V +oracle lsx Y +oral ltJ h +oral sex luJ u +orally ltq f +orange lwu r +orange hawkweed lxZ 5 +orange juice lyS u +orangecoloured lu3 y +orangetree lvp BF +orator lzA c +oratorical lzc z +oratory l0P w +orbit l0/ d +orchard l1c o +orchestra l2E y +orchestrate l22 n +orchestration l3d t +orchid l4K c +ordain l4m o +ordeal l5O e +order l67 FS +order a fresh supply of mAN BA +orderform l5s BP +ordinarily mBN BM +ordinary mCZ d +ore mC2 R +organ mEB k +organ grinder mHZ v +organgrinder mDH 6 +organic mEl p +organism mFO j +organization mFx u +organize mGf 6 +orgiastic mII n +orgy mIv 8 +orient mJr q +origin mKV w +original mLF 9 +originally mMC + +originate mNA Bm +ornament mOm Co +ornamental mRO m +orphan mR0 z +orthodox mSn v +orthography mTW BD +oscillate mV6 3 +oscillation mWx z +oscillator mXk k +oscilloscope mYI p +osmium mYx X +ostrich mZI o +other mZw W +other peoples mc9 q +other than mdn BV +otherwise maG CM +otherwise called mcS r +ouch me8 Q +ought to mfM 7 +ounce mgH R +our mgY V +ours mgt f +out mhM Ba +out of mxV t +out of courtesy myC 3 +out of date my5 BT +out of order m0M y +out of politeness m0+ 4 +out of the way m12 m +outer mim BH +outer cover mjt n +outer space mkU v +outfit mlD Bo +outing mmr 3 +outlet moQ c +outletpipe mni u +outline mos Bl +output mqR p +outrage mq6 c +outset mrW W +outside mrs CD +outside of mtv e +outskirts muN BW +outstanding mvj 0 +outward mwX + +oval m2c f +ovation m27 c +oven m3X n +over m3+ c +over m4a Bf +over there nTZ 4 +overall m55 Bk +overalls m7d 7 +overbearingness m8Y Bo +overcoat m+A l +overdrive m+l 5 +overdue m/e u +overestimate nAM 6 +overflow nBG f +overflowing nBl x +overground nCW BB +overgrow nDX f +overgrown nD2 g +overhaul nEW BL +overhead nFh Bb +overjoyed nG8 m +overlay nHi 6 +overlook nIc BF +overpower nJh CG +override nLn 3 +overshoe nMe h +overtake nM/ B1 +overthrow nO0 CK +overtone nQ+ o +overture nRm 5 +overwhelm nSf 6 +ow nUR i +owe nUz + +owing to nVx B2 +owl nXn O +own nX1 5 +owner nYu Y +ownership nZG v +ox nZ1 N +oxygen naC e +oyster nag Z +ozone na5 U +pace nbj Bj +pacifier ndG n +pacifism ndt h +pacifist neO g +pack neu BP +package nf9 BC +packages ng/ l +packet nhk W +pad nh6 x +paddle nir Bf +paddyfield nkK q +padre nk0 t +paean nlh h +pagan nmC v +paganism nmx i +page nnT 0 +paid up nou f +paidup noH n +pail npN S +pain npf c +pain in the throat nwT t +painful np7 4 +pains of childbirth nqz 4 +paint nrr BU +paint over nvt m +paintbrush ns/ o +painter ntn BG +painting nut BA +pair nxA BO +pair of pincers nyO 6 +pal nzI r +palace nzz X +pale n0K 1 +palet n0/ U +palisade n1T / +palladium n2S i +palm n20 x +palm off on n4L x +palmtree n3l m +pamper n48 BA +pamphlet n58 B3 +pancake n7z d +pancreas n8Q t +pane n89 s +panel n9p BH +panic n+w W +pansy n/G m +pant for breath oFs z +pantechnicon n/s 6 +pantechnicon van oAm BC +panther oBo l +panties oCN d +pantomime oCq BH +pantry oDx BP +pants oFA s +paparazzo oGf j +papaw oHC a +papaya oHc Z +paper oJs BS +paperback oK+ BI +paperbacked oMG k +paperbasket oH1 BK +paperer oMq e +paperhanger oI/ t +paprika oNI c +parachute oNk 8 +parade oOg CU +paradise oQ0 f +paradox oRT e +paradoxical oRx o +paraffin oTR f +paraffinoil oSZ 4 +paragraph oTw BO +parallel oU+ q +paralyse oVo r +paralysed oWT g +paralysis oWz k +parameter oXX j +paranoia oX6 h +paranoid oYb i +parantheses oY9 n +parapet oZk 2 +parasite oaa + +parcel obY s +parcel out odr s +parcels delivery ocE 3 +parcels service oc7 w +parchment oeX k +pardon oe7 Bp +parent ogk a +parental og+ e +parenthesis ohc BV +parents ojt o +parentsinlaw oix 8 +parings okV p +parish ok+ v +parishioner olt n +parity omU c +park omw 2 +parking bay onm 5 +parking garage oof BY +parking place op3 9 +parliament oq0 + +parliamentary ory u +parlour osg g +parochial otA 1 +parrot ot1 p +parry oue Y +parsley ou2 g +part ovW Bn +parterre ow9 g +partial oxd BF +partially oyi q +participate ozM BK +participle o0W i +particle o04 BG +particular o1+ BT +particularly o3R BQ +partition o4h 6 +partly o5b k +partner o5/ v +partridge o6u g +parturition o7O 0 +party o8C Bl +party concerned o9n 2 +party interested o+d 2 +party members o/T 0 +pasqueflower pAH x +pass pA4 Et +pass away pNq Br +pass by pPV Co +pass on pR9 q +pass the night pSn q +pass through pTR BD +passable pFl y +passage pGX BV +passageway pHs 1 +passenger pIh v +passenger train pJQ BD +passerby pKT r +passion pK+ Bd +passionate pMb p +passport pNE m +past pUU BK +paste pVe z +paste up pXA f +pastel pWR v +pastor pXf B0 +pat pZT S +patch pZl CI +patch up pbt BM +pate pc5 f +patent pdY g +paternal aunt pd4 9 +paternal grandfather pe1 BE +paternal grandmother pf5 BC +paternal grandparents pg7 BO +paternal uncle piJ 6 +path pjD m +patience pjp q +patient pkT 9 +patriarch plQ u +patriot pl+ p +patriotic pmn q +patriotism pnR 7 +patrol poM 4 +patrolman ppE BB +patron pqF CN +patronage psS / +pattern ptR BL +pauper puc BJ +pause pvl q +pave pwP g +pavement pwv BS +pavilion pyB BL +paving pzM c +paw pzo i +pawn p0K BS +pawning p1c d +pawnshop p15 w +pay p2p BC +pay attention p57 BW +pay attention to p7R CN +pay homage to p9e z +pay off p+R y +pay on account p/D q +pay out p/t BE +pay the expenses of qAx z +pay tribute qBk t +paying off p3r 1 +payment p4g Bb +pea qCR Q +peace qCh U +peaceful qC1 6 +peaceloving qDv 0 +peach qEj X +peacock qE6 Y +peak qFS 2 +peal qGI BB +peanut qHJ r +pear qIb S +pearl qIt V +peartree qH0 n +peasant qJC s +peat qJu S +peculiar qKA BS +pedagogue qLS u +pedal qMA 3 +peddle qM3 6 +pedestal qNx y +pedestal cupboard qOj v +pedestrian qPS k +pedicab qP2 d +pedigree qQT + +peel qRR BJ +peel off qSa x +peep qTL Bk +peep at qUv c +peep upon qVL BF +peer qWQ m +peevish qW2 Bl +peg qYb c +pelican qY3 c +pelvis qZT i +pen qaZ z +penal law qbM 9 +pencil qcJ X +pencil sharpener qcg BB +pending qdh c +penetrate qd9 9 +penicillin qe6 j +peninsula qfd m +penis qgD W +penitent qgZ s +penknife qZ1 k +penny qhF e +pension qhj c +pensioner qh/ 4 +pensive qi3 c +penthouse qjT BK +penury qkd v +people qlM 5 +pepper qmF k +pepsin qmp Y +per qnB CO +perceive qpP CQ +percent qrf q +percentage qsJ x +percentage sign qs6 9 +perchance qt3 BN +peregrine qvE g +peregrine falcon qvk x +peremptoriness qwV w +peremptory qxF 8 +perfect qyB Bp +perfection qzq / +perform q0p E5 +perfume q5i 5 +perhaps q6b BH +peril q7i t +perimeter q8P BD +period q9S BD +periodical q+V BM +periosteum q/h o +peripheral rAJ t +periphery rA2 BX +periscope rCN g +perish rCt BQ +perishable rD9 k +periwinkle rEh z +permanent rFU Bd +permission rGx Ba +permit rIL Bi +perpendicular rJt 0 +perpetuation rKh t +perplexed rLO CA +perplexity rNO BP +persecute rOd 7 +persecution rPY 9 +perseverance rQV Bg +persevere rR1 CA +persist rT1 Cw +persistent rWl k +person rXJ BM +personal rYV u +personal number raU 0 +personality rZD BR +personnel rbI i +persons rbq s +perspective rcW BS +perspiration rdo n +persuade reP BF +persuasion rfU j +perturb rf3 o +perverse rgf o +pessimism rhH BF +pessimist riM s +pessimistic ri4 BX +pestilence rkP d +pestle rks l +pet rlR 4 +petal rmJ i +peter rmr t +petition rnY BK +petitioner roi k +petrified rpG g +petrol rpm Z +petrol can rqn l +petrol engine rrM s +petrol gauge rr4 q +petrol tank rsi o +petroleum rp/ o +petticoat rtK e +pettifogger rto z +pettiness rub z +pettish rvO y +petty rwA l +petulant rwl Bo +pfennig ryN Y +phallus ryl Z +phantom ry+ BF +pharmaceutical r0D BE +pharmaceutical chemist r1H BG +pharmacist r2N v +pharmacy r28 2 +phase r3y s +pheasant r4e Y +pheasants eye r42 9 +phenomenal r5z v +phenomenon r6i BH +phi r7p O +philately r73 6 +philosopher r8x u +philosophic r9f m +philosophize r+F o +philosophy r+t y +phiz r/f s +phlebitis sAL n +phlegm sAy h +phobia sBT p +phoenix sB8 a +phone booth sD3 l +phonetic sCW 3 +phonetics sDN q +phonograph sEc m +phosphorescent sFC t +phosphorus sFv g +photograph sGP BR +photographer sHg o +photography sII o +photomontage sIw r +photoprint sJb j +physical sJ+ BN +physician sLL 4 +physicist sMD s +physics sMv l +physiognomy sNU r +physiotherapy sN/ x +pi sOw N +pianist sO9 b +piano sPY f +pianoforte sP3 z +pick sRY CT +pick out sVq BP +pick up sW5 Dy +pickaxe sQq u +pickle sTr B/ +picnic sar q +picometre sbV i +picture sc2 CM +picture postcard sf5 BJ +picture writing shC 8 +pictureframe sb3 / +picturesque sfC 3 +pie sh+ e +piece sic Bf +piece of furniture sj7 w +piece of mail skr k +piece of music slP 0 +pier smD BF +pig snI o +pig snw 1 +pigeon sol BG +pigeonhole spr 2 +piglet sqh U +pigmy sq1 e +pike srT S +pile srl By +pile ointment stX s +pile powder suD p +pile up sus Bl +pilfer swR BF +pilgrim sxW r +pilgrimage syB BB +pill szC P +pillar szR 6 +pillow s0L o +pilot s0z Bc +pimple s2P f +pin s2u j +pincers s3R BO +pinch s4f B2 +pine s7N l +pineapple s7y c +pinetree s6V 4 +pink s8O BC +pinnacle s9Q b +pioneer s9r Be +pious s/J 3 +pip tAA i +pipe tAi BD +pirate tCc u +piratechief tBl 3 +pistol tDK X +piston tDh Y +pit tEh BG +pitch tFn S +pitch black tGX i +pitcher tF5 e +pitiable tG5 BB +pitiful tH6 5 +pitprops tD5 o +pitwood tIz a +pity tJN B4 +pivot tLF BA +pixel tMF U +pizza tMZ X +placard tMw BP +place tN/ Cw +place of birth tRQ 4 +place of refuge tSI Bk +place of worship tTs BA +place to sit tUs t +placenta tQv h +plague tVZ h +plaice tV6 V +plain tWP BK +plaintain tXZ f +plait tX4 X +plaited mat tYP p +plan tY4 DU +plane tc1 Cf +planet tfU k +planetary tf4 i +planetree tcM p +plank tga k +plant tg+ BF +plantation tiD Ba +planting tjd 0 +plash tkR + +plasma tlP Z +plaster tlo CL +plastic tnz BC +plate to1 BX +plateau tqM u +platform tq6 Dc +platinum tuW e +platitude tu0 BG +platter tv6 j +plausible twd BH +play tyf BN +play a small part t1Z y +play on t2L c +playful tzs 7 +playground t0n y +playoff txk 7 +plaza t2n Y +plea t2/ 9 +plead t38 1 +plead for t4x j +pleasant t5U BM +pleasantly t6g i +pleasantness t7C BI +please t8K B0 +pleased t9+ w +pleasing t+u BX +pleasurable uAF 4 +pleasure uA9 Bh +pledge uCe 4 +plentiful uDW Bl +plenty uE7 o +plenty of uFj t +plot uGQ BY +plough uHo 4 +pluck uIg Ba +plug uJ6 Bg +plug in uLa o +plug up uMC BU +plum uNW T +plume uNp V +plunder uN+ 6 +plunge uO4 Z +pluperfect uPR BI +plus uQZ S +plus sign uQr g +plutonium uRL k +plywood uRv c +pneumonia uSL o +pocket uSz U +podium uTH i +poem uTp m +poet uUP V +poetic uUk o +poetry uVM n +point uVz BR +point of contact uXl 1 +point out uYa Cd +point out to be ua3 3 +pointed uXE h +poise ubu BJ +poison uc3 Bl +poisonous uec BA +poker ufc W +polar ufy X +pole ugJ Bn +police ui2 a +police motorcyclist uj9 BH +policeman ujQ t +policestation uhw BG +policy ulE BM +polish umQ CH +polite uoX Bb +politeness upy BV +politeness particle Bnxs BR +political urH s +political meeting urz u +politician ush v +politics utQ y +polka uuC W +pollinate uuY g +pollination uu4 n +polonium uvf g +polyp uv/ W +polyvinyl chloride uwV BD +pomegranate uxY p +pomp uyB t +pond uyu l +pony uzT e +pooh uzx S +pool u0D Bi +poop u1l n +poopdeck u2M t +poor u25 CJ +poor devil u5C 6 +poor man u58 BL +pop in u+G a +pope u7H R +poplar u7Y j +poppy u77 n +popular u8i z +population u9V x +porch u+g BJ +porcupine u/p m +pork vAP b +pornographic vAq t +pornography vBX BH +porpoise vCe f +port vC9 v +port of call vKr r +port side vLW g +portable vDs q +portal vEW 1 +portent vFL 6 +porter vGF BU +portfolio vHZ l +porthole vH+ y +portico vIw v +portrait vJf BM +pose vL2 Br +poseur vNh l +position vOG u +positive vO0 BB +positively vP1 w +possess vQl 0 +possession vRZ BG +possessions vSf BB +possibility vTg 1 +possible vUV q +possibly vU/ BL +post vXM Cd +post office vh+ l +post up vij e +postage vZp s +postage stamp vaV 5 +postal code vbO i +postal rates vbw m +postcard vcW i +poster vc4 7 +posterity vdz k +postman veX v +postpaid vWK BC +postpone vfG Bc +postulate vgi Bc +pot vjq i +pot marigold vq0 8 +potassium vkM f +potato vkr l +potato blight vlQ v +potato disease vl/ y +potato flour vmx u +potato peeler vnf 2 +potato rot voV r +potpourri vjB p +potpourri vpA g +potter vpg e +pottery vp+ 2 +poultry vrw e +pounce upon vsO BL +pound vtZ / +pound sign vuY k +pound sterling vu8 v +pour vvr Bj +pour in vxO o +pour off vx2 5 +pour out vyv Bc +poverty v0L l +powder v0w BS +powdered milk v2C n +powdery v2p Z +power v3C v +power station v4O p +powerful v3x d +practicable v43 3 +practical v5u BS +practice v7A DY +practice of medicine v+Y 0 +practise v/M 4 +practised wAE p +practitioner wAt q +prairie wBX d +praise wB0 BY +praseodymium wDM p +prawn wD1 m +pray wEb T +prayer wEu X +preach wFF j +preacher wFo x +precede wGZ BO +precious wHn d +precipice wIE BH +precise wJL B2 +precision wLB Bq +precursor wMr BG +predisposition wNx BJ +predominant wO6 y +predominating wPs w +prefabricated house wQc 6 +prefabrication wRW x +preface wSH o +prefer wSv BN +preferably wT8 BK +prefix wVG f +pregnant wVl 8 +prehistory wWh 5 +prejudice wXa Bn +prejudicially wZB t +preliminary wZu BF +prelude waz m +premier wbZ 0 +premium wcN 2 +première wdD l +preparation wdo BY +preparatory wfA BF +prepare wgF Bp +prepay whu a +preposition wiI p +presage wix 6 +prescription wjr k +presence wkP BG +present wm1 Dv +present one wsp 5 +present with wti BT +presentation wqk Bd +presentday wlV Bg +presentiment wsB o +preservation wu1 BQ +preservative wwF v +preserve ww0 CS +preside wzG p +president wzv + +press w0t CT +press firmly w47 p +press hard w5k h +pressing w3A 7 +pressure w37 BA +prestige w6F y +prestigious w63 n +presume w7e Br +presumption w9J CT +pretence w/c Bh +pretend xA9 BX +pretentious xCU x +pretext xDF BH +pretty xEM BF +prevail over xFR BN +prevent xGe Bw +previous xIO BH +previous to xKx BP +previously xJV Bc +prey xMA 2 +price xNi U +priceless xN2 y +pricelist xM2 s +pricey xOo v +prick xPX Ba +pride xQx U +priest xRF BZ +priestly xSe k +primary xTC e +primary school xTg BE +prime minister xVS + +primeval forest xUk u +primitive xWQ f +primrose xWv q +prince xXZ c +princes son xYg BB +princess xX1 r +principal xZh v +principality xaQ 0 +principally xbE BL +principle xcP 4 +print xdH Bi +printed matter xep p +printer xfS o +prior xf6 Bi +prism xhc X +prison xhz q +prisoner xid + +private xjb BS +private talk xkt m +privet xlT Z +privilege xls BA +privileged xms 0 +prize xng c +probable xpk k +probably xqI BH +problem xrP 7 +proboscis xsK 6 +procedure xtE BB +proceed xuF h +proceed with xum Ba +process xwA Bu +procession xxu 7 +proclaim xyp BI +proclamation xzx DK +procrastination x27 9 +procure x34 Bg +produce x5Y B/ +product x7X BC +production x8Z 0 +proenglish xn8 1 +profane x9N BQ +profess x+d v +profession x/M 3 +professional yAD 2 +professor yA5 h +profile yBa BS +profit yCs CB +profound yEt a +profuse yFH BV +progeny yGc j +progerman xox z +prognosis yG/ 8 +programme yH7 z +programme item yIu 0 +progress yJi CP +prohibit yLx f +project yMQ BS +projectionist yNi 7 +projector yOd 2 +proletarian yPT s +proletariat yP/ s +prologue yQr m +prolonged yRR 5 +promenade ySK BV +promethium yTf m +promise yUF Bg +promote yVl q +promotion yWP v +promotor yW+ g +prompt yXe B3 +prompter yZV g +promulgate yZ1 z +pronounce yao h +pronunciation ybJ s +proof yb1 5 +prop ycu q +propaganda ydY 1 +propeller yeN BK +proper yfX Bu +properly yhF BO +property yiT B/ +prophecy ykS 7 +prophesy ylN BU +prophet ymh 8 +proportion ynd BL +proportional yoo BL +proportional to ypz 8 +proposal yqv v +proprietary law yre 4 +prose ysW U +prosecute ysq B8 +proselyte yum z +prospector yvZ 1 +prospectus ywO l +prosper ywz BP +prosperity yyC BW +prostitute yzY BC +prostitution y0a t +protactinium y1H s +protect y1z BJ +protect from y6z 0 +protection y28 B3 +protectionist y4z BH +protective cover y56 5 +protein y7n l +protest y8M Bv +protocol y97 BG +proton y/B Z +prototype y/a i +protracted y/8 8 +protégé zA4 3 +protégée zBv 7 +proud zCq g +prove zDK BR +proverb zEb BE +provide zFf BN +provided that zGs + +province zHq / +provincial zIp x +provision zJa z +provisional zKN w +provoke zK9 CW +provost zNT BB +prow zOU o +proximity zO8 w +prude zPs o +prudence zQU 7 +prudent zRP e +prudish zRt q +prune zSX X +pruningknife zSu x +psalm zTf V +pseudonym zUv v +pseudoscience zT0 7 +psi zVe O +psyche zVs Y +psychiatric zWE o +psychiatrist zWs n +psychiatry zXT m +psychic zX5 b +psychological zYU u +psychologist zZC 3 +psychology zZ5 0 +pub zat 1 +puberty zbi h +public zcD 7 +public prosecutor ziY BL +public square zjj m +public tender zkJ s +publication zc+ BK +publicity zeI Bh +publicity material zfp BN +publicize zg2 Bi +publish zk1 Bl +publisher zma y +publishing house znM u +pudding zn6 Z +puff zoT W +puff out zop c +puff up zpF k +pug zq6 a +pugdog zpp r +pugnose zqU m +pull zrU BF +pull down zuX x +pull out zvI BO +pull someones leg zwW 4 +pull to pieces zxO / +pull up zyN j +pulley zsZ BJ +pullover sweater zti 1 +pulmonaria zyw o +pulpit zzY + +pulsate z0W l +pulse z07 i +pulverize z1d Bb +puma z24 W +pump z3O p +pumpkin z33 b +pun z4S Y +punch z4q k +punctual z5O BU +punctuate z6i BT +punctuation z71 5 +punish z8u BP +punishment z99 BR +punk z/O v +pupil z/9 z +puppet 0Aw t +purchase 0Bd CD +purchase money 0ER s +purchaser 0Dg x +pure 0E9 j +purgatory 0Fg w +purge 0GQ BC +purify 0HS b +puritan 0Ht v +purity 0Ic Bh +purl 0J9 BE +purple 0LB n +purpose 0Lo 5 +purse 0Mh u +purser 0NP e +pursue 0Nt BD +pursue ones point 0Ow BH +pursuit 0P3 x +pus 0Qo W +push 0Rq Bf +push along 0TJ i +push down 0Tr e +push off 0UJ c +push on 0Ul f +pushbutton 0Q+ s +pussy 0VE a +put 0Wh BO +put a cross against 0cr z +put away 0Z+ Ct +put down 0de BG +put forward 0ek BO +put in 0fy BB +put in finished form 0gz BM +put in order 0h/ Bc +put off 0jb B7 +put on 0lW CC +put on airs 0oB 6 +put on makeup 0o7 7 +put on stamps 0p2 p +put on the map 0qf t +put on the stage 0rM Bk +put on weight 0sw 2 +put onto 0nY p +put out 0tm BJ +put out of countenance 0uv Bi +put out to tender 0wR x +put right 0xC B3 +put to bed 0z+ 3 +put to shame 001 2 +put to sleep 01r x +put together 0y5 BF +put up 02c BF +put up with 03h Cn +putandtake top 0Ve BD +putrefy 0Xv 7 +puttee 0Yq d +putty 0ZH 3 +puzzle 06I Ch +puzzle over 08p 6 +pyjamas 09j g +pyramid 0+D c +quack 0+f 9 +quadrangle 0/c j +quail 0// W +quaint 1AV 4 +qualification 1BN 7 +qualify 1CI t +quality 1C1 4 +quantity 1Dt 9 +quarrel 1Eq DR +quarter 1H7 s +quarter of an hour 1JQ 0 +quarters 1In p +quash 1KE Bs +quay 1Lw 1 +queen 1Ml t +quell 1NS BF +quest 1OX v +question 1PG CS +question mark 1SQ s +question particle Bny9 BE +questionable 1RY 4 +quibble 1S8 BM +quick 1UI 5 +quickly 1VB BC +quicksand 1WD g +quicksands 1Wj i +quicksilver 1XF r +quiet 1Xw B3 +quietly 1Zn y +quietness 1aZ + +quilt 1bX W +quince 1bt Y +quince jam 1cF m +quinine 1cr a +quinsy 1dF p +quip 1du Bd +quit 1fL BZ +quite 1gk Ct +quiver 1jR Bn +quivering 1k4 8 +quotation 1l0 r +quotation mark 1mf 0 +quotation marks 1nT 3 +quote 1oK + +quotes 1pI h +rabbi 1pp e +rabbit 1qH X +rabble 1qe BQ +rabid 1ru l +rabies 1sT y +raccoon 1tF b +race 1uR Bd +racecourse 1tg x +racing driver 1vu r +racing motorist 1wZ w +racism 1xJ b +rack 1xk i +rack ones brains 1yG BE +racoon 1zK a +radar 1zk j +radiate 10H 3 +radiation 10+ z +radiator 11x g +radical 12R BS +radically 13j t +radio 14Q j +radish 14z Y +radium 15L Z +radius 15k x +radon 16V W +raffle 16r i +raft 17N U +rag 17h y +ragout 18T Z +ragpicker 18s 8 +ragwort 19o d +rail 1+F d +rail in 2Dz c +rail off 2EP e +railing 1+i 0 +railroad 1/W k +railway 1/6 i +railway carriage 2Ac 1 +railway platform 2BR 0 +railway station 2CF BD +railway ticket 2DI r +rain 2Et g +rain cats and dogs 2Hg BF +rainbow 2FN c +raincoat 2Fp g +raindrop 2GJ i +rainy season 2Gr 1 +raise 2Il Di +raise the alarm 2MH BC +rake 2NJ + +ram 2OH P +rambutan 2OW h +rampart 2O3 BC +ranch 2P5 BD +rancher 2Q8 c +rancid 2RY v +rancour 2SH BX +rand 2Te i +random 2UA p +range 2Up x +rank 2Va BZ +ransom 2Wz / +rap 2Xy S +rap at the door 2aC t +rapid 2YE 7 +rapier 2Y/ k +rapsody 2Zj f +rare 2av t +rarefy 2bc r +rasp 2cH W +raspberry 2cd j +rat 2ds U +rate 2eA CG +rate of exchange 2gu BV +ratepayer 2gG o +rather 2iD EO +ratification 2mR BG +ratify 2nX s +rational 2oD p +ratpoison 2dA s +rattle 2os BD +rattle off 2qT g +rattlesnake 2pv k +ravage 2qz p +rave 2rc d +raven 2r5 T +ravine 2sM w +raw 2s8 BZ +ray 2uV 9 +razor 2wC a +razorblade 2vS w +re 2wc x +reach 2zg Bn +reach for 21H k +reach forward 21r w +react 22b Y +reaction 22z e +reactionary 23R p +read 236 / +read again 29E 1 +read out 295 w +read over again 2+p BA +reader 245 Y +readiness 25R 4 +readiness to help 26J BE +reading matter 27N p +ready 272 BO +real 2/p BZ +realist 3BC b +reality 3Bd u +realization 3CL + +realize 3DJ B0 +really 3E9 Bk +realm 3Gh Z +rear 3I/ Bb +rear axle 3LA g +rear platform 3Lg u +rear seat 3MO i +rear wheel 3Mw j +rear window 3NT l +rearexit 3G6 x +rearguard 3Ka m +rearlamp 3Hr p +rearlight 3IU r +reason 3N4 Bc +reason about 3RC o +reason upon 3Rq n +reasonable 3PU Bu +rebate 3SR BZ +rebel 3Tq Bh +rebellion 3VL y +rebellious 3V9 r +rebound 3Wo BK +rebuff 3Xy b +rebuke 3YN CV +recall 3ai CH +recapture 3cp l +recede 3dO BG +receipt 3eU Bq +receive 3f+ B6 +recent 3h4 x +recently 3ip BT +reception room 3lg BN +receptionist 3k1 r +receptionroom 3j8 5 +recess 3mt o +recipe 3nV a +recipient 3nv h +reciprocal 3oQ 6 +reciprocal impedance 3p2 BN +reciprocally 3pK s +reciprocate 3rD + +recite 3sB BB +reciter 3tC r +reckoning 3tt d +reclaim 3uK e +recline 3uo Z +recognizable 3vB p +recognize 3vq Bt +recollect 3xX BY +recollection 3yv Bm +recommend 30V 8 +recommendable 31R BD +recommendation 32U Bs +recommended 34A l +recommended price 34l x +record 36K DH +recordplayer 35W 0 +records 39R c +recover 39t BN +recovery 3+6 BZ +recreate 2xN v +recreation 4AT Bf +recruit 4By Bk +rector 4DW Y +recuperate 4Du 4 +red 4Em q +red admiral 4OW BA +red currant 4PW 4 +red deer 4QO d +red herring 4Qr 6 +red of dawn 4Rl m +redeem 4FQ BF +redeemable 4GV j +redemption 4G4 BY +redemption money 4IQ y +reduce 4JC BK +reduce the price 4LV BN +reduced hours 4KM BJ +redundancy pay 4Mi 3 +redundancy scheme 4NZ 9 +reed 4SL S +reedmace 4Sd e +reef 4S7 X +reek 4TS / +reel off 4UR e +reelect 2x8 p +refashioning 4Uv 7 +referee 4Vq s +reference 4WW s +refine 4XC w +refined 4Xy BY +refinement 4ZK 5 +refinery 4aD l +reflect 4ao Bl +reflect on 4c+ BT +reflection 4cN x +reform 4eR 5 +reformed 4fK e +refractory 4fo m +refresh 4gO BF +refreshing 4hT k +refrigerate 4h3 k +refrigerator 4ib z +refuge 4jO e +refugee 4js v +refusal 4kb p +refuse 4lE CP +refuse dump 4nT o +refute 4n7 p +regain 4ok j +regal 4pH o +regard 4pv DA +regeneration 4sv Bi +regiment 4uR g +region 4ux + +regional 4vv 5 +register 4wo Bj +registered 4yL 5 +registered letter 4zE 5 +registered share 4z9 2 +registrar 40z BI +regret 417 Cc +regrettable 44X 0 +regrettably 45L BK +regular 46V BA +regularly 47V Bw +regulate 49F BD +regulation 4+I 9 +regulations 4/F 1 +rehabilitation 4/6 x +reign 5Ar 1 +reign of terror 5Bg 2 +reindeersledge 5CW 3 +reindeersleigh 5DN 1 +reject 5EC Cs +rejection 5Gu 4 +rejoice 5Hm BN +relate 5Iz Bw +related 5Kj n +related by marriage 5LK 3 +relation 5MB Bv +relations 5Nw 7 +relationship 5Or + +relative 5Pp CB +relatively 5Rq r +relatives 5SV 5 +relax 5TO q +relay racer 5T4 n +relay rider 5Uf n +release 5VG Cz +relevance 5X5 i +reliable 5Yb s +relief 5ZH BN +relieved 5aU r +religion 5a/ y +religious 5bx 4 +relinquish 5cp Bw +relinquishment 5eZ q +relish 5fD Y +rely on 5fb BS +remain 5gt Bq +remain in office 5k5 s +remainder 5iX Bd +remaining 5j0 BF +remark 5ll BY +remarkable 5m9 z +remedy 5nw BS +remember 5pC BX +remind 5qZ c +remiss 5q1 y +remissness 5rn z +remit 5sa BH +remittance 5th o +remittance of money 5uJ 7 +remorse 5vE a +remote 5ve Bq +remote control 5x6 BL +remotely 5xI y +removable 5zF j +removal 5zo g +remove 50I BS +remove the cloth 51a 1 +render 52P p +rendezvous 524 w +renegade 53o e +renegate 54G e +renew 54k m +rennetstomach 55K x +renounce 557 Bm +renown 57h CL +rent 59s h +rent money 5/d p +rent out 6AG c +rental 5+N U +rented house 5+h j +renter 5/E Z +renunciation 6Ai 3 +repair 6BZ Bc +reparation 6C1 r +repay 6Dg BB +repayable 6Eh g +repayment 6FB 2 +repeat 6F3 0 +repeat parrotlike 6IQ + +repeated 6Gr e +repeatedly 6HJ BH +repel 6JO X +repellent 6Jl BO +repelling 6Kz u +repent 6Lh BU +repent of 6Oq 3 +repentance 6M1 BH +repentant 6N8 u +repertoire 6Ph l +repetition 6QG u +replace 6Q0 BC +replacement 6R2 BO +replenishment 6TE 2 +replete 6T6 Bd +reply 6VX Br +reply coupon 6Xl t +replying 6XC j +report 6YS Ef +repose 6cx i +reprehensible 6dT n +represent 6d6 Bt +representation 6fn 8 +representative 6gj Bx +reprint 6iU BM +reproach 6jg DN +reproduce 6mt z +reproduction 6ng 3 +reproof 6oX BM +reprove 6pj CJ +reptile 6rs a +republic 6sG q +republican 6sw 5 +repulse 6tp y +repulsive 6ub v +reputation 6vK 3 +repute 6wB BK +request 6xL Bh +require 6ys CE +requisite 60w 2 +requisites 61m l +requisition 62L 8 +reread 2yl 7 +rescue 63H r +research 63y B9 +resemblance 65v z +resemble 66i v +resent 67R 4 +resentment 68J B0 +reserve 699 Bp +reservoir 6/m 7 +residence 7Ah BX +resign 7B4 Cd +resign oneself 7FW p +resignation 7EV BB +resin 7F/ T +resist 7GS BI +resistance 7Ha BE +resolute 7Ie 2 +resolution 7JU / +resonance 7LG BT +resonancebox 7KT z +resound 7MZ Ba +resources 7Nz 0 +respect 7On CJ +respectability 7Qw y +respectable 7Ri BV +respective 7S3 i +respiration 7TZ o +resplendant 7UB BC +respond 7VD BZ +responsibility 7Wc Bg +responsible 7X8 BQ +rest 7ZM BZ +rest of 7lC h +restaurant 7al 4 +restingplace 7bd BH +restrain 7ck C8 +restrained 7fg g +restraint 7gA y +restraints 7gy u +restrict 7hg BQ +restricted 7iw y +restriction 7ji x +restrictive 7kT v +result 7lj Dc +resume 7o/ BM +resumé 7qL BQ +retailer 7rb m +retain 7sB BJ +retardation 7tK p +retarded 7tz h +retention 7uU BJ +retire 7vd x +retired 7wO 0 +retired person 7xC 7 +retiree 7x9 2 +retirement 7yz j +retouch 7zW s +retreat 70C Cm +retrenchment 72o 3 +retrograde 73f m +return 747 EN +returnticket 74F 2 +reunite 79I h +reveal 79p CO +revenge 7/3 Z +revenue agent 8AQ u +reverie 8A+ x +reverse 8Bv CC +reverse side 8Ep BX +reversed 8Dx 4 +review 8GA Bb +revise 8Hb 6 +revive 8IV t +revolt 8JC BY +revolution 8Ka z +revolutionary 8LN x +revolver 8L+ f +revue 8Md u +reward 8NL Bo +rezignation 8Oz x +rhenium 8Pk b +rhetoric 8P/ v +rheumatism 8Qu l +rhino 8RT k +rhinoceros 8R3 w +rho 8Sn O +rhodium 8S1 a +rhododendron 8TP q +rhombus 8T5 n +rhyme 8Ug BJ +rhythm 8Vp X +rhythmic 8WA c +rib 8Wc b +ribbon 8W3 b +rice 8Yr S +rice plant 8Y9 l +ricefarmer 8XS u +ricefield 8YA r +rich 8Zi r +rich man 8aN l +rickety 8ay BO +rid 8cA x +riddle 8cx e +ride 8dP t +ride a horse 8d8 o +ride over 8ek g +ridicule 8fE CW +ridiculous 8ha BL +riffraff 8il Be +rifle 8kD d +rig 8kg m +right 8oZ DO +right away 8uZ Bd +right now 8v2 BY +rightangle 8lG BJ +righteous 8rn y +righteousness 8sZ BL +righthand 8tk 1 +righthand side 8nT BG +righthander 8mP BE +rigid 8xO z +rigor 8yB Z +rigour 8ya BA +rigourous 8za 5 +rill 80T d +rim 80w z +ring 81j Cg +ring binder 84D k +ring the bell 84n + +rinse 85l q +riot 86P BQ +riotous 87f 4 +rip 88X b +ripe 88y k +ripen 89W k +rise 896 Br +rise to the bait 8/l 2 +rising 9Ab c +risk 9A3 B0 +risky 9Cr 8 +rite 9Dn l +ritual 9EM p +rival 9E1 BY +rivalry 9GN x +river 9G+ e +rivet 9Hc / +rivulet 9Ib l +road 9Jt i +roadmetal 9JA t +roam 9KP Bo +roar 9L3 x +roar with laughter 9Mo BD +roast 9Nr s +roastbeef 9OX f +roasted 9O2 n +rob 9Pd Bu +robber 9RL X +robbery 9Ri e +robe 9SA e +robes of office 9Se 5 +robin 9TX b +robot 9Ty W +robust 9UI BS +rock 9XT BZ +rockbottom prize 9Va / +rockcrystal 9WZ 6 +rocket 9Ys g +rod 9ZM BZ +rodent 9al a +roe 9a/ Y +rogue 9bX B+ +role 9dV Q +roll 9et CO +roll off 9jr e +roll up 9kJ 5 +rollcall 9dl BI +roller 9h5 f +rollerblind 9g7 + +rollerskate 9iY l +rolling in money 9i9 u +romance 9lC b +romantic 9ld h +romanticism 9l+ o +roof 9mm j +room 9nJ BO +rooster 9oX Z +root 9ow 6 +rope 9pq 2 +rosary 9qg 2 +rose 9sB R +rosebush 9rW r +rostrum 9sS c +rot 9su w +rotation 9te BB +rotten 9uf BB +rottenness 9vg f +rouble 9v/ X +rough 9wW C/ +rough draft 9zV o +round 9z9 Bs +round dance 922 u +round off 93k p +roundabout 91p BN +rouse 94N DK +route 97X BA +routine 98X j +row 99i BB +rowan 9+j u +rowan berry 9/R m +rowboat 986 o +rower 9/3 W +rowingboat +AN v +royal +A8 n +royal household +CF u +royalty +Bj i +rub +Cz BF +rubber +D4 Bd +rubber tree +FV m +rubbish +F7 Co +rubbish tip +Ij l +rubble +JI BZ +rubidium +Kh g +ruble +LB W +ruby +LX X +rudder +Lu g +ruddle +MO Z +rude +Mn 6 +rudiments +Nh BB +ruff +Oi V +ruffian +O3 q +ruin +Ph B9 +rule +Re Bz +ruler +TR 8 +rules +UN n +rum +U0 P +ruminant +VD h +rumor +Vk BX +rumour +W7 BY +rump +YT 9 +run +ZQ C+ +run across +eL BD +run after +fO x +run amuck +f/ 5 +run away +g4 5 +run down +hx z +run for election +ik BK +run into +ju w +run over +ke r +run up +lJ i +rung +cO 2 +runner +dE b +running track +df s +rural +lr i +ruse +mN 4 +rush +nF Bs +rush at +ox h +rush towards +pS BA +rust +qS t +rustle +q/ w +rusty +rv Z +ruthenium +sI l +rye +st R +régime +s+ 1 +rôle +tz W +sack +uJ BR +sacrament +va j +sacred +v9 0 +sacred object +wx v +sacrilege +xg n +sad +yH Cm +saddle +0t b +sadism +1I b +sadist +1j Y +sadistic +17 g +sadly +2b w +sadness +3L Bu +safe +45 t +safety +7y r +safety pin +8d q +safetybelt +5m x +safetylamp +6X q +safetypin +7B x +sagacious +9H Be +sage ++l 6 +sail +/f w +sailor /AP BJ +sailplane /BY BC +saint /Ca W +saints day /Cw g +sake /DQ d +saki /Dt g +saké /EN i +salad /Ev g +salamander /FP n +salami /F2 d +salary /GT BU +sale /Hn BF +sales activity /J/ 1 +salesman /Is e +saleswoman /JK 1 +saliva /K0 x +salivate /Ll BF +sallow /Mq 5 +salmon /Nj W +salon /N5 c +salt /OV h +salt meat /Qy v +salt water /Rh p +salted meat /O2 z +saltshaker /Pp x +salty /Qa Y +salutation /SK BJ +salute /TT + +salvage /UR Bk +salvage company /V1 8 +salvage money /Wx q +salvage vessel /Xb y +salvation /YN Ba +salvor /Zn j +samarium /aK g +same /aq p +samovar /bT f +sample /dW BH +sample book /ed m +samplebottle /by 0 +samplecard /cm w +samurai /fD c +sanctify /ff g +sanctifying /f/ q +sanction /gp Bt +sanctuary /iW n +sanctum /i9 f +sand /j+ p +sand /jc i +sandal /kn Y +sandbank /k/ 4 +sandwich /l3 f +sandy /mW y +sap /oH P +sapwood /nI / +sarcasm /oW g +sarcastic /o2 l +sardine /pb d +sarong /p4 Z +satang /qR s +satellite /q9 q +satire /rn 2 +satisfaction /sd s +satisfactory /tJ t +satisfied /t2 BP +satisfy /vF BU +saturate /wZ w +sauce /xJ e +saucepan /xn u +saucer /yV 0 +sauna /zJ h +sausage /zq i +savage /0M e +save /0q CF +saving /2v BF +saw /30 d +saxifrage /4R k +saxophone /41 h +say /5W BE +say goodbye /6a BL +say goodbye to /7l BR +say yes /82 7 +scabbard /9x b +scabious /+M e +scaffold /+q d +scald //H BH +scale BAAO Cd +scale off BADU v +scales BACr p +scamp ones work BAED w +scan BAEz X +scandal BAFK Bm +scandium BAGw f +scanty BAHP 0 +scar BAID j +scarce BAIm y +scarcely BAJY BD +scare BAKb m +scatter BALB Be +scene BAMf w +scenery BANP g +scent BANv o +sceptre BAOX m +schedule BAO9 t +scheme BAPq BI +schism BAQy z +scholar BARl Ba +scholary report BAS/ BB +school BAUA 4 +school desk BAU4 u +school holidays BAVm y +school of motoring BAWY BI +school of music BAXg y +science BAYS a +scientific BAYs q +scientist BAZW v +scissors BAaF Z +scold BAae C6 +scooter BAdY i +score BAd6 c +scorpion BAeW i +scoundrel BAe4 BK +scourge BAgC Y +scout BAga j +scrabble BAg9 d +scrap BAha BF +scrape BAif BI +scrape off BAjn h +scratch BAkI CJ +scratch all over BAmR t +scratch off BAm+ k +scratch over BAni o +scream BAoK BP +screechowl BApZ s +screen BAqF / +screw BArE 5 +screw home BAsp l +screw up BAtO BX +screwdriver BAr9 s +script BAul BH +scrupulous BAvs z +scuffle BAwf Bd +sculptor BAx8 i +sculpture BAye Bh +scythe BAz/ U +sea BA0T h +sea BA4G m +sea pink BBCE h +seaanemone BA00 v +seabottom BA1j p +seadog BA2M BA +seagull BA4s j +seal BA5P BQ +seam BA6f BL +seamless BA7q e +seamonster BA3M 6 +seamstress BA8I h +search BA8p Ba +seasickness BA+D k +seaside BA+n 0 +season BA/b BQ +seat BBAr x +seaweed BBBc o +secede BBCl q +secession BBDP s +seclude BBD7 o +secluded BBEj w +second BBFT q +second cousin BBJg 5 +second hand BBKZ m +secondary BBF9 B1 +secondhand BBHy m +secondhand dealer BBIY BI +secret BBK/ B6 +secret code BBQO x +secretary BBM5 + +secrete BBN3 r +secretion BBOi l +secretly BBPH BH +sect BBQ/ S +section BBRR BR +sector BBSi Y +secure BBS6 CT +security BBVN Bd +security officer BBWq 9 +seduce BBXn y +see BBYZ Cm +see again BBfv 2 +see as BBgl 4 +see on television BBhd 7 +seed BBa/ S +seek BBbR BF +seem BBcW BB +seemingly BBdX BR +seemly BBeo BH +seize BBiY B3 +seized with emotion BBkP B2 +seizure BBmF 5 +seldom BBm+ X +selenium BBnV g +self BB2h h +selfassurance BBn1 Bf +selfassured BBpU / +selfcommand BBqT / +selfconfidence BBrS BK +selfconscious BBsc BQ +selfcontrol BBts BE +selfevident BBuw 2 +selfheal BBvm u +selfinterest BB3C BH +selfinterested BBwU BS +selfish BB4J 4 +selfishness BB5B BE +selfless BB6F BM +selflessly BB7R n +selfpossessed BBxm v +selfpossession BByV BG +selfreliance BBzb BI +selfsufficiency BB0j BI +selftaught man BB1r 2 +sell BB74 BJ +sell doortodoor BB9B BP +semantics BB+Q w +semi BB/A a +semicolon BB/a i +send BB/8 BJ +send about his business BCB3 Bb +send for BCDS 9 +send in morse code BCEP 8 +send off BCFL 0 +sender BCBF y +seniority BCF/ 1 +sensation BCG0 9 +sensational BCHx 3 +sense BCIo B7 +sense of taste BCKj z +sensible BCLW f +sensitive BCL1 BW +sensual BCNL z +sensuous BCN+ 0 +sentence BCOy BZ +sentiment BCQL 3 +sentimental BCRC m +sentry BCRo l +separate BCSN Dt +separately BCV6 BQ +separation BCXK Bl +separatrix BCYv i +sepia BCZR 2 +septic BCaH u +serenade BCa1 d +serene BCbS r +serf BCb9 v +serfdom BCcs x +sergeant BCdd h +serial BCd+ 8 +series BCe6 u +serious BCfo Cl +sermon BCiN u +serpent BCi7 b +serum BCjW p +servant BCj/ BS +serve BClR CL +server BCnc a +service BCn2 BV +service charge BCpL z +servitude BCp+ 2 +sessile oak BCq0 l +session BCrZ z +set BCsM DG +set a trap BCyv g +set a trap for BCzP o +set conditions BCz3 Bd +set in BC1U r +set out BC1/ r +set straight BC2q n +setting BCvS i +settle BCv0 Bj +settlement BCxX BY +seven BC3R T +seventeen BC3k g +seventeenth BC4E m +seventh BC4q Z +seventy BC5D c +several BC5f 4 +several times BC6X BC +severance scheme BC7Z 5 +severe BC8S 6 +severity BC9M 2 +sew BC+C 4 +sew on BDEJ y +sew together BDE7 q +sewer BC+6 m +sewingbasket BC/g z +sewingbox BDAT s +sewingclass BDA/ v +sewingmachine BDBu 3 +sewingthings BDCl y +sewingthread BDDX y +sex BDFl x +sexual BDGW BA +sexual desire BDHW 5 +sexual love BDIP + +sexual pleasure BDJN 8 +shack BDKJ 6 +shackle BDLD 9 +shade BDMA Bl +shadow BDNl BH +shaft BDOs 4 +shaggy BDPk p +shah BDQN T +shake BDQg CO +shake hands BDSu r +shake off BDTZ g +shake ones head BDT5 BF +shall BDU+ b +shallop BDVZ Z +shallow BDVy u +shallowly BDWg v +shaman BDXP v +shame BDX+ 6 +shameful BDY4 h +shameless BDZZ v +shampoo BDaI y +shamrock BDa6 c +shanty BDbW u +shape BDcE 9 +share BDd4 DG +share to bearer BDig 6 +sharecapital BDdB 3 +shareholder BDg+ r +shareholdership BDhp 3 +shark BDja V +sharp BDlL Bg +sharpedged BDjv q +sharpen BDmr BG +sharpness BDnx BX +sharptoned BDkZ y +shatter BDpI BG +shave BDqO m +shave off BDq0 f +shavingbrush BDrT BA +shawl BDsT w +she BDvF i +sheaf BDvn z +shear BDwa p +sheass BDtD e +shebear BDth g +shed BDxD D2 +shed tears BD05 x +sheen BD1q y +sheep BD3H W +sheepdog BD2c r +sheet BD3d 8 +sheet metal BD4Z / +shegoat BDuB i +sheikh BD5Y V +shelf BD5t b +shell BD6I CH +shelling BD8P g +shelter BD8v DT +shepherd BEAC 2 +sheriff BEA4 m +sherry BEBe Y +shewolf BDuj i +shield BEB2 BF +shift BEC7 b +shift on to BEEK 7 +shifting spanner BEDW 0 +shilling BEFF c +shinbone BEFh y +shine BEGT Ba +shine upon BEHt BN +shingle BEI6 d +ship BEJX BJ +shipping share BELT v +shippingdate BEKg z +ships boat BEMh e +shipwreck BEMC f +shirt BEM/ Z +shiver BENY 8 +shock BEPJ Bo +shock absorber BEQx v +shockabsorber BEOU 1 +shoe BEUY i +shoehorn BERg 4 +shoelace BESY BJ +shoelift BETh 3 +shoemaker BEU6 l +shogun BEVf b +shoo BEV6 4 +shoot BEWy Ba +shoot dead BEYM z +shoot off BEY/ h +shop BEZg 8 +shopkeeper BEac z +shopping street BEcH v +shoppingcentre BEbP 4 +shore BEc2 x +short BEfa n +short circuit BEoR t +short pants BEo+ 9 +short story BEp7 o +shortage BEgB BP +shortcoming BEhQ BV +shorten BEil BT +shortening of working hours BEj4 Bl +shorthand BEld + +shortlived BEdn u +shortness of breath BEmb BD +shorts BEne z +shortsighted BEeV BF +should BEqj 1 +shoulder BEsX p +shoulderbelt BErY / +shoulderblade BEtA r +shout BEtr BN +shove BEu4 X +shove on BEwP h +shovel BEvP BA +show BEww Dg +show off BE23 s +show window BE3j BT +showcase BE0Q c +shower BE0s BC +showerhead BE1u h +showerstall BE2P o +shrapnel BE42 t +shrew BE5j BP +shrewd BE6y BY +shrewish BE8K h +shrill BE8r BK +shrimp BE91 Y +shrink BE+N Be +shrivel up BE/r BD +shrub BFAu t +shrubbery BFBb t +shrug BFCI 8 +shuffle BFDE BH +shut BFEL BQ +shut off BFFz Br +shut up BFHe / +shutter BFFb Y +shy BFId BF +shyness BFJi t +shyster BFKP t +sibling BFK8 u +siblings BFLq l +sick BFMP W +sickle BFMl f +side BFQc B2 +side with BFVp o +sideboard BFSS c +sidebranch BFNE q +sideissue BFNu BO +sideissue BFSu i +sideline BFTQ a +sideral BFTq 0 +sideshow BFUe c +sidewalk BFU6 v +sideway BFO8 w +sidewhiskers BFPs w +siege BFWR i +sieve BFWz X +sift BFXK a +sigh BFXk c +sight BFYA CV +sightless person BFaV q +sightseeing trip BFa/ BK +sigma BFcJ V +sign BFdg Cc +sign language BFla s +signal BFf8 Bl +signalman BFhh 0 +signature BFiV 1 +signboard BFce BC +significant BFjK BK +signify BFkU n +signpost BFk7 f +silence BFmG BG +silent BFnM v +silently BFn7 l +silhouette BFog t +silicon BFpN k +silk BFpx Z +silk BFqK k +silken BFqu Y +silkworm BFrG r +sill BFrx b +silliness BFsM s +silt up BFs4 d +silver BFuL n +silver fir BFuy BE +silvercoloured BFtV 2 +similar BFv2 BU +similarity BFxK 0 +similarly BFx+ r +simmer BFyp a +simple BFzD BE +simplify BF0H z +simultaneous BF06 BA +sin BF16 s +since BF2m By +sincere BF4Y BQ +sincerity BF5o BZ +sinew BF7B b +sinful BF7c Y +sing BF70 d +singer BF8R n +singing BF9o o +singingbird BF84 w +single BF+Q Bz +single file BGAD m +single man BGAp m +singly BGBP 5 +sink BGCI CI +sip BGEQ 0 +siphon BGFE p +sir BGFt e +sirocco BGGL b +sister BGH0 d +sisterinlaw BGGm BO +sit BGIR 2 +sit at table BGL2 l +sit down BGMb 8 +sit down to table BGNX BC +site BGJH U +sittingroom BGJb BW +situation BGKx BF +six BGOZ P +sixteen BGOo c +sixteenth BGPE i +sixth BGPm V +sixtieth BGP7 g +sixty BGQb Y +sizable BGQz 1 +size BGRo BE +skate BGSs BK +skeleton BGT2 9 +skeptic BGUz c +skeptical BGVP v +sketch BGV+ BZ +ski BGXX d +skid BGX0 BB +skilful BGY1 m +skilfulness BGZb BW +skill BGax BI +skilled BGb5 j +skillful BGcc BQ +skillfulness BGds BX +skim BGfD Bb +skimpy BGge r +skin BGhJ Bj +skinflint BGis 9 +skinny BGjp q +skip rope BGkT m +skirt BGk5 g +skull BGlZ W +skunk BGlv a +sky BGmJ Z +sky blue BGn6 BH +skyjacker BGmi o +skylight BGnK w +slab BGpB h +slam BGpi a +slam shut BGp8 x +slander BGqt Bb +slanting BGsI m +slap BGsu s +slash BGta l +slat BGt/ R +slate BGuQ h +slaughter BGux s +slaughterhouse BGvd BA +slave BGwd h +slay BGw+ o +sledge BGxm t +sleep BGyT q +sleep heavily BG2u BC +sleep on BG3w f +sleepingaccomodation BGy9 BV +sleepingcar BG0S / +sleepingplace BG1R BB +sleepy BG2S c +sleeve BG4P V +sleigh BG4k r +slender BG5P BU +slice BG6j BV +slighting BG74 BD +slim BG87 w +slink off BG9r g +slip BG+L Bl +slipper BG/w c +slippery BHAM 2 +slippery as an eel BHBC BH +slobber BHCJ b +slogan BHCk BK +sloop BHDu V +slope BHED 2 +slope down BHE5 x +sloping BHFq 3 +slot BHGh 7 +sloth BHHc 3 +slothful BHIT a +slow BHJX h +slow train BHK6 j +slowly BHJ4 BC +slowworm BHIt q +slug BHLd d +sluice BHL6 3 +slum BHMx Z +slumber BHNK v +slut BHN5 t +sly BHOm 7 +sly joke BHQM y +slyness BHPh r +smack BHQ+ W +small BHUO y +small ball BHVf k +small basket BHWD n +small bench BHWq j +small letter BHXN 1 +small plate BHYC i +small room BHYk 9 +small woodpecker BHZh y +smallminded BHRU 9 +smallmindedness BHSR BN +smallpox BHVA f +smalltime BHTe w +smart BHaT / +smash BHbS BC +smear BHcU 0 +smell BHdI Bs +smell at BHe0 p +smell bad BHfd q +smell of BHgH s +smile BHgz o +smith BHhb e +smock BHh5 j +smoke BHic t +smoke screen BHlP w +smoked BHjJ Y +smokers BHjh e +smokestack BHj/ BQ +smooth BHl/ CL +smuggle BHoK b +smuggler BHol w +snack BHpV i +snail BHqj y +snailshell BHp3 s +snake BHrV U +snap at BHsJ d +snappy BHrp g +snare BHsm + +snarl at BHtk g +sneer BHuE 1 +sneeze BHu5 0 +sniff at BHvt p +snifter BHwW l +snob BHw7 S +snobbish BHxN g +snore BHxt y +snorter BHyf n +snot BHzG S +snow BHzY i +snow white BH2X 0 +snowball BHz6 j +snowdrop BH0d i +snowflake BH0/ h +snowman BH1g d +snowy BH19 a +snub BH3L X +snuffle BH3i 0 +so BH4W Bs +so as BJNq s +so long BJOW x +so so BJPH o +so that BJPv Z +so to speak BJQI BM +soak BH6C BS +soap BH7U m +soapwort BH76 e +sob BH8Y T +sober BH8r + +sobriquet BH9p e +soccer BH+H Z +soccer ball BH+g k +social BH/E s +socialism BH/w l +socialist BIAV i +society BIA3 CK +sociologist BIDB q +sociology BIDr 7 +sock BIEm Q +socket BIE2 p +sod BIFf a +sod of grass BIHJ t +sodium BIF5 a +sodomize BIGT 2 +sofa BIH2 o +soft BIIe 7 +soften up BIJZ BJ +softness BIKi z +soil BILV C0 +soiled BIOJ z +solair BIO8 a +solder BIPW 1 +soldier BIQL c +sole BIQn Br +sole agency BIUC t +sole agent BIUv z +sole sale BIVi j +solemn BISS w +solemnity BITC BA +solicitor BIWF r +solid BIWw BJ +solidarity BIX5 BM +solidus BIZF b +soliloquy BIZg x +solitary BIaR BT +solo BIbk T +soloist BIb3 b +solution BIcS x +solve BIdD l +somber BIdo 9 +some BIel C3 +some kind of BItB BD +some one BIuE + +somebody BIhc BB +somehow BIid BF +someone BIji 9 +someone of the upperclass BIk9 BR +someones BIkf e +something BImO e +something new BIms BA +sometime BIns z +sometimes BIof BU +sometimes sometimes BIpz BE +somewhat BIq3 BM +somewhere BIsD + +son BIw8 Q +sonata BIxM a +song BIyP g +songbird BIxm p +songbook BIyv 3 +soninlaw BIvC v +sonofabitch BIvx BL +sonorous BIzm v +sonourous BI0V u +sons and daughters BI1D 6 +soon BI19 CC +soot BI3/ Q +soothe BI4P x +sophisticated BI5A s +soprano BI5s f +sorcerer BI6L 0 +sorcery BI6/ f +sore BI7e U +sore throat BI7y k +sorrel BI8W o +sorrow BI8+ Be +sorrowful BI+c i +sort BI++ g +soul BI/e g +sound BI/+ BT +sound the alarm BJBR BD +soup BJCU S +sour BJCm CU +source BJE6 o +source of infection BJFi BK +south BJGs BE +south wind BJJu j +southern BJHw e +southward BJIO v +southwards BJI9 x +souvenir BJKR Ba +sovereign BJLr BH +sow BJMy 4 +spa BJRU V +space BJRp / +spaceman BJSo u +spacious BJTW BV +spade BJUr BV +spades BJWA y +spanish mayor BnoU BE +spanner BJWy v +spare BJXh CL +spare time BJZs i +spare tyre BJaO n +spark BJa1 0 +spark plug BJcv y +sparkling BJbp BG +sparrow BJdh W +spasm BJd3 X +spatter BJeO r +spawn BJe5 q +speak BJfj f +speakers platform BJgC y +spear BJg0 w +special BJhk BK +specialist BJiu BN +speciality BJj7 4 +specially BJkz BN +species BJmA c +specific BJmc u +specifications BJnK q +specify BJn0 g +specimen BJoU BJ +spectacle BJpd BB +spectacles BJqe d +spectator BJq7 7 +speculate BJr2 t +speculation BJsj 2 +speculative BJtZ 1 +speech BJuO Br +speechless BJv5 r +speed BJxW BO +speed limit BJ0H p +speed up BJ0w BG +speedlimit BJwk y +speedwell BJyk f +speedy BJzD BE +spell BJ12 7 +spelt BJ2x U +spend BJ3F B0 +spend the night BJ45 q +sperm BJ5j g +sperm whale BJ6D t +sphere BJ6w 0 +spice BJ7k 3 +spider BJ8b i +spiderweb BJ89 2 +spiked BJ9z e +spill BJ+R U +spin BJ+l U +spinach BJ+5 c +spindle BJ/V p +spine BJ/+ v +spinningwheel BKAt y +spinster BKBf e +spiral BKB9 c +spirit BKCZ Bc +spirits BKD1 BD +spiritual BKE4 k +spiritualism BKFc s +spit BKGI o +spite BKGw v +spittoon BKHf y +splash BKIR Be +splender BKJv 1 +splinter BKKk BI +split BKLs Bq +split hairs BKNW 1 +split off BKOL g +spoil BKOr Ck +spoiled BKRP 1 +sponge BKSE o +sponge down BKTM l +sponge over BKTx n +sponger BKSs g +spongy BKUY 1 +spontaneous BKVN 1 +spool BKWC j +spoon BKWl u +sporadic BKXT t +sport BKYA W +sports ground BKZR BL +sports shirt BKac p +sportsman BKYW 7 +spot BKbF C/ +spouse BKeE y +spray BKe2 k +spray gun BKfa e +spread BKf4 B/ +spread out BKjG BR +spreadsheet programme BKh3 BP +spring BKkX Ci +spring crocus BKnh 3 +spring snowflake BKoY v +spring upon BKpH BM +springtime BKm5 o +sprinkle BKqT Bh +sprout BKr0 x +spruce BKti s +sprucefir BKsl 9 +spur BKuO 2 +spur on BKxi BJ +spurge BKvE d +spurrey BKvh c +spurt BKv9 v +spurt out BKws 2 +spy BKyr Ck +spy on BK1P z +spy upon BK2C BG +squabble BK3I e +squad BK3m BF +square BK4r BP +square decametre BK56 BA +square metre BK66 w +square mile BK7q q +square up BK8U i +squash BK82 m +squat BK9c o +squatter BK+E c +squeeze BK+g 9 +squeeze out BK/d BD +squint BLAg y +squire BLBS g +squirrel BLBy d +squirt BLCP 8 +stab BLDL u +stable BLD5 BT +stableman BLFM BQ +stack BLGc Cj +stadium BLI/ Ba +staff BLKZ V +stag BLKu b +stage BLML DU +stagemanager BLLJ BC +stagnate BLPf q +staid BLQJ Bb +stain BLRk BN +stair BLSx 5 +staircase BLTq k +stairs BLUO e +stake BLUs / +stale BLVr Bx +stalk BLXc Bv +stalk of cabbage BLZL x +stall BLZ8 f +stallion BLab c +stammer BLa3 x +stamp BLcz B9 +stampcollecting BLbo BL +stamped BLew 0 +stanchion BLfk 0 +stand BLgY B3 +stand guard BLl+ x +stand in the way of BLmv Bc +stand up BLoL 9 +stand up to BLpI Bo +standard BLiP Bs +standing on end BLj7 BS +standing together BLlN x +staple BLqw t +star BLrd z +star BLs8 BG +starboard BLuC i +starch BLuk n +stare BLvL p +stare at BLv0 d +starfruit BLsQ s +starling BLwR e +start BLyh E0 +start off BL7D r +start out BL7u / +start running BL8t 3 +starter BL3V BA +starting point BL4V BQ +startled BL5l p +startling BL6O 1 +startup cost BLwv 4 +startup costs BLxn 6 +starvation wage BL9k y +state BL+W CZ +statement BMAv BO +statement of policy BMB9 BM +station BMD/ BA +stationers shop BME/ 3 +stationmaster BMDJ 2 +statistics BMF2 k +statue BMGa m +stature BMHA BG +statute BMIG e +stay BMJo Bs +stay for BMLU e +stay on BMLy BO +stay over BMNA BS +stay with BMOS r +stayathome BMIk BE +steady BMO9 l +steak BMPi d +steal BMP/ BD +stealthily BMRC 5 +steam BMS1 i +steamboiler BMR7 6 +steamship BMTX h +steel BMT4 i +steep BMUa V +steepness BMUv q +steer BMVZ 3 +steering BMXn r +steeringwheel BMWQ BX +stem BMYS / +stench BMZR X +stencil BMZo u +stenography BMaW 3 +stenotypist BMbN o +step BMgb CD +step on BMiz / +stepbrother BMb1 w +stepchild BMcl v +stepdaughter BMdU y +stepfather BMeG v +stepmother BMe1 u +steppe BMie V +stepsister BMfj 4 +stereotype BMjy BZ +sterile BMlL 3 +sterilize BMmC k +stern BMmm p +sterness BMnP 0 +stevedore BMoD u +stew BMox k +steward BMpV BT +stewardess BMqo w +stewardship BMrY t +stichwort BMsF j +stick BMso Br +stick to the pan BMvh s +sticker BMuT m +sticky BMu5 o +stiff BMwN Bl +stiffling BMxy BG +stiletto heel BMy4 n +still BMzf BC +stimulate BM0h D+ +stimulus BM4f BH +sting BM5m B2 +stinginess BM7c BQ +stingray BM8s Y +stingy BM9E BN +stink BM+R k +stinking BM+1 e +stipulate BM/T BT +stipulation BNAm 8 +stir BNBi CU +stir up BND2 EX +stitch BNIN k +stock BNIx s +stock exchange BNKq BE +stocked BNJd 0 +stocking BNKR Z +stodgy BNLu Bg +stoic BNNO 0 +stoical BNOC n +stoke BNOp g +stomach BNPJ Y +stomach ache BNPh z +stone BNUG Ba +stone jar BNWE 4 +stonechippings BNQU 2 +stonecutter BNRK x +stonedead BNR7 p +stonefalcon BNSk v +stonemarten BNTT z +stonemason BNVg k +stool BNW8 r +stoop BNXn BN +stop BNY0 F6 +stop up BNfs BU +stopper BNeu + +storage battery BNhA 9 +storage room BNh9 Bb +store BNkL Bx +storecupboard BNjY z +storey BNl8 m +stork BNmi Z +storksbill BNm7 u +storm BNnp x +storming BNoa 0 +story BNpO Bs +stout BNq6 s +stove BNrm b +stow BNsB B+ +straggler BNt/ l +straigh ahead BNz3 2 +straight BNuk 0 +straight ahead BNxR 8 +straight along BNyN 9 +straight past BNzK t +straightedge BNvY i +straightforward BNv6 BX +strain off BN16 g +strained BN0t w +strainer BN1d d +strait BN2a 7 +straitened BN3V BV +strand BN4q 5 +strange BN5j Bq +stranger BN7N BL +strangle BN8Y s +strangulated hernia BN9E / +strap BN+D U +stratagem BN+X BC +strategic BN/Z 1 +strategy BOAO x +straw BOA/ V +strawberry BOBU i +strawberry tree BOB2 w +stray from ones subject BODE + +stray off BOEC BW +straying BOCm e +streak BOFY 3 +stream BOGP BL +stream towards BOID BE +streamlined BOHa p +strech BOJH BH +strech out BOKy BV +strecher BOKO k +street BOM6 Y +streetcar BONS f +streetcorner BOMH z +streetlamp BONx p +streetlight BOOa p +strength BOPD w +stress BOQk Be +stressmark BOPz x +stretcher BOSC 4 +strew BOS6 a +strict BOTU 5 +stride BOUN Bd +strike BOVq ER +strike a balance BOZ7 BE +strike off BOa/ 6 +strike out BOb5 BQ +strike up an acquaintance BOdJ BP +striking BOeY Bw +string BOgI B7 +stringent BOiD 8 +strip BOi/ CA +stripe BOk/ BD +striped snakehead fish BOmC BY +strive for BOna g +stroke BOn6 Ce +stroke of apoplexy BOqY 1 +stroll BOrN BW +stroller BOsj f +strong BOt0 BT +strong drink BOvt BO +strongbox BOtC y +strongly BOvH m +strontium BOw7 i +strop BOxd a +structure BOx3 BF +struggle BOy8 CY +strut around BO1U m +stub BO16 a +stubborn BO2U Bm +student BO36 r +studfarm BO4l BE +studio BO5p r +studs BO6U W +study BO6q Bg +stuff BO8K B0 +stuffing BO9+ u +stuffy BO+s Z +stumble BO/F u +stun BO/z X +stupefy BPAK g +stupid BPAq Bz +stupidly BPCd d +sturdy BPC6 BU +sturgeon BPEO e +stutter BPEs x +sty BPFd BF +style BPGi b +stylus BPG9 y +subdue BPHv s +subject BPIb BD +subject to BPKD BD +subject to tax BPLG v +subjective BPJe l +subjugate BPL1 y +subjunction BPMn 5 +subjunctive BPNg BN +subjunctive mood BPOt BX +submission BPQE j +submit BPQn + +subordinate BPRl BJ +subscribe BPSu By +subscribe to BPVD BT +subscriber BPUg j +subscription BPWW BF +subsequent BPXb k +subsequently BPX/ B1 +subside BPZ0 BR +subsidiary subject BPbF 1 +subsidize BPb6 k +subsidy BPce BD +subsistence minimum BPdh 7 +substance BPec BH +substantive BPfj + +substitute BPgh Bc +substitution BPh9 3 +subterfuge BPi0 BR +subtle BPkF q +subtlety BPkv t +subtract BPlc g +subtraction BPl8 o +subtraction sum BPmk x +subtrahend BPnV h +suburb BPn2 c +subversion BPoS p +subversive BPo7 z +subvert BPpu u +subway BPqc X +succeed BPqz BA +success BPrz Bl +successfully BPtY + +successor BPuW 4 +succumb BPvO c +such BPvq Ba +such a BPxE CV +such as BPzZ BC +suck BP0b l +suckle BP1A o +suction apparatus BP1o 0 +sudden BP2c a +suddenly BP22 w +sue BP3m B4 +suet BP5e y +suffer BP6Q B3 +suffer damage BP8H / +suffer from BP9G m +suffice BP9s BR +sufficient BP+9 r +sufficiently BP/o BE +suffix BQAs n +suffocate BQBT BP +sugar BQEq X +sugarbowl BQCi t +sugarbowl BQFB j +sugarcane BQDP t +sugarpalm BQD8 u +sugary BQFk d +suggest BQGB B2 +suggestion BQH3 x +suit BQIo C8 +suitability BQLk r +suitable BQMP CT +suitcase BQOi w +suite BQPS W +sulfuric BQPo s +sullen BQQU 8 +sulphur BQRQ a +sulphureous BQRq v +sulphuric BQSZ r +sultan BQTE b +sultry BQTf BE +sum BQUj BL +sum up BQcW c +summarize BQVu u +summer BQXV i +summerhouse BQWc 5 +summery BQX3 BL +summit BQZC Bm +summon BQao r +summons BQbT BD +sun BQcy X +sun BQd2 BL +sun oneself BQi0 s +sunbeam BQfB g +sundew BQfh d +sunflower BQf+ j +sunglasses BQdJ t +sunlight BQgh p +sunrise BQhK e +sunset BQho f +sunshine BQiH t +super highway BQvh 1 +superb BQjg / +superficial BQkf BG +superfluous BQll 0 +superimpose BQmZ u +superintendant BQnH BX +superior BQoe Bf +superman BQp9 l +supermarket BQqi p +superscription BQrL 0 +supersign BQr/ z +superstition BQsy r +superstitious BQtd t +supervise BQuK BX +supper BQwW m +supplement BQw8 w +supplementary BQxs q +supplication BQyW u +supply BQzE DQ +support BQ2U DL +supporter BQ5f BP +supporters BQ6u 2 +supporting actor BQ7k w +supporting base BQ8U 0 +supporting wall BQ9I v +suppose BQ93 BH +supposing that BQ++ BQ +supposition BRAO Ba +suppress BRBo Bz +sure BRDb BG +surely BREh q +surface BRFL BT +surgeon BRGe r +surgery BRHJ BA +surgical BRIJ j +surgical operation BRIs BL +surly BRJ3 BS +surmise BRLJ BW +surname BRMf x +surpass BRNQ Bo +surplus BRO4 y +surprise BRPq B4 +surprising BRRi k +surrender BRSG BB +surround BRTH x +surroundings BRT4 7 +survive BRUz w +susceptibility BRVj 0 +suspect BRWX u +suspenders BRXF h +suspicion BRXm BK +suspicious BRYw BN +sustain BRZ9 Bg +sustained BRbd CG +suture BRdj + +swaddling cloth BReh u +swallow BRfP BI +swallow up BRgX 6 +swamp BRhR t +swan BRh+ T +swap BRiR BM +swarm BRjd BT +swarm of bees BRkw r +swathe BRlb Z +sway BRl0 x +swear BRml Ba +swear off BRn/ g +sweat BRof u +sweep BRpN Bj +sweeper BRqw e +sweet BRs2 Bj +sweet potato BRyY 3 +sweet smell BRzP o +sweet william BRz3 s +sweetandsour BRrO 0 +sweetheart BRuZ B9 +sweetness BRwW CC +sweetsmelling BRsC 0 +swell BR0j BN +swell into a roar BR2N x +swelling BR1w d +swift BR2+ BY +swiftly BR4W BD +swim BR5Z d +swim away BR8i j +swimming trunks BR72 s +swimmingbath BR52 9 +swimmingpool BR6z BD +swindle BR9F CH +swindler BR/M e +swindling BR/q h +swine BSAL c +swing BSAn CS +swing open BSC5 l +swirl BSDe 5 +swish BSEX v +switch BSFG B7 +switch off BSHB Bu +switch on BSIv BI +swoon BSJ3 B1 +sword BSLs i +swotch on BSMO 8 +sycamore BSNK BL +sycamore fig BSOV m +syllable BSO7 o +symbol BSPj j +symbolize BSQG k +symmetric BSQq t +symmetry BSRX f +sympathetic BSR2 + +sympathize BSS0 j +sympathy BSTX + +symphonic BSUV h +symphony BSU2 f +symptom BSVV x +synagogue BSWG q +synchronous BSWw o +syndicate BSXY 4 +syntax BSYQ v +syphilis BSY/ b +syringe BSZa t +syrup BSaH e +system BSal q +system of taxation BScU 9 +systematic BSbP BF +tabernacle BSdR k +table BSev l +tablecloth BSd1 6 +tablet BSfU l +tabulation BSf5 w +tackle BSgp BD +taco BShs T +tact BSh/ n +tactic BSim Z +tactics BSi/ b +tadpole BSja f +tail BSkk j +taillight BSj5 r +tailor BSlH BT +tails BSma S +taint BSms V +take BSnB Cw +take a break BS1g k +take a rest BS2E i +take a shower BS2m m +take a sunbath BS3M 0 +take a switch to BS4A 2 +take account of BSsU BM +take action BStg BW +take after BSu2 n +take aim BSvd d +take along BSv6 BX +take an interest BSxR 1 +take an interest in BSyG 6 +take apart BSzA BE +take away BS0E Bc +take care BS42 BZ +take care of BS6P BC +take down BS7R x +take exception BS8C v +take exception to BS8x 5 +take fire BS9q BD +take in BS+t CC +take into account BTAv B0 +take leave BTCj BK +take leave from BTDt BU +take note BTFB 4 +take off BTF5 BP +take off makeup BTK1 9 +take offence BTHI BT +take offence at BTIb BK +take offense BTJl BQ +take ones measure for BTLy BB +take over BTMz o +take part BTNb BC +take refuge BTOd y +take somebodys name BTPP BN +take steps BTQc BV +take the bait BTRx w +take the place of BTSh Ba +take up BTT7 Bi +taken aback BSpx B9 +taken in BSru m +taking BTVd Y +taking over BTV1 l +tale BTWa z +talent BTXN 3 +talented BTYE BF +talisman BTZJ d +talk BTZm e +talkative BTaE j +talkativeness BTan s +tall BTbT BP +tallow BTci 0 +talon BTdW W +tamarisk BTds f +tambon BTeL Z +tambourine BTek BA +tame BTfk 4 +tampon BTgc Z +tan BTg1 l +tangerine BTha z +tangle BTiN BX +tango BTjk g +tank BTkE BA +tannin BTlE s +tantalum BTlw g +tap BTmQ BU +tap at BTrr b +tape BTpe BK +taper BTqo Z +taperecorder BTnk BD +taperecording BTon 3 +tapestry BTrB q +tar BTsG e +tarantula BTsk k +tard BTtI 0 +tardy BTt8 l +target BTuh 5 +tart BTva BH +tartness BTwh f +task BTxA q +taskmaster BTxq BE +taste BTyu 9 +tasteless BTzr / +tasty BT0q BE +tattered BT1u p +tattler BT2X BH +tattoo BT3e Y +tau BT32 O +taunt BT4E l +tavern BT4p m +tax BT7S BZ +tax abatement BT/D 0 +tax burden BT/3 o +tax collector BUAf q +tax consultant BUBJ 2 +tax deduction BUB/ x +tax form BUCw m +tax hike BUDW o +tax increase BUD+ w +tax reduction BUEu 0 +tax relief BUFi t +tax return BUGP t +tax system BUG8 r +tax with BUHn m +taxcollectors office BT5P BK +taxfree BT6Z 5 +taxi BT9d V +taxidriver BT8r y +taxidriver BT9y p +taxpayer BT+b o +tea BUKM Q +teach BUKc Be +teacher BUL6 By +teahouse BUIN m +teak tree BUNs u +team BUOa h +team of three horses BUO7 3 +teapot BUIz i +teapot BUPy m +tear BUQY Bn +tear off BUR/ BI +tease BUTH V +teaspoon BUTc j +teat BUT/ e +teatowel BUJV 3 +technetium BUUd j +technical BUVA e +technics BUVe c +technique BUV6 e +technology BUWY p +teeth BUXB V +teetotalism BUXW 9 +teetotaller BUYT 2 +teetotum BUZJ p +telegram BUZy e +telegraph BUaQ BB +telemetry BUbR k +telephone BUb1 6 +telephone booth BUcv t +telephone kiosk BUdc v +telescope BUeL 8 +television BUfH h +television aerial BUfo BG +television antenna BUgu BG +television film BUh0 q +television movie BUie v +television set BUjN 6 +tell BUkH Ck +tell a lie BUoT e +telltale BUmr BH +tellurium BUny h +temper BUox BL +temperament BUp8 n +temperate BUqj BA +temperature BUrj o +template BUsL x +temple BUs8 2 +templet BUty v +temporarily BUuh BZ +temporary BUv6 Bo +tempt BUxi BO +temptation BUyw BV +tempting BU0F p +ten BU0u O +ten thousand BVGE l +tenant BU08 Z +tench BU1V V +tend to BVAu 0 +tendencious BU1q l +tendency BU2P C+ +tendentious BU5N m +tender BU5z EL +tenderness BU9+ CU +tendon BVAS c +tennis BVBi W +tenor BVB4 g +tenor violin BVCY z +tension BVDL 4 +tent BVED k +tented camp BVEn 2 +tenth BVFd n +terbium BVGp a +term BVHD c +term of abuse BVNz 7 +term of office BVOu t +terminal BVHf e +terminate BVH9 B3 +terminology BVJ0 BQ +termite BVLE y +terms BVL2 v +terms of employment BVMl BO +terrace BVPb Y +terrace house BVQc n +terraced house BVPz p +terrain BVRD Y +terrestrial BVRb t +terrible BVSI BW +terrify BVTe BA +territorial BVUe q +territory BVVI BH +terror BVWP BR +terrorism BVXg j +terrorist BVYD g +tertian fever BVYj 2 +test BVZZ DX +testament BVcw BK +testicle BVd6 0 +testify BVeu q +testimony BVfY Bb +text BVgz T +textbook BVhG o +textile BVhu 3 +thallium BVil g +than BVjF R +thank BVjW BC +thank for BVnG z +thank you BVn5 n +thank you very much BVog / +thankful BVkY w +thankless BVlI j +thanks BVlr 6 +thanks to BVml h +that BVpf BJ +that is BVvh h +that kind of BVwC Bc +that much BVxe 1 +that one BVyT k +that one over there BVzt Bj +that ones BVy3 2 +that over there BV1Q BG +that over there BVt1 Bs +that way BV2W BP +thats all right BVqo BQ +thats okay BVr4 BO +thats too bad BVtG v +thaw BV3l 3 +the BV4c BP +the antarctic BWXa BF +the arctic BWYf z +the day after tomorrow BWe1 8 +the day before yesterday BWfx BM +the dustman BWZS l +the far east BWZ3 o +the netherlands BWaf r +the power of suggestion BWg9 BZ +the proletariat BWiW y +the same BWjI m +the sandman BWbK l +the sleeping beauty BWbv 3 +the the BWeT i +the transvaal BWcm p +the ukraine BWdP h +the vatican BWdw j +theatre BV5r 9 +theatre play BV6o u +theatrical BV7W 9 +theatrical tour BV8T 0 +thee BV9H BG +thee and thou BV+N 7 +theft BV/I l +their BV/t l +theirs BWAS l +them BWA3 5 +theme BWBw v +then BWCf BS +thence BWDx z +theocracy BWEk l +theologian BWFJ l +theology BWFu 0 +theorem BWGi n +theory BWHJ b +theory of music BWHk y +therapy BWIW e +there BWI0 Bo +there are BWP5 m +there is BWQf i +thereabout BWKc BO +thereabouts BWLq BQ +thereafter BWM6 w +thereby BWNq l +therefore BWOP BK +therein BWPZ g +thermometer BWRB 1 +thermos BWR2 h +thermos bottle BWS/ t +thermos flask BWTs v +thermostat BWSX o +these BWUb o +these days BWVD l +thesis BWVo + +theta BWWm W +they BWW8 e +thick BWju v +thicken BWkd n +thickness BWlE n +thief BWlr r +thigh BWmW c +thimble BWmy q +thin BWnc Bn +thing BWpD BQ +thingamajig BWqT x +things BWrE g +think BWrk CC +think about BWuF BV +think ahead BWva BW +think to oneself BWww BK +think well of BWx6 BY +thinking BWtm f +third BWzS W +thirst BWzo Y +thirst after drinking to excess BW0e BN +thirsty BW0A e +thirteen BW1r f +thirteenth BW2K l +thirty BW2v b +this BW3K m +this afternoon BW42 u +this evening BW5k y +this here BW6W m +this morning BW68 1 +this way BW7x Bo +thisle BW4g W +thislefield BW3w w +thither BW9Z c +thorium BW91 c +thorn BW+R W +thorn bush BW+n l +thoroughgoing BW/M x +thoroughly BW/9 8 +those BXA5 7 +thou BXB0 l +though BXCZ y +thought BXDL b +thoughtful BXDm BC +thoughtfulness BXEo BP +thousand BXF3 b +thrash BXGS z +thread BXHF z +threat BXH4 w +threaten BXIo z +threatening BXJb t +three BXMP U +three times BXMj u +threepiece suite BXKI 6 +threetoed sloth BXLC BN +thresh BXNR 5 +threshold BXOK o +thrice BXOy j +thrifty BXPV e +throat BXPz l +throb BXQY h +throes BXQ5 b +throne BXRU W +through BXRq Ef +throughout BXWJ Ba +throughout the BXXj Bu +throw BXZ6 w +throw away BXaq 6 +throw off BXbk z +throw snowballs BXcX + +throw up BXdV / +throwback BXZR p +thrush BXeU Z +thrust BXet w +thrust down BXfd j +thrust out BXgA v +thulium BXgv d +thumb BXhM T +thunder BXhf 9 +thurible BXic i +thus BXi+ C0 +thwart BXly f +tibia BXmR i +tic BXmz e +tic douloureux BXqv 1 +tick BXnR Q +tick off BXqB u +ticket BXoP BQ +ticketwindow BXnh u +tickle BXpf i +tidbit BXrk 6 +tide BXse X +tidy BXs1 B5 +tie BXuu C1 +tie on BXxj Bi +tie up BXzF Bg +tiger BX0l X +tight BX08 3 +tighten BX1z BB +tightly BX20 Z +tightness of the chest BX3N BE +tilde BX4R T +tile BX4k v +till BX5T BA +tillage BX6T BO +tiller BX7h Z +tilt BX76 s +timber BX8m i +timbrel BX9I 5 +time BX+7 v +time off BX/q c +time to consider BYAG z +timetable BX+B 6 +timid BYA5 BL +timidity BYCE w +tin BYDs Bm +tin can BYKD 6 +tin plate BYK9 8 +tinder BYFS y +tinker BYGE e +tinkle BYGi BJ +tinopener BYC0 4 +tint BYHr BK +tiny BYI1 BO +tip BYL5 CI +tipsy BYOB 7 +tire BYO8 T +tire lever BYRd o +tire out BYSF 0 +tire tread BYS5 h +tired BYPP i +tired out BYPx u +tiresome BYQf + +titanic BYTa n +titanium BYUB g +title BYUh BS +titmouse BYVz Y +to BYW1 BN +to a certain degree BZeB / +to be continued BZfA u +to both sides BZfu BB +to her BZgv BP +to him BZh+ 8 +to it BZi6 BM +to me BZkG 5 +to my thinking BZk/ w +to some extent BZlv Bz +to some place BZni p +to tell the truth BZoL Bi +to the BZpt 7 +to the east of BZss 4 +to the end BZtk z +to the left BZuX 1 +to the letter BZvM 3 +to the point BZwD s +to the right BZwv 5 +to the side BZxo p +to thee BZqo BM +to them BZr0 4 +to us BZyR e +to what extent BZyv s +to you BZzb Br +toad BYYC Q +toast BYYS B3 +tobacco BYaJ a +tobacconists BYaj t +today BYbQ e +todo BYWL q +toe BYbu P +toga BYb9 T +together BYcQ 8 +together with BYdM CE +toilet BYgf 4 +toilet brush BYhX 3 +toiletpaper BYfQ BP +token BYiO BY +tolerant BYjm 6 +tolerate BYkg BV +tolling BYl1 r +tomato BYnU c +tomato juice BYnw s +tomatosoup BYmg 0 +tomb BYoc a +tomorrow BYo2 c +tomorrow afternoon BYpS 5 +tomorrow evening BYqL x +tomorrow morning BYq8 0 +ton BYrw P +tone BYr/ c +tone down BYsb f +tongs BYs6 BC +tongue BYt8 f +tonight BYub b +tonsil BYu2 h +too BYvX BL +too much BY0A 1 +tool BYwi h +tooth BYyg U +toothache BYy0 p +toothbrush BYxD x +toothpaste BYx0 s +toothpick BYzd j +top BY01 Bm +top of the head BY6C l +topaz BY2b X +topgallant mast BY2y w +topic BY3i BQ +topical BY4y o +topicality BY5a o +torch BY6n q +torment BY7R B8 +torn BY9N Y +torpedo BY9l 7 +torrent BY+g t +tortoise BY/N g +torture BY/t BZ +torturer BZBG f +toss BZBl d +toss off BZCC 7 +total BZC9 5 +total abstainer BZEu + +total abstinence BZFs BC +totalitarianism BZD2 4 +totem BZGu W +toucan BZHE Z +touch BZHd BZ +touch lightly BZKU 4 +touch lightly on BZLM x +touch on BZL9 h +touch up BZMe s +touch upon BZNK v +touching BZI2 Be +tour BZN5 f +tourism BZOY e +tourist BZO2 b +tourist agency BZPR x +tourist bureau BZQC y +tournament BZQ0 v +tousle BZRj BO +tow BZSx z +toward BZTk CG +towards BZVq C2 +towel BZYg Y +tower BZY4 V +towingline BZZN BF +town BZbI k +town hall BZca 4 +townhall BZaS 2 +township BZbs u +toy BZdS v +trace BZ1G 3 +trace out BZ19 f +track BZ2c 8 +tractability BZ3Y BH +tractable BZ4f x +tractor BZ5Q l +trade BZ7X CJ +trade and industry BZ9g BG +tradeunion BZ51 Bi +trading BZ+m s +tradition BZ/S v +traditional BaAB o +traffic BaAp BG +traffic block BaBv / +traffic congestion BaCu BO +traffic jam BaD8 + +traffic rules BaE6 t +traffic tieup BaFn BN +tragedy BaG0 7 +tragic BaHv c +trail BaIL s +trailer BaI3 g +train BaJX Bn +train ticket BaK+ n +trait BaLl u +trajectory BaMT u +tram BaNB S +tramping BaNT o +trample on BaN7 w +tranquil BaOr z +transact money BaQG 2 +transaction BaPe o +transfer BaQ8 CP +transform BaTL CF +transformation BaVQ D5 +transgress BaZJ t +transgression BaZ2 l +transistor Baab q +transitive BabF z +translate Bab4 6 +translation Bacy BL +transliteration Bad9 BM +transmit BafJ BS +transparant Bagb 2 +transport BahR Bo +transporter Bai5 BO +transverse BakH g +transvestite Bakn o +trap BalP h +travel Balw 7 +travel agency BaoQ w +travel as passenger BapA BD +travelled Bamr e +traveller BanJ f +travelling Bano o +travesty BaqD i +tray Baql BB +treacherous Barm BF +treachery Basr h +tread BatM B/ +treasure BavL a +treat Bavl CK +treat as Ba0P k +treat to Ba0z v +treatise Baxv h +treatment BayQ BM +treaty Bazc z +tree Ba1i b +tree Ba2v g +treetrunk Ba19 y +trefoil Ba3P a +tremble Ba3p 8 +trench Ba4l b +tressle Ba5A BL +trial Ba6L BE +triangle Ba7P p +triangular Ba74 o +tribal Ba8g Y +tribe Ba84 r +tribunal Ba9j BM +tribune Ba+v c +tribute Ba/L l +trick Ba/w C6 +trickery BbCq BL +trickle down BbD1 w +tricycle BbEl f +tried BbFE X +trifle BbFb Bo +trifling BbHD BO +trigger BbIR v +trigonometry BbJA BA +trill BbKA q +trillion BbKq d +trilogy BbLH g +trim BbLn BR +trimming BbM4 n +trip BbNf k +triple BbOD m +trishaw BbOp e +trite BbPH BO +triteness BbQV w +triumph BbRF BU +triviality BbSZ 1 +trolley BbT6 j +trolleybus BbTO s +trollop BbUd z +trombone BbVQ t +troop BbV9 v +tropical BbWs d +tropics BbXJ d +trot BbXm f +trouble BbYF DY +troublesome Bbbd BL +trough Bbco 8 +troupe Bbdk d +trousers BbeB x +trousers wearing Bbey y +trout Bbfk U +truce Bbf4 n +truck Bbgf w +truculent BbhP 9 +true BbiM Ba +truly Bbjm Bi +trump BblI u +trumpet Bbl2 Bp +truncate Bbnf 7 +trunk Bboa B0 +trust BbqO Cf +trust in Bbtj BD +trustworthy Bbst 2 +truth Bbum s +try BbvS Bu +try on BbxA o +tub Bbxo i +tube BbyK Bp +tuberculosis Bbzz BG +tuft Bb05 x +tufted lark Bb1q q +tug Bb2U t +tugboat Bb3B d +tulip Bb3e W +tumble down Bb30 + +tummy Bb4y V +tummy ache Bb5H h +tumor Bb5o q +tumour Bb6S r +tumult Bb69 BX +tumulus Bb8U i +tuna Bb9e X +tunafish Bb82 o +tune Bb91 By +tuneful Bb/n BC +tungsten BcAp f +tunic BcBI g +tunnel BcBo W +tunny BcB+ X +tup BcCV R +turf BcCm d +turkey BcDD b +turmoil BcDe 6 +turn BcEY GW +turn around BcOq B6 +turn away BcQk CP +turn back BcSz BF +turn off BcT4 B1 +turn on BcVt Cj +turn over BcYQ B8 +turn round BcaM B3 +turn to account BccD B0 +turn to good account Bcd3 CT +turnedin legs BcKu 0 +turnip BcMs 0 +turniprooted celery BcLi BK +turnover BcNg e +turnpike BcN+ s +turret BcgK u +turtledove Bcg4 2 +tussle Bchu 8 +tutor Bciq BK +twaddle Bcj0 v +twelfth Bckj b +twelve Bck+ W +twentieth BclU i +twenty Bcmj a +twentyone Bcl2 t +twice Bcm9 n +twich Bcnk j +twig BcoH e +twill Bcol T +twin Bco4 z +twine Bcpr X +twins BcqC X +twist BcqZ CQ +twitter Bcsp 2 +two Bcvl Q +two hundred Bcv1 o +two weeks Bcwd y +twofooted Bctf r +twoyear BcvA l +twoyearold BcuK 2 +type BcxP BF +typeset BcyU c +typewriter Bcyw o +typewriter carriage BczY BF +typhoon Bc0d a +typhus Bc03 X +typical Bc1O m +typist Bc10 Z +tyrannize Bc2N v +tyranny Bc28 r +tyrant Bc3n w +tyre Bc4X t +tyre lever Bc5E o +ubiquitous Bc5s s +udder Bc6Y a +ugly Bc6y t +ulcer Bc7f W +ultimately Bc71 / +ultimatum Bc80 j +umbrella Bc9X d +umpire Bc90 r +un Bc+f n +unabbreviated Bc/G s +unanimous Bc/y v +unarmed BdAh f +unattached man BdBA w +unbaptized BdBw k +unbearable BdCU 1 +uncertain BdDJ z +uncharted BdD8 x +uncivilized BdEt n +uncle BdFU BD +unclean BdGX 3 +unconscious BdHO BS +uncontrollable BdIg w +uncooked BdJQ j +uncouple BdJz 1 +uncover BdKo f +undecided BdLH l +under BdLs f +under age Bdfu v +under the circumstances Bdgd Be +underestimate BdML + +undergarment BdNJ t +underground BdN2 / +underhand BdO1 s +underhanded BdPh 4 +underline BdQZ k +undermine BdQ9 v +underneath BdRs B0 +underpants BdTg l +underscore BdUF p +understand BdUu BT +understandable BdWB BY +understandably BdXZ 4 +understanding BdYR B5 +understood BdaK h +undertake Bdar Bm +undertaker BdcR w +undertakers business BddB BD +undertakers man BdeE / +underwater BdfD r +undoubtedly Bdh7 5 +undress Bdi0 B2 +undue Bdkq q +undulate BdlU d +unemployed Bdlx t +unemployment Bdme 7 +unexpectedly BdnZ 3 +unfaithful BdoQ 4 +unfit for work BdpI 3 +unfortunate Bdp/ r +unfortunately Bdqq B5 +unfounded Bdsj h +unfruitful BdtE o +ungrateful Bdts l +unhappy BduR g +unharness Bdux i +unhook BdvT i +unicorn Bdv1 g +unified BdwV h +uniform Bdw2 BE +unilateral Bdx6 o +unimportant Bdyi 1 +unintelligible BdzX BD +unintentional Bd0a 3 +union Bd1R o +unique Bd15 f +unit Bd2Y g +unite Bd24 BS +united Bd4K d +unity Bd4n x +universal Bd5Y v +universality Bd6H BE +universe Bd7L 1 +university Bd8A 2 +university graduate Bd82 + +unkempt Bd90 c +unkind Bd+Q BS +unknown Bd/i b +unlearn Bd/9 BQ +unleavened bread BeBN x +unless BeB+ Y +unload BeCW r +unlock BeDB c +unmarried BeDd r +unmarried woman BeEI r +unoccupied BeEz 0 +unordinary BeFn w +unpack BeGX b +unpleasant BeGy BX +unreliable BeIJ w +unrest BeI5 Z +unrig BeJS v +unripe BeKB l +unripe fruit BeKm s +unroll BeLS 3 +unrolling BeMJ j +unseemly BeMs t +unseen BeNZ b +unsocial BeN0 x +unsound BeOl z +unthankful BePY n +untidiness BeP/ 7 +untidy BeQ6 BC +untie BeR8 u +until BeSq p +untrodden BeTT h +untroubled BeT0 y +untrue BeUm Z +unusual BeU/ x +unwell BeVw j +unwind BeWT d +unwinding BeWw k +up BeYN q +uphill BeY3 x +upholster BeZo 0 +upload Beac a +upon Bea2 B+ +upon which Bec0 g +upper Bed9 n +upper arm Beek h +upper hand BefF BJ +upper side BegO u +upperarm BedU p +upset Beg8 D4 +upsilon Bek0 i +upstairs BelW 0 +uptodate BeXU 5 +upwards BemK 0 +uranium Bem+ d +urban Benb s +urchin BeoH l +urge Beos CE +urge on BetR BG +urgency Beqw 4 +urgent Bero 9 +urgently Besl s +urinate BeuX BU +urine Bevr h +us BewM m +use Bewy Bc +use up Be3U CO +used BeyO i +used to Beyw p +used up BezZ BJ +useful Be0i y +useless Be1U Bm +user Be26 a +usual Be5i BB +usually Be6j + +usurp Be7h BJ +usury Be8q u +uterus Be9Y g +utility Be94 f +utilize Be+X Bg +utopian Be/3 t +utter BfAk BK +vacant BfBu BH +vacation BfC1 g +vaccinate BfDV r +vaccine BfEA s +vacuum BfFg v +vacuum cleaner BfGP v +vacuumcleaner BfEs 0 +vagabond BfG+ p +vagina BfHn k +vague BfIL x +vain BfI8 BP +valerian BfKL i +valiant BfKt 2 +valid BfLj z +validate BfMW m +valise BfM8 t +valley BfNp d +valuable BfOG BH +value BfPN BL +valve BfQY 8 +vampire BfRU 1 +van BfSJ j +vanadium BfSs g +vandal BfTM o +vanilla BfT0 b +vanity BfUP p +vanity bag BfU4 w +vanity case BfVo q +vapor BfWS j +vapour BfW1 0 +variable BfXp 8 +variation BfYl 0 +varied BfZZ 5 +variety BfaS 5 +variety entertainment BfbL 9 +various BfcI 8 +varnish BfdE z +vary Bfd3 v +vase Bfem i +vaseline BffI g +vassal Bffo g +vast BfgI BP +vat BfhX i +vault Bfh5 c +vaulted ceiling BfiV w +veal BfjF Y +vegetable Bfjd g +vegetarian Bfj9 8 +vegetate Bfk5 p +vegetation Bfli BU +vehicle Bfm2 0 +veil Bfnq 9 +veiled Bfon BJ +vein Bfpw 0 +velvet Bfqk w +vend BfrU BK +venerability Bfse v +venereal BftN w +venereal disease Bft9 3 +vengeance Bfu0 d +venison BfvR c +ventilate Bfvt BL +ventilator Bfw4 q +venture Bfxi By +veranda BfzU d +verb Bfzx Y +verbatim Bf0J y +verdict Bf07 7 +verge Bf12 W +verge of a road Bf2M p +verify Bf21 BQ +vermin Bf4F d +vermouth Bf4i e +verse Bf5A BB +version Bf6B o +vertebra Bf6p e +vertical Bf7H BE +vertigo Bf8L t +verve Bf84 BO +very Bf+G 7 +very ancient Bf/B 4 +very best Bf/5 h +very much BgAa BG +very well BgBg v +vessel BgCP Bv +vest BgD+ n +vestal BgEl o +vestal virgin BgFN 5 +vetch BgGG U +vetchling BgGa f +veteran BgG5 q +veterinarian BgHj 4 +vex BgIb BC +vibrate BgJd k +vibration BgKB BN +vicar BgLO X +vice BgOL 3 +vicepresident BgLl Bg +viceversa BgNF BG +vicinity BgPC s +vicious BgPu Br +victim BgRZ v +victor BgSI d +victory BgSl x +video recorder BgVu y +videorecorder BgTW 9 +videotape BgUT 5 +videotape BgVM i +view BgWg D+ +viewpoint Bgae w +vigour BgbO BW +viking Bgck Y +vile Bgc8 9 +villa Bgd5 3 +village Bgew f +villager BgfP g +villain Bgfv t +vinagerbottle Bggc 3 +vine BghT 4 +vinegar BgiL f +vineyard Bgiq d +viola BgjH m +violate Bgjt B2 +violence Bglj s +violently BgmP k +violet Bgmz BT +violin BgoG Y +violoncello Bgoe o +viper BgpG W +virgin Bgpc 1 +virginal BgqR s +virgule Bgq9 d +virility Bgra z +virtual BgsN g +virtue Bgst BP +virtuoso Bgt8 j +virus Bguf Y +visa Bgu3 V +visible BgvM m +vision Bgvy 6 +visit Bgws + +visit a person BgzD 1 +visit a place Bgz4 x +visit regularly Bg0p BA +visitor Bgxq c +visitors register BgyG 9 +visual acuity Bg1p 2 +visé Bg2f Y +vital Bg23 q +vitamin Bg3h c +vitamin c Bg39 w +vivacious Bg4t 6 +vivacity Bg5n BW +vixen Bg69 Bo +viz Bg8l n +vocabulary Bg9M BK +vodka Bg+W W +voice Bg+s 0 +void Bg/g q +volcano BhAK w +volley BhA6 r +voltage BhBl m +volume BhCL BN +voluntarily BhDY BN +voluntary BhEl z +volunteer BhFY x +voluptuous BhGJ + +vomit BhHH 6 +vote BhIB 1 +voting by ballot BhI2 6 +vow BhJw p +vowel BhKZ s +voyage BhLF q +vulgar BhLv BW +vulture BhNF o +wadding BhNt Z +wade BhOG m +wafer BhOs Z +waffle BhPF c +wage BhPh BO +wage war BhSr s +wage war against BhTX v +wage war on BhUG n +wager BhQv r +wages BhRa BR +waggon BhUt j +wah BhVQ P +wail BhVf s +wainscot BhWL Br +waist BhX2 k +waistcoat BhYa l +waistline BhY/ s +wait BhZr BW +wait for Bhd7 Be +waiter BhbB Y +waiting BhcK c +waiting room Bhcm n +waitingroom BhbZ x +waitress BhdN u +waive BhfZ s +wake BhgF BJ +wake up BhhO BP +walk Bhid Bw +walk back BhkN m +walk backwards Bhkz w +walk upon Bhlj z +wall BhoI BC +wall covering BhrS 0 +wallet BhpK BC +wallflower BhqM l +wallpainting BhmW BJ +wallpaper Bhqx h +walltile Bhnf p +walnut BhtO n +walnuttree BhsG BI +waltz Bht1 V +wand BhuK t +wander Bhu3 B6 +wander about Bhxv BI +wander from ones subject Bhy3 BC +wandering from the point Bhwx + +wane Bhz5 BF +want Bh0+ Bp +want of civilization Bh3q / +want to have Bh4p y +want to know Bh5b t +wanton Bh2n e +wantonness Bh3F l +wapiti Bh6I Z +war Bh6h Y +war of aggression BiFx z +ward off Bh7j x +wardrobe Bh65 q +warehouse Bh8U r +wares Bh8/ BB +warlike Bh+A x +warlock Bh+x w +warm Bh/h q +warmly BiAL p +warmth BiA0 Z +warn BiBN a +warning BiBn 2 +warrant BiCd CR +warrior BiEu w +wart BiFe T +wash BiGk 4 +wash clothes BiK6 q +wash off BiLk s +wash up BiMQ 9 +washable BiHc w +washingbear BiIM s +washingup bowl BiI4 BD +washingup liquid BiJ7 / +wasp BiNN S +waste BiOK Cl +waste product BiRJ 4 +wasted BiQv a +wastepipe BiNf r +watch BiSB Ch +watch out for BiXr BW +watch over BiZB BQ +watchmaker BiUi BE +watchman BiVm 0 +watchword BiWa BR +water BiaR 7 +water Bif1 Ca +water buffalo Bimr v +waterbottle BibM v +waterbuffalo Bib7 5 +watercolour Bic0 BV +watercourse BiiP 0 +watercress BijD k +waterfall Bijn k +watering Bik6 BL +wateringcan BikL v +waterlily BimF m +watermelon BieJ x +waterplaintain Bie6 7 +watt Bina R +wave Binr Bs +wave about BirX 0 +waver BipX BB +wavering BiqY / +wax BisL w +wax candle Bis7 i +way Bitd BV +way out Biuy o +we Biva Z +weak Bivz Z +weaken BiwM BL +weakly BixX y +wealth BiyJ X +wealthy Biyg x +weapon BizR W +wear Bizn BM +wear away Bi1O BS +wear down Bi2g r +wear off Bi3L Bc +wear out Bi4n B1 +weary Bi0z b +weasel Bi6c W +weather Bi6y z +weather report Bi7l 8 +weave Bi8h U +weaverbird Bi81 u +wed Bi9j 6 +wedding BjA2 Bn +weddingfeast Bi+d 1 +weddingparty Bi/S Bk +wedge BjCd BH +weed BjDk g +weed out BjEE n +week BjFa S +weekend BjEr v +weekly BjFs o +weep BjGU BG +weever BjHa d +weigh BjH3 BB +weighing machine BjI4 BN +weight BjKF r +weightlessness BjKw u +weighty BjLe f +weird BjL9 BS +welcome BjNP Ba +weld BjOp b +well BjY9 CF +well then BjbC BF +well up BjcH BH +wellbeing BjPE Bv +wellbeloved BjQz Bc +wellinformed BjSP BA +wellknown BjTP x +wellmannered BjUA Bs +welloff BjVs 4 +wellread BjWk m +welltodo BjXK BK +welltried BjYU p +wench BjdO c +west Bjdq 7 +western Bjel u +westward BjfT w +westwards BjgD y +wet Bjg1 e +wet season BjhT l +whack Bjh4 Y +whale BjiQ T +whalebone Bjij f +wharf BjjC 1 +what Bjj3 e +what a BjnN BO +what a pity Bjob q +what for BjpF y +what kind of Bjp3 Bb +what kind of BjrS BH +what number BjsZ m +what sort of Bjs/ Bc +what time Bjub e +what time is it Bju5 x +whatchamacallit BjkV 2 +whatever BjlL 8 +whats more BjmH BG +wheat Bjvq a +wheel BjwE Y +wheelbarrow Bjwc n +wheeziness BjxD x +wheezy Bjx0 1 +when Bjyp j +whence BjzM 0 +where Bj0A BF +where to Bj5c BH +whereabouts Bj1F 9 +whereas Bj2C BH +whereby Bj3J m +wherefore Bj3v h +wherein Bj4Q o +wherever Bj44 k +which Bj6j f +which ones Bj74 BA +whichever Bj7C 2 +while Bj84 BQ +whilst Bj+I BB +whim Bj/J 1 +whimstone Bj/+ c +whinny BkAa Z +whip BkAz Bn +whipped cream BkCa k +whirl BkC+ s +whirlwind BkDq BG +whiskers BkEw e +whiskey BkFO Z +whisky BkFn Y +whisper BkF/ BP +whist BkHO T +whistle BkHh 8 +white BkKp d +white man BkLv n +white waterlily BkMW BC +whiten BkLG p +whitetailed eagle BkId 8 +whitewash BkJZ BQ +whither BkNY 9 +who BkOV v +whoever BkPE z +whole BkQq BR +wholehearted BkP3 z +wholesale BkR7 i +wholly BkSd BJ +whom BkTm j +whore BkUJ BQ +whorehouse BkVZ 9 +whose BkWW 1 +whose BkXL x +why BkX8 p +why BkYl f +wick BkZE n +wide BkZr BM +widely Bkbq t +widelytravelled Bka3 z +widow BkcX V +widower Bkcs d +width BkdJ z +wife Bkd8 n +wig Bkej Q +wild Bkez Z +wild animal Bkf2 BL +wild boar BkhB 0 +wild card Bkh1 v +wild duck Bkik h +wild goat BkjF h +wildebeest BkfM q +will Bkku Bc +willing BkmK BI +willingly BknS z +willingness BkoF h +willlessness Bkjm BI +willow Bkom U +willowherb Bko6 l +willy Bkql r +willynilly Bkpf BG +win BkrQ l +win back Bk7W m +win over Bk78 BA +wind Bksy CW +wind around Bkzt w +wind instrument Bk0d z +wind off Bk1Q f +wind up Bk1v CO +windfall BkvI q +windinstrument Bkr1 9 +windlessness Bkvy m +window Bkyl q +windowframe BkwY BW +windowglass Bkxu 3 +windpipe BkzP e +wine Bk39 R +wing Bk4O t +winged Bk47 e +winkle Bk5Z Z +winter Bk5y u +winter Bk6g 2 +wipe Bk88 3 +wipe off Bk9z + +wipe out Bk+x o +wire BlAR + +wireless BlBP m +wireless telegraph BlB1 BH +wireshears Bk/Z 4 +wiring BlC8 g +wisdom BlDc a +wise BlD2 k +wisely BlEa e +wisent BlE4 W +wish BlFO B6 +wit BlHI d +witch BlHl u +witchcraft BlIT j +with BlI2 BG +with difficulty BlYm 7 +with effort BlZh 3 +with one accord BlaY Bf +with reference to Blb3 Bj +with the exception of Blda BB +with wisdom Bleb k +with young Ble/ p +withdraw BlJ8 Bd +withdraw ones name BlLZ BO +withdraw ones subscription BlMn Bg +wither BlOH BG +withhold BlPN h +within BlPu 4 +without BlQm BR +without a name BlR3 z +without delay BlSq Bl +without fail BlUP Bu +without prejudice BlV9 y +withstand BlWv B3 +witness Blfo BL +witticism Blgz 3 +witty Blhq u +wizard BliY s +wizardry BljE e +woe Blji T +wolf Blj1 R +wolverine BlkG g +woman BllW V +woman writer Bllr x +womanchaser Blkm w +womb Blmc Y +wonder Blm0 CG +wonderful Blo6 BN +wonted BlqH m +woo Blqt t +wood Blsg Q +wooddye Blra h +wooden Blsw V +wooden cloth BltF g +woodpecker Bltl e +woodpile BluD v +woods Bluy X +woodstain Blr7 l +wool BlvJ c +word Blws g +wordforword Blvl BH +work Blyi Cx +work hard BmBm z +work of art BmCZ BB +work out BmDa Bi +workbasket BlxM u +workbench Bl1T BU +worker Bl2n BK +workers Bl3x x +workhouse Bl4i BC +working capital Bl5k BE +working class Bl6o BB +working classes Bl7p BF +working man Bl8u BR +workman Bl9/ 7 +workmen Bl+6 x +workplace Bl/r s +workroom Blx6 o +works BmAX f +workshop BmA2 w +world BmE8 h +worldglobe BmFd 1 +worldwar BmGS k +worldwide BmG2 + +worm BmI2 Z +wormeaten BmH0 BC +worn BmJ2 c +worn out BmKS t +wornout BmJP n +worries BmK/ 8 +worry BmL7 CA +worry oneself BmN7 t +worse BmOo Y +worship BmPA Bp +worship service BmR1 BW +worshipper BmQp BM +worst BmTL Z +worsted BmTk n +worth BmUL BK +worth reading BmW3 u +worth seeing BmXl t +worthless BmVV 3 +worthy BmWM r +wound BmYS Bj +wounded BmZ1 t +woundwort Bmai f +wrangle BmbB BS +wrap up Bmd0 u +wrapper BmcT u +wrappingpaper BmdB z +wreath Bmei BL +wreathe Bmft a +wreck BmgH h +wren Bmgo a +wrench BmhC Y +wrest Bmha r +wrestle BmiF h +wretched Bmim BP +wring Bmj1 r +wrinkle Bmkg BV +wrist Bmms h +wristwatch Bml1 3 +writ BmnN t +write Bmn6 BW +write down BmqO Br +write off Bmr5 g +write shorthand BmsZ x +write upon BmtK y +writer BmpQ + +writhe Bmt8 z +writing BmxJ BN +writing desk BmyW / +writingdesk Bmuv BK +writingpaper Bmv5 BQ +written BmzV c +wrong Bmzx BY +xenon Bm1J V +xerographic Bm1e q +xerographic copy Bm2I z +xerography Bm27 4 +xi Bm3z O +xylophone Bm4B g +yacht Bm4h T +yam Bm40 6 +yard Bm5u + +yarrow Bm6s c +yawn Bm7I BB +ye Bm8J l +year Bm9b T +yearbook Bm8u t +yearly Bm9u u +yearn Bm+c BQ +yeast Bm/s U +yell BnAA 6 +yellow BnA6 U +yellow fever BnBO p +yen BnB3 O +yes BnCF U +yesterday BnCZ z +yet BnDM B1 +yewtree BnFB o +yield BnFp BE +yodel BnGt W +yoga BnHD T +yoghourt BnHW k +yoghurt BnH6 j +yoke BnId z +yon BnJQ q +yonder BnJ6 BA +you BnK6 Bw +young BnMq Bz +young of a wild boar BnTI BI +young woman BnUQ k +younger brother BnOd BW +younger sibling BnPz BP +younger sister BnRC BT +youngster BnSV z +your BnU0 o +youre welcome BnYt Bu +yours BnVc BA +yourself BnWc k +youth BnXA BE +youth hostel BnYE p +ytterbium Bnab f +yttrium Bna6 c +zap BnbW S +zeal Bnbo i +zealous BncK + +zebra BndI i +zen Bndq N +zenith Bnd3 i +zephyr BneZ l +zero Bne+ e +zest Bnfc BM +zeta Bngo T +zigzag Bng7 BO +zilch BniJ a +zinc Bnij S +zipper Bni1 x +zirconium Bnjm k +zither BnkK X +zodiac Bnkh l +zone BnlG 3 +zoo Bnl9 W +zoologist BnmT y +zoology BnnF v +zucchini Bnn0 g diff --git a/apps/readest-app/src/__tests__/services/dictionaries/_dictFixtures.ts b/apps/readest-app/src/__tests__/services/dictionaries/_dictFixtures.ts new file mode 100644 index 00000000..d947ec0c --- /dev/null +++ b/apps/readest-app/src/__tests__/services/dictionaries/_dictFixtures.ts @@ -0,0 +1,28 @@ +/** + * Shared DICT (dictd) test fixture loader. + * + * Points at the real FreeDict English-Dutch bundle in + * `src/__tests__/fixtures/data/dicts/`. 7,720 entries, dictzip-compressed + * body, plain UTF-8 plain-text definitions — small enough for fast tests + * but large enough to exercise multi-chunk reads. + */ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +const FIXTURES_DIR = path.resolve(__dirname, '../../fixtures/data/dicts'); + +export const INDEX_FIXTURE_PATH = path.join(FIXTURES_DIR, 'freedict-eng-nld.index'); +export const DICT_FIXTURE_PATH = path.join(FIXTURES_DIR, 'freedict-eng-nld.dict.dz'); + +export const INDEX_FIXTURE_NAME = 'freedict-eng-nld.index'; +export const DICT_FIXTURE_NAME = 'freedict-eng-nld.dict.dz'; + +async function readAsFile(filePath: string, name: string): Promise { + const bytes = await readFile(filePath); + const buf = new Uint8Array(bytes.length); + buf.set(bytes); + return new File([buf], name); +} + +export const readIndexFile = () => readAsFile(INDEX_FIXTURE_PATH, INDEX_FIXTURE_NAME); +export const readDictFile = () => readAsFile(DICT_FIXTURE_PATH, DICT_FIXTURE_NAME); diff --git a/apps/readest-app/src/__tests__/services/dictionaries/_slobFixtures.ts b/apps/readest-app/src/__tests__/services/dictionaries/_slobFixtures.ts new file mode 100644 index 00000000..d0314310 --- /dev/null +++ b/apps/readest-app/src/__tests__/services/dictionaries/_slobFixtures.ts @@ -0,0 +1,24 @@ +/** + * Shared Slob (Aard 2) test fixture loader. + * + * Points at the real FreeDict English-Dutch slob in + * `src/__tests__/fixtures/data/dicts/`. ~7,700 refs, zlib-compressed bins, + * `text/html;charset=utf-8` content for headwords and `text/css` for the + * bundled stylesheet (`~/css/default.css`). + */ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +const FIXTURES_DIR = path.resolve(__dirname, '../../fixtures/data/dicts'); + +export const SLOB_FIXTURE_PATH = path.join(FIXTURES_DIR, 'eng-nld.slob'); +export const SLOB_FIXTURE_NAME = 'eng-nld.slob'; + +async function readAsFile(filePath: string, name: string): Promise { + const bytes = await readFile(filePath); + const buf = new Uint8Array(bytes.length); + buf.set(bytes); + return new File([buf], name); +} + +export const readSlobFile = () => readAsFile(SLOB_FIXTURE_PATH, SLOB_FIXTURE_NAME); diff --git a/apps/readest-app/src/__tests__/services/dictionaries/dictProvider.test.ts b/apps/readest-app/src/__tests__/services/dictionaries/dictProvider.test.ts new file mode 100644 index 00000000..09f1c89c --- /dev/null +++ b/apps/readest-app/src/__tests__/services/dictionaries/dictProvider.test.ts @@ -0,0 +1,348 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { gzipSync } from 'node:zlib'; + +import { createDictProvider } from '@/services/dictionaries/providers/dictProvider'; +import { decodeDictBase64, parseDictIndex } from '@/services/dictionaries/dictReader'; +import type { ImportedDictionary } from '@/services/dictionaries/types'; +import type { BaseDir } from '@/types/system'; + +import { + INDEX_FIXTURE_NAME, + DICT_FIXTURE_NAME, + readIndexFile, + readDictFile, +} from './_dictFixtures'; +import { makeReadCounter, withReadCounting } from './_countingFs'; + +// --------------------------------------------------------------------------- +// Base64 alphabet decoder unit tests. +// --------------------------------------------------------------------------- + +describe('decodeDictBase64', () => { + it('decodes single-character tokens', () => { + expect(decodeDictBase64('A')).toBe(0); + expect(decodeDictBase64('B')).toBe(1); + expect(decodeDictBase64('Z')).toBe(25); + expect(decodeDictBase64('a')).toBe(26); + expect(decodeDictBase64('z')).toBe(51); + expect(decodeDictBase64('0')).toBe(52); + expect(decodeDictBase64('9')).toBe(61); + expect(decodeDictBase64('+')).toBe(62); + expect(decodeDictBase64('/')).toBe(63); + }); + + it('decodes multi-character big-endian tokens', () => { + // 'BA' = 1·64 + 0 = 64 + expect(decodeDictBase64('BA')).toBe(64); + // 'Bn0B' = 1·64³ + 39·64² + 52·64 + 1 = 262144+159744+3328+1 = 425217 + expect(decodeDictBase64('Bn0B')).toBe(425217); + }); + + it('rejects characters outside the alphabet', () => { + expect(() => decodeDictBase64('A!B')).toThrow(/Invalid DICT base64/); + }); +}); + +// --------------------------------------------------------------------------- +// Synthetic-bundle tests — minimal DICT bundle exercising parser edge cases. +// --------------------------------------------------------------------------- + +const A64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + +function encodeDictBase64(n: number): string { + if (n === 0) return 'A'; + let s = ''; + while (n > 0) { + s = A64[n & 63] + s; + n = Math.floor(n / 64); + } + return s; +} + +interface SynEntry { + word: string; + text: string; +} + +function buildBundle( + entries: SynEntry[], + meta: { short?: string; info?: string; url?: string } = {}, +) { + // Concatenate bodies and remember offsets. + const enc = new TextEncoder(); + const bodyParts: Uint8Array[] = []; + const lines: string[] = []; + let pos = 0; + + const push = (word: string, text: string) => { + const data = enc.encode(text); + lines.push(`${word}\t${encodeDictBase64(pos)}\t${encodeDictBase64(data.length)}`); + bodyParts.push(data); + pos += data.length; + }; + + if (meta.short) push('00databaseshort', meta.short); + if (meta.info) push('00databaseinfo', meta.info); + if (meta.url) push('00databaseurl', meta.url); + push('00databaseutf8', ''); + for (const e of entries) push(e.word, e.text); + + // Concatenate body and gzip it as a (non-DictZip) gzip stream — the + // reader's whole-file gunzip fallback handles that. + const body = new Uint8Array(bodyParts.reduce((n, p) => n + p.length, 0)); + let off = 0; + for (const p of bodyParts) { + body.set(p, off); + off += p.length; + } + const dictDz = new Uint8Array(gzipSync(Buffer.from(body))); + + // .index: deliberately keep the original input order (FreeDict files in + // the wild are mostly sorted by `dictsort`, but the parser must still + // re-sort defensively before binary search). + const indexBytes = enc.encode(lines.join('\n') + '\n'); + + return { index: indexBytes, dictDz }; +} + +describe('parseDictIndex', () => { + it('handles CRLF and trailing whitespace', () => { + const text = ['hello\tA\tB', '00databaseshort\tA\tA', 'world\tB\tA', ''].join('\r\n'); + const parsed = parseDictIndex(text); + expect(parsed.words).toEqual(['hello', 'world']); + expect(parsed.meta['00databaseshort']).toEqual({ offset: 0, size: 0 }); + }); + + it('sorts entries case-insensitively', () => { + const text = ['Banana\tA\tA', 'apple\tB\tA', 'Cherry\tC\tA'].join('\n'); + const parsed = parseDictIndex(text); + expect(parsed.words).toEqual(['apple', 'Banana', 'Cherry']); + }); +}); + +describe('dictProvider — synthetic bundle', () => { + let bundleFiles: { index: Uint8Array; dictDz: Uint8Array }; + + beforeEach(() => { + bundleFiles = buildBundle( + [ + { word: 'apple', text: 'a fruit' }, + { word: 'banana', text: 'another fruit' }, + { word: 'cherry', text: 'a small red fruit' }, + ], + { short: 'Test Dict', info: 'A tiny test dictionary.' }, + ); + }); + + const buildDict = (): ImportedDictionary => ({ + id: 'dict:test', + kind: 'dict', + name: 'Test Dict', + bundleDir: 'test-bundle', + files: { index: 'test.index', dict: 'test.dict.dz' }, + addedAt: 1, + }); + + const makeFs = (files = bundleFiles) => ({ + openFile: async (path: string, _base: BaseDir) => { + const base = path.split('/').pop()!; + let bytes: Uint8Array; + if (base === 'test.index') bytes = files.index; + else if (base === 'test.dict.dz') bytes = files.dictDz; + else throw new Error(`Unknown fixture file: ${base}`); + const buf = new Uint8Array(bytes.length); + buf.set(bytes); + return new File([buf], base); + }, + }); + + it('initializes lazily and looks up a headword', async () => { + const provider = createDictProvider({ dict: buildDict(), fs: makeFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('banana', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(true); + if (outcome.ok) { + expect(outcome.headword).toBe('banana'); + expect(outcome.sourceLabel).toBe('Test Dict'); + } + expect(container.querySelector('h1')?.textContent).toBe('banana'); + expect(container.querySelector('pre')?.textContent).toBe('another fruit'); + }); + + it('case-insensitive lookup', async () => { + const provider = createDictProvider({ dict: buildDict(), fs: makeFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('APPLE', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(true); + if (outcome.ok) expect(outcome.headword).toBe('apple'); + }); + + it('returns empty when the headword does not exist', async () => { + const provider = createDictProvider({ dict: buildDict(), fs: makeFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('zzzz', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) expect(outcome.reason).toBe('empty'); + }); + + it('reports `error` on init failure', async () => { + const failingFs = { + openFile: async () => { + throw new Error('disk gone'); + }, + }; + const provider = createDictProvider({ dict: buildDict(), fs: failingFs }); + const container = document.createElement('div'); + const outcome = await provider.lookup('apple', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) { + expect(outcome.reason).toBe('error'); + expect(outcome.message).toContain('disk gone'); + } + }); + + it('shares the parsed instance across lookups', async () => { + const fs = makeFs(); + let openCount = 0; + const wrappedFs = { + openFile: async (path: string, base: BaseDir) => { + openCount += 1; + return fs.openFile(path, base); + }, + }; + const provider = createDictProvider({ dict: buildDict(), fs: wrappedFs }); + const container = document.createElement('div'); + await provider.lookup('apple', { signal: new AbortController().signal, container }); + const opensAfterFirst = openCount; + container.replaceChildren(); + await provider.lookup('banana', { signal: new AbortController().signal, container }); + expect(openCount).toBe(opensAfterFirst); + }); +}); + +// --------------------------------------------------------------------------- +// Real-fixture tests — the FreeDict English-Dutch bundle. +// --------------------------------------------------------------------------- + +describe('dictProvider — real freedict-eng-nld fixture', () => { + const realDict: ImportedDictionary = { + id: 'dict:freedict-eng-nld', + kind: 'dict', + name: 'FreeDict eng-nld', + bundleDir: 'freedict-eng-nld-bundle', + files: { index: INDEX_FIXTURE_NAME, dict: DICT_FIXTURE_NAME }, + addedAt: 1, + }; + + const makeRealFs = () => ({ + openFile: async (p: string, _base: BaseDir) => { + const base = p.split('/').pop()!; + if (base === INDEX_FIXTURE_NAME) return readIndexFile(); + if (base === DICT_FIXTURE_NAME) return readDictFile(); + throw new Error(`Unknown fixture file: ${base}`); + }, + }); + + it('looks up a real headword and renders a definition', async () => { + const provider = createDictProvider({ dict: realDict, fs: makeRealFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('hello', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(true); + if (outcome.ok) { + expect(outcome.headword).toBe('hello'); + // 00databaseshort exists in this fixture and is used as the source label. + expect(outcome.sourceLabel).toBeTruthy(); + } + expect(container.querySelector('h1')?.textContent).toBe('hello'); + const def = container.querySelector('pre')?.textContent ?? ''; + expect(def.length).toBeGreaterThan(0); + }); + + it('finds several common headwords', async () => { + const provider = createDictProvider({ dict: realDict, fs: makeRealFs() }); + for (const word of ['hello', 'world', 'reveal']) { + const container = document.createElement('div'); + const outcome = await provider.lookup(word, { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(true); + if (outcome.ok) expect(outcome.headword).toBe(word); + expect(container.querySelector('pre')?.textContent?.length ?? 0).toBeGreaterThan(0); + } + }); + + it('returns empty for non-existent words', async () => { + const provider = createDictProvider({ dict: realDict, fs: makeRealFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('zzznonsenseword', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) expect(outcome.reason).toBe('empty'); + }); + + it('caches the reader instance — second lookup opens no extra files', async () => { + const realFs = makeRealFs(); + let opens = 0; + const wrappedFs = { + openFile: async (p: string, base: BaseDir) => { + opens += 1; + return realFs.openFile(p, base); + }, + }; + const provider = createDictProvider({ dict: realDict, fs: wrappedFs }); + const c1 = document.createElement('div'); + await provider.lookup('hello', { signal: new AbortController().signal, container: c1 }); + const opensAfterInit = opens; + expect(opensAfterInit).toBe(2); // .index, .dict.dz + const c2 = document.createElement('div'); + await provider.lookup('world', { signal: new AbortController().signal, container: c2 }); + expect(opens).toBe(opensAfterInit); + }); + + // ------------------------------------------------------------------------- + // Per-lookup .dict.dz reads should be small (lazy chunk decompression). + // ------------------------------------------------------------------------- + it('per-lookup .dict.dz reads are bounded by the chunk size', async () => { + const counter = makeReadCounter(); + const fs = withReadCounting(makeRealFs(), counter); + const provider = createDictProvider({ dict: realDict, fs }); + + // Prime init. + await provider.lookup('hello', { + signal: new AbortController().signal, + container: document.createElement('div'), + }); + + let dictLast = counter.perFile.get(DICT_FIXTURE_NAME) ?? 0; + for (const word of ['world', 'reveal', 'vital']) { + await provider.lookup(word, { + signal: new AbortController().signal, + container: document.createElement('div'), + }); + const dictNow = counter.perFile.get(DICT_FIXTURE_NAME) ?? 0; + const delta = dictNow - dictLast; + // The freedict bundle's dictzip chunks are ~8 KB compressed; bound + // generously at 64 KB to absorb format variance. + expect(delta).toBeLessThanOrEqual(64 * 1024); + dictLast = dictNow; + } + }); +}); diff --git a/apps/readest-app/src/__tests__/services/dictionaries/registry.test.ts b/apps/readest-app/src/__tests__/services/dictionaries/registry.test.ts index 43513c2d..7c89f1ef 100644 --- a/apps/readest-app/src/__tests__/services/dictionaries/registry.test.ts +++ b/apps/readest-app/src/__tests__/services/dictionaries/registry.test.ts @@ -1,7 +1,11 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { getEnabledProviders, __resetRegistryForTests } from '@/services/dictionaries/registry'; -import { BUILTIN_PROVIDER_IDS } from '@/services/dictionaries/types'; -import type { DictionarySettings, ImportedDictionary } from '@/services/dictionaries/types'; +import { BUILTIN_PROVIDER_IDS, BUILTIN_WEB_SEARCH_IDS } from '@/services/dictionaries/types'; +import type { + DictionarySettings, + ImportedDictionary, + WebSearchEntry, +} from '@/services/dictionaries/types'; const baseSettings: DictionarySettings = { providerOrder: [BUILTIN_PROVIDER_IDS.wiktionary, BUILTIN_PROVIDER_IDS.wikipedia], @@ -58,6 +62,56 @@ describe('dictionary registry', () => { expect(a[0]).toBe(b[0]); }); + it('dispatches built-in web-search ids to a web provider', () => { + const settings: DictionarySettings = { + providerOrder: [BUILTIN_WEB_SEARCH_IDS.google, BUILTIN_WEB_SEARCH_IDS.urban], + providerEnabled: { + [BUILTIN_WEB_SEARCH_IDS.google]: true, + [BUILTIN_WEB_SEARCH_IDS.urban]: true, + }, + }; + const providers = getEnabledProviders({ settings, dictionaries: [] }); + expect(providers.map((p) => p.id)).toEqual([ + BUILTIN_WEB_SEARCH_IDS.google, + BUILTIN_WEB_SEARCH_IDS.urban, + ]); + expect(providers.every((p) => p.kind === 'web')).toBe(true); + }); + + it('resolves custom web-search ids from settings.webSearches', () => { + const customEntry: WebSearchEntry = { + id: 'web:custom123', + name: 'My Site', + urlTemplate: 'https://example.com/?q=%WORD%', + }; + const settings: DictionarySettings = { + providerOrder: ['web:custom123'], + providerEnabled: { 'web:custom123': true }, + webSearches: [customEntry], + }; + const providers = getEnabledProviders({ settings, dictionaries: [] }); + expect(providers).toHaveLength(1); + expect(providers[0]!.label).toBe('My Site'); + expect(providers[0]!.kind).toBe('web'); + }); + + it('drops custom web-search ids whose entries are missing or soft-deleted', () => { + const settings: DictionarySettings = { + providerOrder: ['web:gone', 'web:dead'], + providerEnabled: { 'web:gone': true, 'web:dead': true }, + webSearches: [ + { + id: 'web:dead', + name: 'Dead', + urlTemplate: 'https://example.com/?q=%WORD%', + deletedAt: 1, + }, + ], + }; + const providers = getEnabledProviders({ settings, dictionaries: [] }); + expect(providers).toEqual([]); + }); + it('skips imported dictionaries that are unavailable, deleted, or unsupported', () => { const fs = { openFile: async () => new File([], '') }; const dicts: ImportedDictionary[] = [ diff --git a/apps/readest-app/src/__tests__/services/dictionaries/slobProvider.test.ts b/apps/readest-app/src/__tests__/services/dictionaries/slobProvider.test.ts new file mode 100644 index 00000000..ee108356 --- /dev/null +++ b/apps/readest-app/src/__tests__/services/dictionaries/slobProvider.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect } from 'vitest'; + +import { createSlobProvider } from '@/services/dictionaries/providers/slobProvider'; +import { SlobReader, cleanTag } from '@/services/dictionaries/slobReader'; +import type { ImportedDictionary } from '@/services/dictionaries/types'; +import type { BaseDir } from '@/types/system'; + +import { SLOB_FIXTURE_NAME, readSlobFile } from './_slobFixtures'; +import { makeReadCounter, withReadCounting } from './_countingFs'; + +// --------------------------------------------------------------------------- +// Real-fixture tests — the FreeDict English-Dutch slob (zlib-compressed). +// --------------------------------------------------------------------------- + +describe('slobReader — real eng-nld.slob fixture', () => { + it('parses the header and exposes metadata', async () => { + const file = await readSlobFile(); + const reader = new SlobReader(); + await reader.load({ slob: file }); + expect(reader.header.encoding).toBe('utf-8'); + expect(reader.header.compression).toBe('zlib'); + // FreeDict bundles ship 7,717 refs (3 resource refs + 7,714 headwords). + expect(reader.refCount).toBe(7717); + expect(reader.header.contentTypes).toContain('text/html;charset=utf-8'); + expect(reader.header.contentTypes).toContain('text/css'); + expect(cleanTag(reader.header.tags['label'] ?? '')).toBe('English-Dutch FreeDict Dictionary'); + }); + + it('finds a known headword case-insensitively', async () => { + const file = await readSlobFile(); + const reader = new SlobReader(); + await reader.load({ slob: file }); + const ref = await reader.findRef('hello'); + expect(ref).toBeDefined(); + if (ref) { + expect(ref.key).toBe('hello'); + expect(ref.binIndex).toBeGreaterThanOrEqual(0); + } + const refUpper = await reader.findRef('HELLO'); + expect(refUpper?.key).toBe('hello'); + }); + + it('decompresses a bin and reads the html blob', async () => { + const file = await readSlobFile(); + const reader = new SlobReader(); + await reader.load({ slob: file }); + const ref = await reader.findRef('hello'); + expect(ref).toBeDefined(); + if (!ref) return; + const blob = await reader.readBlob(ref); + expect(blob.contentType.startsWith('text/html')).toBe(true); + const html = new TextDecoder('utf-8').decode(blob.data); + expect(html.length).toBeGreaterThan(0); + // FreeDict slob entries are wrapped in HTML; just sanity-check there's tags. + expect(html).toMatch(/<\/?\w+/); + }); + + it('returns undefined for non-existent words', async () => { + const file = await readSlobFile(); + const reader = new SlobReader(); + await reader.load({ slob: file }); + expect(await reader.findRef('zzznonsenseword')).toBeUndefined(); + }); +}); + +describe('slobReader — error handling', () => { + it('rejects a wrong-magic file', async () => { + const file = new File([new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0, 0, 0, 0])], 'bad.slob'); + const reader = new SlobReader(); + await expect(reader.load({ slob: file })).rejects.toThrow(/bad magic/); + }); +}); + +// --------------------------------------------------------------------------- +// Provider-level tests — full popup flow. +// --------------------------------------------------------------------------- + +describe('slobProvider — real eng-nld.slob fixture', () => { + const realDict: ImportedDictionary = { + id: 'slob:eng-nld', + kind: 'slob', + name: 'English-Dutch FreeDict Dictionary', + bundleDir: 'eng-nld-slob-bundle', + files: { slob: SLOB_FIXTURE_NAME }, + addedAt: 1, + }; + + const makeRealFs = () => ({ + openFile: async (p: string, _base: BaseDir) => { + const base = p.split('/').pop()!; + if (base === SLOB_FIXTURE_NAME) return readSlobFile(); + throw new Error(`Unknown fixture file: ${base}`); + }, + }); + + it('looks up a real headword and renders HTML', async () => { + const provider = createSlobProvider({ dict: realDict, fs: makeRealFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('hello', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(true); + if (outcome.ok) { + expect(outcome.headword).toBe('hello'); + expect(outcome.sourceLabel).toBe('English-Dutch FreeDict Dictionary'); + } + expect(container.querySelector('h1')?.textContent).toBe('hello'); + // Body div must contain at least one element (the HTML body of the entry). + const body = container.querySelector('div'); + expect(body).toBeTruthy(); + expect((body?.innerHTML ?? '').length).toBeGreaterThan(0); + }); + + it('returns empty for missing headwords', async () => { + const provider = createSlobProvider({ dict: realDict, fs: makeRealFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('zzznotaword', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) expect(outcome.reason).toBe('empty'); + }); + + it('hides bundled resource keys (~/...)', async () => { + const provider = createSlobProvider({ dict: realDict, fs: makeRealFs() }); + const container = document.createElement('div'); + const outcome = await provider.lookup('~/css/default.css', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) expect(outcome.reason).toBe('empty'); + }); + + it('caches the reader — second lookup opens no extra files', async () => { + const realFs = makeRealFs(); + let opens = 0; + const wrappedFs = { + openFile: async (p: string, base: BaseDir) => { + opens += 1; + return realFs.openFile(p, base); + }, + }; + const provider = createSlobProvider({ dict: realDict, fs: wrappedFs }); + await provider.lookup('hello', { + signal: new AbortController().signal, + container: document.createElement('div'), + }); + const opensAfterInit = opens; + expect(opensAfterInit).toBe(1); + await provider.lookup('world', { + signal: new AbortController().signal, + container: document.createElement('div'), + }); + expect(opens).toBe(opensAfterInit); + }); + + it('per-lookup reads are bounded (lazy bin decompression)', async () => { + const counter = makeReadCounter(); + const fs = withReadCounting(makeRealFs(), counter); + const provider = createSlobProvider({ dict: realDict, fs }); + + // Prime init + one lookup. + await provider.lookup('hello', { + signal: new AbortController().signal, + container: document.createElement('div'), + }); + + const sizes: number[] = []; + let last = counter.perFile.get(SLOB_FIXTURE_NAME) ?? 0; + for (const word of ['world', 'cat', 'apple', 'about']) { + await provider.lookup(word, { + signal: new AbortController().signal, + container: document.createElement('div'), + }); + const now = counter.perFile.get(SLOB_FIXTURE_NAME) ?? 0; + sizes.push(now - last); + last = now; + } + // Each subsequent lookup should be tiny — log2(7717)≈13 ref probes, + // each ≤4 KB, plus at most one new bin decompression. Bound at 256 KB. + for (const delta of sizes) expect(delta).toBeLessThanOrEqual(256 * 1024); + }); +}); diff --git a/apps/readest-app/src/__tests__/services/dictionaries/webSearchProvider.test.ts b/apps/readest-app/src/__tests__/services/dictionaries/webSearchProvider.test.ts new file mode 100644 index 00000000..22da8298 --- /dev/null +++ b/apps/readest-app/src/__tests__/services/dictionaries/webSearchProvider.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect } from 'vitest'; + +import { createWebSearchProvider } from '@/services/dictionaries/providers/webSearchProvider'; +import { + BUILTIN_WEB_SEARCHES, + getBuiltinWebSearch, + isValidUrlTemplate, + substituteUrlTemplate, +} from '@/services/dictionaries/webSearchTemplates'; +import type { WebSearchEntry } from '@/services/dictionaries/types'; + +describe('substituteUrlTemplate', () => { + it('replaces %WORD% (case-insensitive) with the URL-encoded word', () => { + expect(substituteUrlTemplate('https://x.com/?q=%WORD%', 'hello')).toBe( + 'https://x.com/?q=hello', + ); + expect(substituteUrlTemplate('https://x.com/?q=%word%', 'hello')).toBe( + 'https://x.com/?q=hello', + ); + }); + + it('URL-encodes spaces and special characters', () => { + expect(substituteUrlTemplate('https://x.com/?q=%WORD%', 'hello world')).toBe( + 'https://x.com/?q=hello%20world', + ); + expect(substituteUrlTemplate('https://x.com/?q=%WORD%', 'a&b?c')).toBe( + 'https://x.com/?q=a%26b%3Fc', + ); + }); + + it('handles double-encoded %25WORD%25', () => { + expect(substituteUrlTemplate('https://x.com/?q=%25WORD%25', 'hi')).toBe('https://x.com/?q=hi'); + }); + + it('replaces every occurrence', () => { + expect(substituteUrlTemplate('a/%WORD%/b/%WORD%', 'X')).toBe('a/X/b/X'); + }); +}); + +describe('isValidUrlTemplate', () => { + it('requires http(s):// + a placeholder', () => { + expect(isValidUrlTemplate('https://x.com/?q=%WORD%')).toBe(true); + expect(isValidUrlTemplate(' http://x.com/?q=%WORD% ')).toBe(true); + expect(isValidUrlTemplate('https://x.com/?q=%25WORD%25')).toBe(true); + }); + + it('rejects missing scheme or placeholder', () => { + expect(isValidUrlTemplate('https://x.com/?q=hi')).toBe(false); + expect(isValidUrlTemplate('//x.com/?q=%WORD%')).toBe(false); + expect(isValidUrlTemplate('ftp://x.com/?q=%WORD%')).toBe(false); + expect(isValidUrlTemplate('')).toBe(false); + }); +}); + +describe('getBuiltinWebSearch', () => { + it('returns each of the three built-ins by id', () => { + for (const tpl of BUILTIN_WEB_SEARCHES) { + const found = getBuiltinWebSearch(tpl.id); + expect(found).toBeDefined(); + expect(found?.urlTemplate).toContain('%WORD%'); + } + }); + + it('returns undefined for unknown ids', () => { + expect(getBuiltinWebSearch('web:builtin:nope')).toBeUndefined(); + expect(getBuiltinWebSearch('web:custom:abc')).toBeUndefined(); + }); +}); + +describe('webSearchProvider', () => { + const customEntry: WebSearchEntry = { + id: 'web:abc123', + name: 'Custom Site', + urlTemplate: 'https://example.com/?q=%WORD%', + }; + + it('renders headword + sub-label + open button + URL preview', async () => { + const provider = createWebSearchProvider({ template: customEntry }); + const container = document.createElement('div'); + const outcome = await provider.lookup('hello world', { + signal: new AbortController().signal, + container, + }); + expect(outcome.ok).toBe(true); + if (outcome.ok) { + expect(outcome.headword).toBe('hello world'); + expect(outcome.sourceLabel).toBe('Custom Site'); + } + expect(container.querySelector('h1')?.textContent).toBe('hello world'); + const link = container.querySelector('a'); + expect(link).toBeTruthy(); + expect(link?.getAttribute('href')).toBe('https://example.com/?q=hello%20world'); + expect(link?.getAttribute('rel')).toBe('noopener noreferrer'); + // URL preview rendered for transparency. + expect(container.textContent).toContain('https://example.com/?q=hello%20world'); + }); + + it('returns empty for a blank word', async () => { + const provider = createWebSearchProvider({ template: customEntry }); + const outcome = await provider.lookup(' ', { + signal: new AbortController().signal, + container: document.createElement('div'), + }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) expect(outcome.reason).toBe('empty'); + }); + + it('respects abort signals', async () => { + const provider = createWebSearchProvider({ template: customEntry }); + const ac = new AbortController(); + ac.abort(); + const outcome = await provider.lookup('hello', { + signal: ac.signal, + container: document.createElement('div'), + }); + expect(outcome.ok).toBe(false); + if (!outcome.ok) expect(outcome.reason).toBe('error'); + }); + + it('uses a label override when provided', () => { + const provider = createWebSearchProvider({ + template: customEntry, + label: 'Override', + }); + expect(provider.label).toBe('Override'); + expect(provider.kind).toBe('web'); + }); + + it('opens each built-in URL with the headword substituted', async () => { + for (const tpl of BUILTIN_WEB_SEARCHES) { + const provider = createWebSearchProvider({ template: tpl }); + const container = document.createElement('div'); + await provider.lookup('hello', { + signal: new AbortController().signal, + container, + }); + const link = container.querySelector('a'); + expect(link).toBeTruthy(); + const href = link?.getAttribute('href') ?? ''; + expect(href).toContain('hello'); + expect(href.startsWith('https://')).toBe(true); + expect(href).not.toContain('%WORD%'); + } + }); +}); diff --git a/apps/readest-app/src/__tests__/store/custom-dictionary-store.test.ts b/apps/readest-app/src/__tests__/store/custom-dictionary-store.test.ts new file mode 100644 index 00000000..d6c05029 --- /dev/null +++ b/apps/readest-app/src/__tests__/store/custom-dictionary-store.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useCustomDictionaryStore } from '@/store/customDictionaryStore'; +import { BUILTIN_WEB_SEARCH_IDS } from '@/services/dictionaries/types'; + +const ZERO = (s: string) => s.startsWith('web:builtin:'); + +describe('customDictionaryStore — web search CRUD', () => { + beforeEach(() => { + // Reset state to defaults so tests don't bleed. + useCustomDictionaryStore.setState({ + dictionaries: [], + settings: { + providerOrder: [ + BUILTIN_WEB_SEARCH_IDS.google, + BUILTIN_WEB_SEARCH_IDS.urban, + BUILTIN_WEB_SEARCH_IDS.merriamWebster, + ], + providerEnabled: { + [BUILTIN_WEB_SEARCH_IDS.google]: false, + [BUILTIN_WEB_SEARCH_IDS.urban]: false, + [BUILTIN_WEB_SEARCH_IDS.merriamWebster]: false, + }, + webSearches: [], + }, + }); + }); + + it('seeds the three built-in web ids in default order, all disabled', () => { + const { settings } = useCustomDictionaryStore.getState(); + const builtinWeb = settings.providerOrder.filter(ZERO); + expect(builtinWeb).toEqual([ + BUILTIN_WEB_SEARCH_IDS.google, + BUILTIN_WEB_SEARCH_IDS.urban, + BUILTIN_WEB_SEARCH_IDS.merriamWebster, + ]); + for (const id of builtinWeb) { + expect(settings.providerEnabled[id]).toBe(false); + } + }); + + it('addWebSearch appends to order, enables, returns the entry', () => { + const { addWebSearch } = useCustomDictionaryStore.getState(); + const entry = addWebSearch('My Site', 'https://example.com/?q=%WORD%'); + expect(entry.id.startsWith('web:')).toBe(true); + expect(entry.id.startsWith('web:builtin:')).toBe(false); + expect(entry.name).toBe('My Site'); + expect(entry.urlTemplate).toBe('https://example.com/?q=%WORD%'); + + const after = useCustomDictionaryStore.getState().settings; + expect(after.providerOrder.includes(entry.id)).toBe(true); + expect(after.providerEnabled[entry.id]).toBe(true); + expect((after.webSearches ?? []).map((w) => w.id)).toEqual([entry.id]); + }); + + it('addWebSearch trims whitespace from name and URL', () => { + const { addWebSearch } = useCustomDictionaryStore.getState(); + const entry = addWebSearch(' Spaced Name ', ' https://x.com/?q=%WORD% '); + expect(entry.name).toBe('Spaced Name'); + expect(entry.urlTemplate).toBe('https://x.com/?q=%WORD%'); + }); + + it('updateWebSearch updates name + URL of a custom entry', () => { + const { addWebSearch, updateWebSearch } = useCustomDictionaryStore.getState(); + const entry = addWebSearch('Old', 'https://old.com/?q=%WORD%'); + updateWebSearch(entry.id, { name: 'New', urlTemplate: 'https://new.com/?q=%WORD%' }); + const list = useCustomDictionaryStore.getState().settings.webSearches ?? []; + const updated = list.find((w) => w.id === entry.id); + expect(updated?.name).toBe('New'); + expect(updated?.urlTemplate).toBe('https://new.com/?q=%WORD%'); + }); + + it('updateWebSearch is a no-op for built-in ids', () => { + const { updateWebSearch, settings } = useCustomDictionaryStore.getState(); + updateWebSearch(BUILTIN_WEB_SEARCH_IDS.google, { name: 'Hijacked' }); + // No `webSearches` entry was added or modified. + const after = useCustomDictionaryStore.getState().settings; + expect(after.webSearches).toEqual(settings.webSearches ?? []); + }); + + it('removeWebSearch soft-deletes a custom entry and removes it from order/enabled', () => { + const { addWebSearch, removeWebSearch } = useCustomDictionaryStore.getState(); + const entry = addWebSearch('Tmp', 'https://tmp.com/?q=%WORD%'); + expect(removeWebSearch(entry.id)).toBe(true); + const after = useCustomDictionaryStore.getState().settings; + expect(after.providerOrder.includes(entry.id)).toBe(false); + expect(entry.id in after.providerEnabled).toBe(false); + const found = (after.webSearches ?? []).find((w) => w.id === entry.id); + expect(found?.deletedAt).toBeGreaterThan(0); + }); + + it('removeWebSearch refuses built-in ids', () => { + const { removeWebSearch } = useCustomDictionaryStore.getState(); + expect(removeWebSearch(BUILTIN_WEB_SEARCH_IDS.google)).toBe(false); + const after = useCustomDictionaryStore.getState().settings; + expect(after.providerOrder.includes(BUILTIN_WEB_SEARCH_IDS.google)).toBe(true); + }); +}); diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index 33d40bfb..96c15c8a 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -51,7 +51,8 @@ import ExportMarkdownDialog from './ExportMarkdownDialog'; const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); - const { settings } = useSettingsStore(); + const { settings, setSettingsDialogBookKey, setSettingsDialogOpen, setActiveSettingsItemId } = + useSettingsStore(); const { isDarkMode } = useThemeStore(); const { getConfig, saveConfig, getBookData, updateBooknotes } = useBookDataStore(); const { getProgress, getView, getViewsById, getViewSettings } = useReaderStore(); @@ -970,6 +971,15 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { popupWidth={dictPopupWidth} popupHeight={dictPopupHeight} onDismiss={handleDismissPopupAndSelection} + onManage={() => { + // Dismiss the popup so the user returns to the reader cleanly + // when they close settings; the dictionaries sub-page in the + // SettingsDialog is enough surface for managing providers. + handleDismissPopupAndSelection(); + setSettingsDialogBookKey(bookKey); + setActiveSettingsItemId('settings.language.dictionaries.manage'); + setSettingsDialogOpen(true); + }} /> )} {showDeepLPopup && trianglePosition && translatorPopupPosition && ( diff --git a/apps/readest-app/src/app/reader/components/annotator/DictionaryPopup.tsx b/apps/readest-app/src/app/reader/components/annotator/DictionaryPopup.tsx index add9f918..97e6b4ce 100644 --- a/apps/readest-app/src/app/reader/components/annotator/DictionaryPopup.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/DictionaryPopup.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { MdArrowBack } from 'react-icons/md'; +import { MdArrowBack, MdSettings } from 'react-icons/md'; import clsx from 'clsx'; import { openUrl } from '@tauri-apps/plugin-opener'; @@ -22,6 +22,13 @@ interface DictionaryPopupProps { popupWidth: number; popupHeight: number; onDismiss?: () => void; + /** + * Invoked when the user clicks the bottom-right "Manage Dictionaries" + * icon. The host (Annotator) decides how to navigate — typically by + * opening the SettingsDialog and deep-linking to the dictionaries + * sub-page. + */ + onManage?: () => void; } interface TabState { @@ -45,6 +52,7 @@ const DictionaryPopup: React.FC = ({ popupWidth, popupHeight, onDismiss, + onManage, }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); @@ -294,7 +302,11 @@ const DictionaryPopup: React.FC = ({ className='select-text' onDismiss={onDismiss} > -
+ {/* `overflow-hidden rounded-lg` clips child surfaces (the tab strip's + gray bg, the bottom footer divider, etc.) to the popup's rounded + shape — without it the tab bar's `bg-base-300/40` and `border-b` + paint into the corner area and break the rounded edge. */} +
{providers.length > 1 && (
= ({ }) )} - {sourceLabel && ( -
- {/* min-w-0 lets the truncate engage inside the flex parent — - without it the span sizes to its content and overflows. */} - Source: {sourceLabel} + {/* Footer always renders so the manage-dictionaries icon has a + consistent home at the bottom-right of every tab. The source + label fills the left side when present; otherwise the spacer + pushes the icon to the right. */} + {(sourceLabel || onManage) && ( +
+ {sourceLabel ? ( + + Source: {sourceLabel} + + ) : ( + + )} + {onManage && ( + + )}
)}
diff --git a/apps/readest-app/src/components/settings/CustomDictionaries.tsx b/apps/readest-app/src/components/settings/CustomDictionaries.tsx index f3d80e09..29cb18a0 100644 --- a/apps/readest-app/src/components/settings/CustomDictionaries.tsx +++ b/apps/readest-app/src/components/settings/CustomDictionaries.tsx @@ -27,7 +27,11 @@ import { useCustomDictionaryStore } from '@/store/customDictionaryStore'; import { eventDispatcher } from '@/utils/event'; import { evictProvider } from '@/services/dictionaries/registry'; import { BUILTIN_PROVIDER_IDS } from '@/services/dictionaries/types'; -import type { ImportedDictionary } from '@/services/dictionaries/types'; +import type { ImportedDictionary, WebSearchEntry } from '@/services/dictionaries/types'; +import { + getBuiltinWebSearch, + isValidUrlTemplate, +} from '@/services/dictionaries/webSearchTemplates'; interface CustomDictionariesProps { onBack: () => void; @@ -36,13 +40,23 @@ interface CustomDictionariesProps { interface ProviderRow { id: string; label: string; - kind: 'builtin' | 'stardict' | 'mdict'; + kind: 'builtin' | 'stardict' | 'mdict' | 'dict' | 'slob' | 'web'; badge: string; imported?: ImportedDictionary; + /** Set on `kind: 'web'` rows. The shape distinguishes deletable custom + * entries (when `builtinWeb` is false) from immutable built-ins. */ + webSearch?: WebSearchEntry; + builtinWeb?: boolean; disabled?: boolean; reason?: string; } +const builtinWebLabel = (id: string, _: (key: string) => string): string => { + const tpl = getBuiltinWebSearch(id); + if (!tpl) return id; + return _(tpl.nameKey); +}; + const builtinLabel = (id: string, _: (key: string) => string): string => { if (id === BUILTIN_PROVIDER_IDS.wiktionary) return _('Wiktionary'); if (id === BUILTIN_PROVIDER_IDS.wikipedia) return _('Wikipedia'); @@ -55,6 +69,7 @@ interface SortableRowProps { isDeleteMode: boolean; onToggle: (id: string, next: boolean) => void; onDelete: (row: ProviderRow) => void; + onEditWebSearch?: (entry: WebSearchEntry) => void; _: (key: string, options?: Record) => string; } @@ -64,6 +79,7 @@ const SortableRow: React.FC = ({ isDeleteMode, onToggle, onDelete, + onEditWebSearch, _, }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ @@ -83,8 +99,8 @@ const SortableRow: React.FC = ({ ref={setNodeRef} style={style} className={clsx( - 'flex items-center gap-2 px-3 py-2', - isDragging && 'bg-base-200 z-10 shadow-md', + 'flex items-center gap-2 px-3 py-2 transition-colors', + isDragging ? 'bg-base-200 z-10 shadow-md' : 'hover:bg-base-200/40', )} > {/* Drag handle (left). Always present so reorder works for built-ins @@ -110,7 +126,15 @@ const SortableRow: React.FC = ({ > {row.label} - {row.badge} + {row.kind === 'web' && !row.builtinWeb && row.webSearch && onEditWebSearch && ( + + )}
{row.reason && (
@@ -120,6 +144,11 @@ const SortableRow: React.FC = ({ )}
+ {/* End-aligned type badge. Sits just before the toggle so all + badges form a uniform column regardless of name length, instead + of trailing the truncated name at a ragged x position. */} + {row.badge} + = ({ aria-label={enabled ? _('Disable') : _('Enable')} /> - {/* Delete X — only for imported rows, only in delete mode. Built-ins - never show it; imported rows reserve no width when not in delete - mode so their toggle aligns with the built-ins'. */} - {row.imported && isDeleteMode && ( + {/* Delete X — for imported dictionaries and custom web searches, only + in delete mode. Built-ins (incl. built-in web searches) never show + it; deletable rows reserve no width when not in delete mode so the + toggles align across the list. */} + {(row.imported || (row.kind === 'web' && !row.builtinWeb)) && isDeleteMode && (
- {hasImported && ( + {hasDeletable && ( + -
+
{rows.length === 0 && (
@@ -368,6 +486,7 @@ const CustomDictionaries: React.FC = ({ onBack }) => { isDeleteMode={isDeleteMode} onToggle={handleToggle} onDelete={handleDelete} + onEditWebSearch={openEditWebSearch} _={_} /> ))} @@ -376,17 +495,76 @@ const CustomDictionaries: React.FC = ({ onBack }) => {
-
-
-
{_('Tips')}:
-
    -
  • {_('Drag the handle on the left to reorder.')}
  • +
    +
    +
    + + {_('Tips')} +
    +
    • {_('StarDict bundles need .ifo, .idx, and .dict.dz files (.syn optional).')}
    • {_('MDict bundles use .mdx files; companion .mdd files are optional.')}
    • +
    • {_('DICT bundles need a .index file and a .dict.dz file.')}
    • +
    • {_('Slob bundles need a .slob file.')}
    • {_('Select all the bundle files together when importing.')}
    + + {/* Add / edit web-search modal. Lightweight inline `` (daisyUI + modal classes); the heavier `Dialog.tsx` is overkill for a 2-field + form. */} + {webModal && ( +
    +
    +

    + {webModal.editingId ? _('Edit Web Search') : _('Add Web Search')} +

    +
    + + +
    +
    + + +
    +
    + {/* Click-outside dismiss. */} +
    + )}
); }; diff --git a/apps/readest-app/src/components/settings/LangPanel.tsx b/apps/readest-app/src/components/settings/LangPanel.tsx index 757189e8..cc0411d6 100644 --- a/apps/readest-app/src/components/settings/LangPanel.tsx +++ b/apps/readest-app/src/components/settings/LangPanel.tsx @@ -25,7 +25,8 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset const _ = useTranslation(); const { token } = useAuth(); const { envConfig } = useEnv(); - const { settings, applyUILanguage } = useSettingsStore(); + const { settings, applyUILanguage, activeSettingsItemId, setActiveSettingsItemId } = + useSettingsStore(); const { getView, getViewSettings, setViewSettings, recreateViewer } = useReaderStore(); const view = getView(bookKey); const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings; @@ -44,6 +45,18 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset ); const [showCustomDictionaries, setShowCustomDictionaries] = useState(false); + // Deep-link: callers (e.g. the dictionary popup's manage icon) can set + // activeSettingsItemId to `'settings.language.dictionaries.manage'` to + // jump straight into the Manage Dictionaries sub-page on open. Clear the + // id once consumed so SettingsDialog's scroll-to-element fallback + // (which runs on a 100ms timeout) doesn't re-fire. + useEffect(() => { + if (activeSettingsItemId === 'settings.language.dictionaries.manage') { + setShowCustomDictionaries(true); + setActiveSettingsItemId(null); + } + }, [activeSettingsItemId, setActiveSettingsItemId]); + const resetToDefaults = useResetViewSettings(); const handleReset = () => { diff --git a/apps/readest-app/src/hooks/useFileSelector.ts b/apps/readest-app/src/hooks/useFileSelector.ts index dd77a8e1..0f6332e0 100644 --- a/apps/readest-app/src/hooks/useFileSelector.ts +++ b/apps/readest-app/src/hooks/useFileSelector.ts @@ -140,8 +140,8 @@ export const FILE_SELECTION_PRESETS = { dialogTitle: _('Select Fonts'), }, dictionaries: { - accept: '.mdx, .mdd, .ifo, .idx, .dict, .dz, .syn', - extensions: ['mdx', 'mdd', 'ifo', 'idx', 'dict', 'dz', 'syn'], + accept: '.mdx, .mdd, .ifo, .idx, .dict, .dz, .syn, .index, .slob', + extensions: ['mdx', 'mdd', 'ifo', 'idx', 'dict', 'dz', 'syn', 'index', 'slob'], dialogTitle: _('Select Dictionary Files'), }, covers: { diff --git a/apps/readest-app/src/services/dictionaries/dictReader.ts b/apps/readest-app/src/services/dictionaries/dictReader.ts new file mode 100644 index 00000000..47354b17 --- /dev/null +++ b/apps/readest-app/src/services/dictionaries/dictReader.ts @@ -0,0 +1,261 @@ +/** + * DICT (dictd) reader. Reference: RFC 2229 § 3.1 (dictzip body), the + * `dictd` source `dictd.h`/`index.c` for the `.index` grammar. + * + * A DICT bundle is two files sharing a stem: + * - `name.index` : plain ASCII, one entry per line: + * `headword\tbase64(offset)\tbase64(length)`. + * The offsets are big-endian 6-bit groups encoded with + * the alphabet `[A-Za-z0-9+/]` — *not* standard base64 + * (no padding, variable-length). + * - `name.dict` : raw or `.dict.dz` dictzip-compressed body. Random- + * access reads at the offsets from the index. + * + * Index entries with words starting `00database` are reserved metadata: + * + * 00databasealphabet : 8.dictsort alphabet + * 00databasedictfmt1121 : format marker + * 00databaseinfo : long-form description + * 00databaseshort : one-line label + * 00databaseurl : source URL + * 00databaseutf8 : present iff bodies are UTF-8 + * + * The reader exposes: + * - `info`: parsed metadata including the human-readable label. + * - `entryCount`: number of non-metadata entries. + * - `lookup(word)`: case-insensitive exact match → `Uint8Array` body. + * - `read(entry)`: random-access read of the dict body. + * + * Like `StarDictReader`, it does index parsing once at init, then per- + * lookup it does a binary search over an `Int32Array` of line offsets and + * reads ~one line plus one chunk from the dict body. + */ +import { LRU, loadDictBody, type DictBody } from './dictZip'; + +const decoder = new TextDecoder('utf-8'); + +export interface DictEntry { + word: string; + offset: number; + size: number; +} + +/** RFC 2229 base64-like alphabet (A-Za-z0-9+/), big-endian 6-bit groups. */ +const B64: Int8Array = (() => { + const t = new Int8Array(256).fill(-1); + const set = (s: string, base: number) => { + for (let i = 0; i < s.length; i++) t[s.charCodeAt(i)] = base + i; + }; + set('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 0); + set('abcdefghijklmnopqrstuvwxyz', 26); + set('0123456789', 52); + t['+'.charCodeAt(0)] = 62; + t['/'.charCodeAt(0)] = 63; + return t; +})(); + +/** Decode a DICT base64 token (no padding, variable length) into a number. */ +export function decodeDictBase64(s: string): number { + let n = 0; + for (let i = 0; i < s.length; i++) { + const v = B64[s.charCodeAt(i)]!; + if (v < 0) throw new Error(`Invalid DICT base64 character at ${i}: ${s[i]}`); + n = n * 64 + v; + } + return n; +} + +export interface DictInfo { + /** Human-readable label, derived from `00databaseshort` if present. */ + label?: string; + /** Long-form description from `00databaseinfo`. */ + description?: string; + /** Source URL from `00databaseurl`. */ + url?: string; + /** dictd alphabet hint from `00databasealphabet`. Rarely useful. */ + alphabet?: string; + /** True iff the dict is UTF-8 (presence of `00databaseutf8`). */ + utf8: boolean; +} + +const cmpAscii = (a: string, b: string): number => { + const x = a.toLowerCase(); + const y = b.toLowerCase(); + return x < y ? -1 : x > y ? 1 : 0; +}; + +/** + * Parse a `.index` text into: + * - `entries`: `Int32Array` of `[offset, size]` pairs (2 ints per entry). + * - `words`: `string[]` aligned with `entries`, headwords lowercased so + * binary search is straightforward. + * - `meta`: a record of `00database*` headwords → body slice (offset,size) + * so callers can read out the description / short label after the body + * is open. + */ +export interface ParsedDictIndex { + /** Sorted (case-insensitive) headwords (original case preserved). */ + words: string[]; + /** Per-entry [offset, size] pairs, two ints per entry. */ + entries: Int32Array; + /** `00database*` metadata entries by name, holding their body slice. */ + meta: Record; +} + +export function parseDictIndex(text: string): ParsedDictIndex { + const words: string[] = []; + const offsets: number[] = []; + const sizes: number[] = []; + const meta: Record = {}; + + let line = 0; + let pos = 0; + while (pos < text.length) { + let nl = text.indexOf('\n', pos); + if (nl < 0) nl = text.length; + const ln = + text.charCodeAt(nl - 1) === 13 /* \r */ ? text.slice(pos, nl - 1) : text.slice(pos, nl); + pos = nl + 1; + if (!ln) continue; + line++; + + const tab1 = ln.indexOf('\t'); + if (tab1 < 0) continue; + const tab2 = ln.indexOf('\t', tab1 + 1); + if (tab2 < 0) continue; + const word = ln.slice(0, tab1); + const offTok = ln.slice(tab1 + 1, tab2); + const sizeTok = ln.slice(tab2 + 1).trim(); + let off: number; + let size: number; + try { + off = decodeDictBase64(offTok); + size = decodeDictBase64(sizeTok); + } catch { + continue; + } + + if (word.startsWith('00database')) { + meta[word] = { offset: off, size }; + continue; + } + words.push(word); + offsets.push(off); + sizes.push(size); + } + + // The dictd convention is that .index is sorted by `dictsort` (mostly + // case-insensitive ASCII). We re-sort here defensively so binary search + // is correct regardless of the producer. + const order = words.map((_, i) => i).sort((a, b) => cmpAscii(words[a]!, words[b]!)); + const sortedWords: string[] = new Array(words.length); + const sortedEntries = new Int32Array(words.length * 2); + for (let i = 0; i < order.length; i++) { + const k = order[i]!; + sortedWords[i] = words[k]!; + sortedEntries[i * 2] = offsets[k]!; + sortedEntries[i * 2 + 1] = sizes[k]!; + } + + void line; + return { words: sortedWords, entries: sortedEntries, meta }; +} + +export interface DictReaderOpts { + index: Blob; + /** Either `.dict.dz` (gzip) or a raw `.dict` (no compression). */ + dict: Blob; + /** LRU cache size for decoded bodies. Defaults to 256. */ + cacheSize?: number; +} + +export class DictReader { + info: DictInfo = { utf8: false }; + private words: string[] = []; + private entries: Int32Array = new Int32Array(0); + private body: DictBody | null = null; + private bodyCache: LRU; + + constructor(cacheSize = 256) { + this.bodyCache = new LRU(cacheSize); + } + + async load(opts: DictReaderOpts): Promise { + if (opts.cacheSize !== undefined) { + this.bodyCache = new LRU(opts.cacheSize); + } + const cacheSize = opts.cacheSize ?? 256; + + const indexText = await opts.index.text(); + const parsed = parseDictIndex(indexText); + this.words = parsed.words; + this.entries = parsed.entries; + + this.body = await loadDictBody(opts.dict, { + chunkCacheSize: Math.max(8, Math.floor(cacheSize / 16)), + }); + + // Resolve metadata bodies (if present) against the now-open dict. + const tryRead = async (key: string): Promise => { + const m = parsed.meta[key]; + if (!m || !this.body) return undefined; + const bytes = await this.body.read(m.offset, m.size); + return decoder.decode(bytes).trim(); + }; + const [short, longInfo, url, alphabet, utf8] = await Promise.all([ + tryRead('00databaseshort'), + tryRead('00databaseinfo'), + tryRead('00databaseurl'), + tryRead('00databasealphabet'), + tryRead('00databaseutf8'), + ]); + this.info = { + label: short, + description: longInfo, + url, + alphabet, + utf8: utf8 !== undefined, + }; + } + + get entryCount(): number { + return this.words.length; + } + + /** Random-access read of `size` bytes from the dict body at `offset`. */ + async read(entry: DictEntry): Promise { + if (!this.body) throw new Error('dict body not loaded'); + return this.body.read(entry.offset, entry.size); + } + + /** Look up a headword (case-insensitive exact match). */ + async lookup(word: string): Promise { + let lo = 0; + let hi = this.words.length - 1; + while (lo <= hi) { + const mid = (lo + hi) >>> 1; + const cmp = cmpAscii(word, this.words[mid]!); + if (cmp === 0) { + return { + word: this.words[mid]!, + offset: this.entries[mid * 2]!, + size: this.entries[mid * 2 + 1]!, + }; + } + if (cmp > 0) lo = mid + 1; + else hi = mid - 1; + } + return undefined; + } + + /** Decode one entry's body to UTF-8 text, with LRU caching by index. */ + async readText(entry: DictEntry, idx?: number): Promise { + if (idx !== undefined) { + const cached = this.bodyCache.get(idx); + if (cached) return decoder.decode(cached); + } + const bytes = await this.read(entry); + if (idx !== undefined) this.bodyCache.set(idx, bytes); + return decoder.decode(bytes); + } +} diff --git a/apps/readest-app/src/services/dictionaries/dictZip.ts b/apps/readest-app/src/services/dictionaries/dictZip.ts new file mode 100644 index 00000000..5907f965 --- /dev/null +++ b/apps/readest-app/src/services/dictionaries/dictZip.ts @@ -0,0 +1,247 @@ +/** + * DictZip reader — shared between StarDict (`.dict.dz`) and DICT (`.dict.dz`). + * + * `.dict.dz` is a standard gzip with a FEXTRA "RA" subfield listing the + * compressed length of each chunk. Each chunk's uncompressed length is + * exactly `chlen` (the last may be shorter). Chunks are separated by + * `Z_FULL_FLUSH` (BFINAL=0 + sync marker), so each chunk's deflate stream is + * non-terminating — `inflateSync` rejects them with "unexpected EOF". + * fflate's streaming `Inflate.push(bytes, false)` handles them correctly and + * emits chunk bytes via `ondata`. + * + * Both readers share the same lazy chunk decompression strategy. The + * `loadDictBody` factory probes the candidate `.dict.dz`; when chunk-mode is + * viable it returns a `DictZipChunkedDict` that decompresses on demand. When + * the FEXTRA is missing or the probe fails (or the file is plain `.dict`), + * it falls back to whole-file gunzip / passthrough into a buffer. + */ +import { gunzipSync, Inflate } from 'fflate'; + +const GZIP_MAGIC = [0x1f, 0x8b]; + +const isGzip = (bytes: Uint8Array): boolean => + bytes.length >= 2 && bytes[0] === GZIP_MAGIC[0] && bytes[1] === GZIP_MAGIC[1]; + +export interface DictZipMeta { + /** Uncompressed bytes per chunk (last may be shorter). */ + chlen: number; + /** Compressed bytes per chunk; sums to `compressedDataSize`. */ + chunkSizes: number[]; + /** Byte offset within the file where compressed chunk data begins. */ + compressedDataOffset: number; +} + +/** + * Parse the gzip header + FEXTRA RA subfield. Returns `null` for non-gzip + * files or gzip files without an RA subfield (those need whole-file gunzip). + */ +export function parseDictZipHeader(bytes: Uint8Array): DictZipMeta | null { + if (bytes.length < 12 || bytes[0] !== 0x1f || bytes[1] !== 0x8b || bytes[2] !== 0x08) { + return null; + } + const flg = bytes[3]!; + const hasFEXTRA = (flg & 0b100) !== 0; + if (!hasFEXTRA) return null; + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const xlen = view.getUint16(10, true); + let chlen = 0; + let chcnt = 0; + const chunkSizes: number[] = []; + + let p = 12; + while (p + 4 <= 12 + xlen) { + const si1 = bytes[p]!; + const si2 = bytes[p + 1]!; + const slen = view.getUint16(p + 2, true); + if (si1 === 0x52 && si2 === 0x41) { + // RA subfield: ver(2) + chlen(2) + chcnt(2) + chcnt × chunkSize(2). + const ver = view.getUint16(p + 4, true); + if (ver !== 1) return null; + chlen = view.getUint16(p + 6, true); + chcnt = view.getUint16(p + 8, true); + for (let i = 0; i < chcnt; i++) { + chunkSizes.push(view.getUint16(p + 10 + 2 * i, true)); + } + } + p += 4 + slen; + } + if (chcnt === 0 || chunkSizes.length !== chcnt) return null; + + // Skip past FEXTRA + optional FNAME, FCOMMENT, FHCRC. + let offset = 12 + xlen; + if (flg & 0b1000) { + while (offset < bytes.length && bytes[offset] !== 0) offset++; + offset++; + } + if (flg & 0b10000) { + while (offset < bytes.length && bytes[offset] !== 0) offset++; + offset++; + } + if (flg & 0b10) offset += 2; + + return { chlen, chunkSizes, compressedDataOffset: offset }; +} + +/** + * Inflate a single DictZip chunk via fflate's streaming `Inflate`. The + * chunk's deflate stream ends at `Z_FULL_FLUSH` (BFINAL=0 — `inflateSync` + * would reject), so we push with `final=false` and capture the data emitted + * by `ondata`. Returns `null` on failure. + */ +export function inflateChunkStreaming(chunkBytes: Uint8Array): Uint8Array | null { + let result: Uint8Array | null = null; + try { + const inf = new Inflate(); + inf.ondata = (data, _final) => { + if (result === null) result = data; + }; + inf.push(chunkBytes, false); + } catch { + return null; + } + if (!result || (result as Uint8Array).length === 0) return null; + return result; +} + +/** + * Bounded LRU. Tiny enough we don't bother with a real linked-list + * implementation; reusing the Map insertion order is fine. + */ +export class LRU { + private readonly max: number; + private readonly map = new Map(); + + constructor(max: number) { + this.max = max; + } + + get(key: K): V | undefined { + const v = this.map.get(key); + if (v !== undefined) { + this.map.delete(key); + this.map.set(key, v); + } + return v; + } + + set(key: K, value: V): void { + if (this.map.has(key)) this.map.delete(key); + this.map.set(key, value); + if (this.map.size > this.max) { + const first = this.map.keys().next().value as K | undefined; + if (first !== undefined) this.map.delete(first); + } + } +} + +/** Body of a DICT-style dict file — supports random-access uncompressed reads. */ +export interface DictBody { + read(offset: number, size: number): Promise; +} + +class BufferedDictBody implements DictBody { + constructor(private readonly buf: Uint8Array) {} + async read(offset: number, size: number): Promise { + return this.buf.subarray(offset, offset + size); + } +} + +class DictZipChunkedDict implements DictBody { + private readonly chunkOffsets: number[]; + private readonly chunkCache: LRU; + + constructor( + private readonly blob: Blob, + private readonly meta: DictZipMeta, + cacheSize: number, + ) { + this.chunkCache = new LRU(cacheSize); + const offsets: number[] = []; + let acc = meta.compressedDataOffset; + for (const cs of meta.chunkSizes) { + offsets.push(acc); + acc += cs; + } + this.chunkOffsets = offsets; + } + + async read(offset: number, size: number): Promise { + const chlen = this.meta.chlen; + const startChunk = Math.floor(offset / chlen); + const endChunk = Math.floor((offset + size - 1) / chlen); + + if (startChunk === endChunk) { + const chunk = await this.getChunk(startChunk); + const local = offset - startChunk * chlen; + return chunk.subarray(local, local + size); + } + const parts: Uint8Array[] = []; + let totalLen = 0; + for (let i = startChunk; i <= endChunk; i++) { + const chunk = await this.getChunk(i); + parts.push(chunk); + totalLen += chunk.length; + } + const combined = new Uint8Array(totalLen); + let pos = 0; + for (const p of parts) { + combined.set(p, pos); + pos += p.length; + } + const local = offset - startChunk * chlen; + return combined.subarray(local, local + size); + } + + private async getChunk(i: number): Promise { + const cached = this.chunkCache.get(i); + if (cached) return cached; + const start = this.chunkOffsets[i]!; + const compressedSize = this.meta.chunkSizes[i]!; + const compressed = new Uint8Array( + await this.blob.slice(start, start + compressedSize).arrayBuffer(), + ); + const inflated = inflateChunkStreaming(compressed); + if (!inflated) throw new Error(`Failed to inflate DictZip chunk ${i}`); + this.chunkCache.set(i, inflated); + return inflated; + } +} + +async function probeChunkInflate(blob: Blob, meta: DictZipMeta): Promise { + if (meta.chunkSizes.length === 0) return false; + const cs = meta.chunkSizes[0]!; + const start = meta.compressedDataOffset; + const compressed = new Uint8Array(await blob.slice(start, start + cs).arrayBuffer()); + const inflated = inflateChunkStreaming(compressed); + return !!inflated && inflated.length > 0 && inflated.length <= meta.chlen; +} + +export interface LoadDictBodyOpts { + /** LRU size for decompressed chunks in chunk mode. Defaults to 16. */ + chunkCacheSize?: number; +} + +/** + * Open a `.dict[.dz]` body for random-access reads. Tries lazy DictZip chunk + * mode first (header probe + chunk-0 inflate); falls back to whole-file + * gunzip when the FEXTRA is missing or the probe fails. For raw `.dict`, + * returns a passthrough buffer. + */ +export async function loadDictBody(blob: Blob, opts: LoadDictBodyOpts = {}): Promise { + const cacheSize = opts.chunkCacheSize ?? 16; + // Read just the gzip header + FEXTRA region (much less than the whole + // file). 64 KB is enormously generous — even thousands of chunks fit + // in a few KB. + const headBuf = await blob.slice(0, Math.min(blob.size, 64 * 1024)).arrayBuffer(); + const head = new Uint8Array(headBuf); + if (isGzip(head)) { + const meta = parseDictZipHeader(head); + if (meta && (await probeChunkInflate(blob, meta))) { + return new DictZipChunkedDict(blob, meta, cacheSize); + } + const all = new Uint8Array(await blob.arrayBuffer()); + return new BufferedDictBody(gunzipSync(all)); + } + return new BufferedDictBody(new Uint8Array(await blob.arrayBuffer())); +} diff --git a/apps/readest-app/src/services/dictionaries/dictionaryService.ts b/apps/readest-app/src/services/dictionaries/dictionaryService.ts index 9692745d..8727109c 100644 --- a/apps/readest-app/src/services/dictionaries/dictionaryService.ts +++ b/apps/readest-app/src/services/dictionaries/dictionaryService.ts @@ -48,7 +48,20 @@ interface MDictGroup { mdd: SourceFile[]; } -type Bundle = StarDictGroup | MDictGroup; +interface DictGroup { + kind: 'dict'; + stem: string; + index: SourceFile; + dict: SourceFile; +} + +interface SlobGroup { + kind: 'slob'; + stem: string; + slob: SourceFile; +} + +type Bundle = StarDictGroup | MDictGroup | DictGroup | SlobGroup; interface GroupResult { bundles: Bundle[]; @@ -96,7 +109,11 @@ function classify(source: SelectedFile): SourceFile { * - StarDict bundle = exactly one `.ifo` + one `.idx` + one `.dict` or * `.dict.dz` (sharing a stem). `.syn` is optional. * - MDict bundle = one `.mdx` + zero or more `.mdd` (sharing a stem). - * - A stem with both StarDict markers AND `.mdx` is treated as two bundles. + * - DICT (dictd) bundle = one `.index` + one `.dict` or `.dict.dz` + * (sharing a stem). Note: `.idx` (StarDict) and `.index` (DICT) differ + * only by spelling — the StarDict branch wins when both are present. + * - Slob bundle = one `.slob` file. + * - A stem with multiple format markers is treated as multiple bundles. */ export function groupBundlesByStem(files: SelectedFile[]): GroupResult { const classified = files.map(classify); @@ -111,15 +128,21 @@ export function groupBundlesByStem(files: SelectedFile[]): GroupResult { for (const [stem, group] of byStem) { const ifo = group.find((f) => f.ext === 'ifo'); const idx = group.find((f) => f.ext === 'idx'); + const indexFile = group.find((f) => f.ext === 'index'); const dict = group.find((f) => f.ext === 'dict' || f.isDictZip); const syn = group.find((f) => f.ext === 'syn'); const mdx = group.find((f) => f.ext === 'mdx'); const mdd = group.filter((f) => f.ext === 'mdd'); + const slob = group.find((f) => f.ext === 'slob'); if (ifo && idx && dict) { bundles.push({ kind: 'stardict', stem, ifo, idx, dict, syn }); + } else if (indexFile && dict) { + bundles.push({ kind: 'dict', stem, index: indexFile, dict }); } else if (mdx) { bundles.push({ kind: 'mdict', stem, mdx, mdd }); + } else if (slob) { + bundles.push({ kind: 'slob', stem, slob }); } else { orphans.push(...group); } @@ -313,6 +336,114 @@ async function importMdictBundle(fs: FileSystem, group: MDictGroup): Promise { + const bundleDir = await createBundleDir(fs); + const indexFile = await readSource(fs, group.index.source); + const dictFile = await readSource(fs, group.dict.source); + await writeBundleFile(fs, bundleDir, group.index.name, indexFile); + await writeBundleFile(fs, bundleDir, group.dict.name, dictFile); + + // Try to read the `00databaseshort` body for a friendly bundle name. The + // index lists it; the body lives in the dict. We do this best-effort: any + // failure falls back to the stem. + let name = group.stem; + let unsupported = false; + let unsupportedReason: string | undefined; + try { + const indexText = await indexFile.text(); + // Find the "00databaseshort\t\t" line. + const m = indexText.match(/^00databaseshort\t([^\t]+)\t([^\t\r\n]+)/m); + if (m) { + const decode = (s: string): number => { + let n = 0; + const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + for (const ch of s) { + const v = A.indexOf(ch); + if (v < 0) throw new Error('bad b64'); + n = n * 64 + v; + } + return n; + }; + const off = decode(m[1]!); + const size = decode(m[2]!); + // Read the dict body. If gzipped we need the whole thing — but for + // the friendly-name read, that's still cheap (the freedict bundles + // are <300 KB compressed). + const buf = await dictFile.arrayBuffer(); + const u8 = new Uint8Array(buf); + let body: Uint8Array; + if (u8[0] === 0x1f && u8[1] === 0x8b) { + const { gunzipSync } = await import('fflate'); + body = gunzipSync(u8); + } else { + body = u8; + } + name = new TextDecoder('utf-8').decode(body.subarray(off, off + size)).trim() || group.stem; + } + } catch { + // Best-effort label; the bundle is still importable. + } + if (!(await isGzip(dictFile))) { + // Plain `.dict` is technically supported by the reader, but we keep + // v1 scope identical to StarDict for consistency. + unsupported = true; + unsupportedReason = 'Raw .dict files are not supported in v1; please use .dict.dz format.'; + } + + return { + id: bundleDir, + kind: 'dict', + name, + bundleDir, + files: { + index: group.index.name, + dict: group.dict.name, + }, + addedAt: Date.now(), + unsupported: unsupported || undefined, + unsupportedReason, + }; +} + +async function importSlobBundle(fs: FileSystem, group: SlobGroup): Promise { + const bundleDir = await createBundleDir(fs); + const slobFile = await readSource(fs, group.slob.source); + await writeBundleFile(fs, bundleDir, group.slob.name, slobFile); + + // Read header bytes to derive the friendly name + sanity-check compression. + let name = group.stem; + let unsupported = false; + let unsupportedReason: string | undefined; + try { + const { SlobReader } = await import('./slobReader'); + const reader = new SlobReader(); + await reader.load({ slob: slobFile }); + const labelTag = reader.header.tags['label']; + if (labelTag) name = labelTag.replace(/\0+$/u, '') || group.stem; + } catch (err) { + const message = (err as Error).message ?? String(err); + unsupported = true; + if (/Unsupported Slob compression/i.test(message)) { + unsupportedReason = message; + } else if (/Unsupported Slob encoding/i.test(message)) { + unsupportedReason = message; + } else { + unsupportedReason = `Failed to parse Slob header: ${message}`; + } + } + + return { + id: bundleDir, + kind: 'slob', + name, + bundleDir, + files: { slob: group.slob.name }, + addedAt: Date.now(), + unsupported: unsupported || undefined, + unsupportedReason, + }; +} + export interface ImportDictionariesResult { imported: ImportedDictionary[]; /** Filenames that didn't form a valid bundle. */ @@ -333,8 +464,12 @@ export async function importDictionaries( for (const bundle of bundles) { if (bundle.kind === 'stardict') { imported.push(await importStarDictBundle(fs, bundle)); - } else { + } else if (bundle.kind === 'mdict') { imported.push(await importMdictBundle(fs, bundle)); + } else if (bundle.kind === 'dict') { + imported.push(await importDictBundle(fs, bundle)); + } else { + imported.push(await importSlobBundle(fs, bundle)); } } return { imported, orphanFiles: orphans.map((o) => o.name) }; diff --git a/apps/readest-app/src/services/dictionaries/providers/dictProvider.ts b/apps/readest-app/src/services/dictionaries/providers/dictProvider.ts new file mode 100644 index 00000000..d687bd4f --- /dev/null +++ b/apps/readest-app/src/services/dictionaries/providers/dictProvider.ts @@ -0,0 +1,107 @@ +/** + * DICT (dictd) provider. + * + * Wraps {@link DictReader} for the popup. Renders entries as plain text + * with newlines preserved — DICT bodies are nearly always plain text per + * RFC 2229; HTML is rare and the few HTML-bearing dicts in the wild use + * `MIME` indicators outside the v1 scope. + */ +import type { DictionaryProvider, ImportedDictionary } from '../types'; +import type { DictionaryFileOpener } from './starDictProvider'; +import { DictReader } from '../dictReader'; + +const renderEntry = (container: HTMLElement, word: string, text: string): void => { + const h1 = document.createElement('h1'); + h1.textContent = word; + h1.className = 'text-lg font-bold'; + container.appendChild(h1); + + const pre = document.createElement('pre'); + pre.textContent = text; + pre.className = 'mt-2 whitespace-pre-wrap break-words text-sm font-sans'; + container.appendChild(pre); +}; + +export interface CreateDictProviderArgs { + dict: ImportedDictionary; + fs: DictionaryFileOpener; + /** Localized label override; defaults to the bundle name. */ + label?: string; +} + +export const createDictProvider = ({ + dict, + fs, + label, +}: CreateDictProviderArgs): DictionaryProvider => { + let reader: DictReader | null = null; + let initPromise: Promise | null = null; + let initError: Error | null = null; + + const initOnce = async (): Promise => { + if (reader) return reader; + if (initError) throw initError; + if (!initPromise) { + initPromise = (async () => { + if (!dict.files.index || !dict.files.dict) { + throw new Error('DICT bundle is missing required files'); + } + const [indexFile, dictFile] = await Promise.all([ + fs.openFile(`${dict.bundleDir}/${dict.files.index}`, 'Dictionaries'), + fs.openFile(`${dict.bundleDir}/${dict.files.dict}`, 'Dictionaries'), + ]); + + const r = new DictReader(); + await r.load({ index: indexFile, dict: dictFile }); + reader = r; + return r; + })().catch((err) => { + initError = err instanceof Error ? err : new Error(String(err)); + initPromise = null; + throw initError; + }); + } + return initPromise; + }; + + return { + id: dict.id, + kind: 'dict', + label: label ?? dict.name, + async lookup(word, ctx) { + let r: DictReader; + try { + r = await initOnce(); + } catch (err) { + return { + ok: false, + reason: 'error', + message: `Failed to load dictionary: ${(err as Error).message}`, + }; + } + if (ctx.signal.aborted) return { ok: false, reason: 'error', message: 'aborted' }; + + try { + const entry = await r.lookup(word); + if (ctx.signal.aborted) return { ok: false, reason: 'error', message: 'aborted' }; + if (!entry) return { ok: false, reason: 'empty' }; + + const text = await r.readText(entry); + if (ctx.signal.aborted) return { ok: false, reason: 'error', message: 'aborted' }; + if (!text) return { ok: false, reason: 'empty' }; + renderEntry(ctx.container, entry.word, text); + return { + ok: true, + headword: entry.word, + sourceLabel: r.info.label || dict.name, + }; + } catch (err) { + return { + ok: false, + reason: 'error', + message: (err as Error).message, + }; + } + }, + }; +}; diff --git a/apps/readest-app/src/services/dictionaries/providers/slobProvider.ts b/apps/readest-app/src/services/dictionaries/providers/slobProvider.ts new file mode 100644 index 00000000..6006090e --- /dev/null +++ b/apps/readest-app/src/services/dictionaries/providers/slobProvider.ts @@ -0,0 +1,144 @@ +/** + * Slob (Aard 2) provider. + * + * Wraps {@link SlobReader} for the popup. Renders entries based on their + * content type: + * - `text/html*` → set as `innerHTML` (Wikipedia/Wiktionary slobs) + * - `text/plain` → wrapped `
` with newlines preserved
+ *   - everything else → flagged as unsupported (e.g. CSS/JS resources, which
+ *     belong to the slob's bundled stylesheet, not lookup results)
+ *
+ * Slob refs starting with `~/` are bundled resource paths (CSS/JS used by
+ * the dictionary's own templates). They are not user-visible headwords and
+ * are filtered before binary search hits them.
+ */
+import type { DictionaryProvider, ImportedDictionary } from '../types';
+import type { DictionaryFileOpener } from './starDictProvider';
+import { SlobReader } from '../slobReader';
+
+const renderEntry = (
+  container: HTMLElement,
+  word: string,
+  contentType: string,
+  data: Uint8Array,
+): { ok: true } | { ok: false; reason: 'empty' | 'unsupported'; message?: string } => {
+  if (data.length === 0) return { ok: false, reason: 'empty' };
+
+  const ct = contentType.split(';', 1)[0]!.trim().toLowerCase();
+  const text = new TextDecoder('utf-8').decode(data);
+
+  if (ct === 'text/html' || ct === 'application/xhtml+xml') {
+    const h1 = document.createElement('h1');
+    h1.textContent = word;
+    h1.className = 'text-lg font-bold';
+    container.appendChild(h1);
+    const div = document.createElement('div');
+    // Strip  tags pointing at slob-internal CSS;
+    // they 404 because we don't expose the bundled CSS as URLs in the popup.
+    div.innerHTML = text.replace(/]*rel=["']?stylesheet[^>]*>/gi, '');
+    div.className = 'mt-2 text-sm';
+    container.appendChild(div);
+    return { ok: true };
+  }
+  if (ct === 'text/plain' || ct === '') {
+    const h1 = document.createElement('h1');
+    h1.textContent = word;
+    h1.className = 'text-lg font-bold';
+    container.appendChild(h1);
+    const pre = document.createElement('pre');
+    pre.textContent = text;
+    pre.className = 'mt-2 whitespace-pre-wrap break-words text-sm font-sans';
+    container.appendChild(pre);
+    return { ok: true };
+  }
+  return {
+    ok: false,
+    reason: 'unsupported',
+    message: `Slob content type "${ct}" is not supported`,
+  };
+};
+
+export interface CreateSlobProviderArgs {
+  dict: ImportedDictionary;
+  fs: DictionaryFileOpener;
+  /** Localized label override; defaults to the bundle name. */
+  label?: string;
+}
+
+export const createSlobProvider = ({
+  dict,
+  fs,
+  label,
+}: CreateSlobProviderArgs): DictionaryProvider => {
+  let reader: SlobReader | null = null;
+  let initPromise: Promise | null = null;
+  let initError: Error | null = null;
+
+  const initOnce = async (): Promise => {
+    if (reader) return reader;
+    if (initError) throw initError;
+    if (!initPromise) {
+      initPromise = (async () => {
+        if (!dict.files.slob) {
+          throw new Error('Slob bundle is missing the .slob file');
+        }
+        const slobFile = await fs.openFile(`${dict.bundleDir}/${dict.files.slob}`, 'Dictionaries');
+        const r = new SlobReader();
+        await r.load({ slob: slobFile });
+        reader = r;
+        return r;
+      })().catch((err) => {
+        initError = err instanceof Error ? err : new Error(String(err));
+        initPromise = null;
+        throw initError;
+      });
+    }
+    return initPromise;
+  };
+
+  return {
+    id: dict.id,
+    kind: 'slob',
+    label: label ?? dict.name,
+    async lookup(word, ctx) {
+      let r: SlobReader;
+      try {
+        r = await initOnce();
+      } catch (err) {
+        return {
+          ok: false,
+          reason: 'error',
+          message: `Failed to load dictionary: ${(err as Error).message}`,
+        };
+      }
+      if (ctx.signal.aborted) return { ok: false, reason: 'error', message: 'aborted' };
+
+      try {
+        // Bundled resources (CSS/JS) live under refs starting with `~/`.
+        // We don't expose them as lookup hits.
+        if (word.startsWith('~/')) return { ok: false, reason: 'empty' };
+
+        const ref = await r.findRef(word);
+        if (ctx.signal.aborted) return { ok: false, reason: 'error', message: 'aborted' };
+        if (!ref) return { ok: false, reason: 'empty' };
+        if (ref.key.startsWith('~/')) return { ok: false, reason: 'empty' };
+
+        const blob = await r.readBlob(ref);
+        if (ctx.signal.aborted) return { ok: false, reason: 'error', message: 'aborted' };
+        const outcome = renderEntry(ctx.container, ref.key, blob.contentType, blob.data);
+        if (!outcome.ok) return outcome;
+        return {
+          ok: true,
+          headword: ref.key,
+          sourceLabel: r.header.tags['label']?.replace(/\0+$/u, '') || dict.name,
+        };
+      } catch (err) {
+        return {
+          ok: false,
+          reason: 'error',
+          message: (err as Error).message,
+        };
+      }
+    },
+  };
+};
diff --git a/apps/readest-app/src/services/dictionaries/providers/webSearchProvider.ts b/apps/readest-app/src/services/dictionaries/providers/webSearchProvider.ts
new file mode 100644
index 00000000..805c08e0
--- /dev/null
+++ b/apps/readest-app/src/services/dictionaries/providers/webSearchProvider.ts
@@ -0,0 +1,80 @@
+/**
+ * Web-search provider.
+ *
+ * Renders an "Open in [name]" card inside the popup tab. The ``
+ * uses the same pattern as `wikipediaProvider`'s "Read on Wikipedia" link,
+ * so the popup's container click delegation routes the click through
+ * Tauri's `openUrl` on native and `target="_blank"` works on the web build.
+ *
+ * Iframe embedding was considered and rejected — Google, Urban Dictionary,
+ * and Merriam-Webster all set `X-Frame-Options: DENY/SAMEORIGIN`. v1
+ * pragmatically opens externally; a Tauri-native webview overlay is a
+ * follow-up if there's demand.
+ */
+import type { DictionaryProvider, WebSearchEntry } from '../types';
+import { substituteUrlTemplate } from '../webSearchTemplates';
+import { isTauriAppPlatform } from '@/services/environment';
+import { stubTranslation as _ } from '@/utils/misc';
+
+const isTauri = isTauriAppPlatform();
+
+export interface CreateWebSearchProviderArgs {
+  template: WebSearchEntry;
+  /** Override the displayed label (e.g. localized built-in name). */
+  label?: string;
+}
+
+export const createWebSearchProvider = ({
+  template,
+  label,
+}: CreateWebSearchProviderArgs): DictionaryProvider => ({
+  id: template.id,
+  kind: 'web',
+  label: label ?? template.name,
+  async lookup(word, ctx) {
+    if (ctx.signal.aborted) return { ok: false, reason: 'error', message: 'aborted' };
+    const trimmed = word.trim();
+    if (!trimmed) return { ok: false, reason: 'empty' };
+
+    const url = substituteUrlTemplate(template.urlTemplate, trimmed);
+
+    const hgroup = document.createElement('hgroup');
+    const h1 = document.createElement('h1');
+    h1.textContent = trimmed;
+    h1.className = 'text-lg font-bold';
+    hgroup.append(h1);
+    const sub = document.createElement('p');
+    sub.textContent = template.name;
+    sub.className = 'text-sm italic not-eink:opacity-75';
+    hgroup.append(sub);
+    ctx.container.append(hgroup);
+
+    const description = document.createElement('p');
+    description.className = 'mt-3 text-sm';
+    description.textContent = _('Open the search result in your browser:');
+    ctx.container.append(description);
+
+    const linkWrapper = document.createElement('p');
+    linkWrapper.className = 'mt-3';
+    const link = document.createElement('a');
+    link.href = url;
+    // Skip target="_blank" on Tauri; the popup's click delegation routes
+    // through `openUrl`. iOS WebView's _blank handling fails otherwise.
+    if (!isTauri) link.target = '_blank';
+    link.rel = 'noopener noreferrer';
+    link.className =
+      'btn btn-sm btn-primary normal-case text-primary-content not-eink:no-underline';
+    // `stubTranslation` is just an extraction marker — the runtime value is
+    // the key itself. We interpolate the provider name manually.
+    link.textContent = _('Open in {{name}}').replace('{{name}}', template.name);
+    linkWrapper.append(link);
+    ctx.container.append(linkWrapper);
+
+    const urlPreview = document.createElement('p');
+    urlPreview.className = 'mt-3 text-base-content/60 break-all text-xs';
+    urlPreview.textContent = url;
+    ctx.container.append(urlPreview);
+
+    return { ok: true, headword: trimmed, sourceLabel: template.name };
+  },
+});
diff --git a/apps/readest-app/src/services/dictionaries/registry.ts b/apps/readest-app/src/services/dictionaries/registry.ts
index 1d8e18fd..fd72869e 100644
--- a/apps/readest-app/src/services/dictionaries/registry.ts
+++ b/apps/readest-app/src/services/dictionaries/registry.ts
@@ -13,12 +13,21 @@
  * runtime-only and non-serializable; storing them alongside metadata would
  * pollute the synced settings shape.
  */
-import type { DictionaryProvider, DictionarySettings, ImportedDictionary } from './types';
+import type {
+  DictionaryProvider,
+  DictionarySettings,
+  ImportedDictionary,
+  WebSearchEntry,
+} from './types';
 import { BUILTIN_PROVIDER_IDS } from './types';
 import { wiktionaryProvider } from './providers/wiktionaryProvider';
 import { wikipediaProvider } from './providers/wikipediaProvider';
 import { createStarDictProvider, type DictionaryFileOpener } from './providers/starDictProvider';
 import { createMdictProvider } from './providers/mdictProvider';
+import { createDictProvider } from './providers/dictProvider';
+import { createSlobProvider } from './providers/slobProvider';
+import { createWebSearchProvider } from './providers/webSearchProvider';
+import { getBuiltinWebSearch } from './webSearchTemplates';
 
 const instanceCache = new Map();
 
@@ -39,10 +48,23 @@ const builtinFor = (id: string): DictionaryProvider | undefined => {
   return undefined;
 };
 
+/**
+ * Resolve a `web:*` id to its template — built-in if id starts with
+ * `web:builtin:`, else look it up in `settings.webSearches`.
+ */
+const findWebTemplate = (id: string, settings: DictionarySettings): WebSearchEntry | undefined => {
+  if (id.startsWith('web:builtin:')) return getBuiltinWebSearch(id);
+  const list = settings.webSearches ?? [];
+  const tpl = list.find((t) => t.id === id);
+  if (!tpl || tpl.deletedAt) return undefined;
+  return tpl;
+};
+
 const getOrCreate = (
   id: string,
   dict: ImportedDictionary | undefined,
   fs: DictionaryFileOpener | undefined,
+  settings: DictionarySettings,
 ): DictionaryProvider | undefined => {
   const cached = instanceCache.get(id);
   if (cached) return cached;
@@ -51,6 +73,13 @@ const getOrCreate = (
     instanceCache.set(id, builtin);
     return builtin;
   }
+  if (id.startsWith('web:')) {
+    const tpl = findWebTemplate(id, settings);
+    if (!tpl) return undefined;
+    const provider = createWebSearchProvider({ template: tpl });
+    instanceCache.set(id, provider);
+    return provider;
+  }
   if (!dict) return undefined;
   if (!fs) return undefined;
   if (dict.kind === 'stardict') {
@@ -63,6 +92,16 @@ const getOrCreate = (
     instanceCache.set(id, provider);
     return provider;
   }
+  if (dict.kind === 'dict') {
+    const provider = createDictProvider({ dict, fs });
+    instanceCache.set(id, provider);
+    return provider;
+  }
+  if (dict.kind === 'slob') {
+    const provider = createSlobProvider({ dict, fs });
+    instanceCache.set(id, provider);
+    return provider;
+  }
   return undefined;
 };
 
@@ -83,14 +122,19 @@ export const getEnabledProviders = ({
   for (const id of settings.providerOrder) {
     if (settings.providerEnabled[id] === false) continue;
     if (id.startsWith('builtin:')) {
-      const provider = getOrCreate(id, undefined, undefined);
+      const provider = getOrCreate(id, undefined, undefined, settings);
+      if (provider) out.push(provider);
+      continue;
+    }
+    if (id.startsWith('web:')) {
+      const provider = getOrCreate(id, undefined, undefined, settings);
       if (provider) out.push(provider);
       continue;
     }
     const dict = dictById.get(id);
     if (!dict) continue;
     if (dict.deletedAt || dict.unavailable || dict.unsupported) continue;
-    const provider = getOrCreate(id, dict, fs);
+    const provider = getOrCreate(id, dict, fs, settings);
     if (provider) out.push(provider);
   }
   return out;
diff --git a/apps/readest-app/src/services/dictionaries/slobReader.ts b/apps/readest-app/src/services/dictionaries/slobReader.ts
new file mode 100644
index 00000000..36d6e0fc
--- /dev/null
+++ b/apps/readest-app/src/services/dictionaries/slobReader.ts
@@ -0,0 +1,476 @@
+/**
+ * Slob (Aard 2) reader. Reference: https://github.com/itkach/slob `slob.py`.
+ *
+ * # File layout
+ *
+ * ```
+ *   header    : magic, uuid, encoding, compression, tags, content_types,
+ *               blob_count, store_offset, file_size
+ *   refs      : ItemList at refs_offset = end of header
+ *   store     : ItemList at store_offset
+ * ```
+ *
+ * ## ItemList format (used for refs and store):
+ *
+ * ```
+ *   count  : 4 bytes BE  (refs only — bin/store list count is 4 bytes BE)
+ *   pos[i] : 8 bytes BE × count
+ *   data   : items, accessed at data_offset + pos[i]
+ * ```
+ *
+ * ## Ref item:
+ *
+ * ```
+ *   key       : pascal text, 2-byte BE length
+ *   bin_index : 4 bytes BE
+ *   item_index: 2 bytes BE
+ *   fragment  : pascal text, 1-byte length
+ * ```
+ *
+ * ## Store item:
+ *
+ * ```
+ *   bin_item_count   : 4 bytes BE
+ *   content_type_ids : 1 byte × bin_item_count
+ *   compressed_len   : 4 bytes BE
+ *   compressed       : bytes (compression algorithm from header)
+ *
+ *   decompressed:
+ *     pos[i]   : 4 bytes BE × bin_item_count
+ *     items    : at pos[i]: 4-byte length + content
+ * ```
+ *
+ * # Compression
+ *
+ * The header carries a compression name: `zlib`, `bz2`, `lzma2`, or empty.
+ * v1 supports `zlib` only — that's what virtually every Wikipedia/Wiktionary-
+ * derived slob ships with. `bz2` and `lzma2` are flagged unsupported at
+ * import time so the popup hides the provider with a clear reason.
+ *
+ * # Refs sort order
+ *
+ * `slob.py` uses ICU PRIMARY/TERTIARY collation when sorting refs. In
+ * practice for Latin-only ASCII keys the byte order matches lowercased-
+ * ASCII order; for accented or non-Latin scripts the orders differ. v1
+ * does case-insensitive ASCII binary search, plus a tiny linear "near"
+ * scan around the binary-search bracket to absorb the small ICU/byte
+ * order mismatches that occur with diacritics. Real Wikipedia/Wiktionary
+ * slobs lookup hot headwords correctly with this strategy; pure ICU
+ * collation would require shipping ICU data and isn't worth it for v1.
+ */
+import { unzlibSync } from 'fflate';
+import { LRU } from './dictZip';
+
+const utf8 = new TextDecoder('utf-8');
+
+export interface SlobHeader {
+  uuid: Uint8Array;
+  encoding: string;
+  compression: string;
+  tags: Record;
+  contentTypes: string[];
+  blobCount: number;
+  storeOffset: number;
+  fileSize: number;
+  refsOffset: number;
+}
+
+export interface SlobRef {
+  key: string;
+  binIndex: number;
+  itemIndex: number;
+  fragment: string;
+}
+
+export interface SlobBlob {
+  /** Resolved content type, e.g. `text/html;charset=utf-8`. */
+  contentType: string;
+  /** Raw decompressed item bytes (not text-decoded — could be CSS, JS, etc.). */
+  data: Uint8Array;
+}
+
+const MAGIC = new Uint8Array([0x21, 0x2d, 0x31, 0x53, 0x4c, 0x4f, 0x42, 0x1f]); // "!-1SLOB\x1F"
+
+class ByteCursor {
+  pos = 0;
+  constructor(public readonly buf: Uint8Array) {}
+
+  read(n: number): Uint8Array {
+    const out = this.buf.subarray(this.pos, this.pos + n);
+    this.pos += n;
+    return out;
+  }
+  byte(): number {
+    return this.buf[this.pos++]!;
+  }
+  u16(): number {
+    const b0 = this.buf[this.pos]!;
+    const b1 = this.buf[this.pos + 1]!;
+    this.pos += 2;
+    return (b0 << 8) | b1;
+  }
+  u32(): number {
+    const v = readU32BE(this.buf, this.pos);
+    this.pos += 4;
+    return v;
+  }
+  u64(): number {
+    const hi = readU32BE(this.buf, this.pos);
+    const lo = readU32BE(this.buf, this.pos + 4);
+    this.pos += 8;
+    // JS numbers are safe up to 2^53; slob files are far smaller.
+    return hi * 0x1_0000_0000 + lo;
+  }
+  /** Pascal-text with `n`-byte BE length prefix. Trailing NULs are trimmed (slob's editable padding). */
+  text(lenBytes: 1 | 2): string {
+    const len = lenBytes === 1 ? this.byte() : this.u16();
+    const bytes = this.read(len);
+    return decodePascalText(bytes);
+  }
+}
+
+function readU32BE(b: Uint8Array, o: number): number {
+  return (
+    (((b[o]! << 24) >>> 0) | ((b[o + 1]! << 16) >>> 0) | ((b[o + 2]! << 8) >>> 0) | b[o + 3]!) >>> 0
+  );
+}
+
+/**
+ * slob's `_read_text` trims a NUL-padded value when the byte length equals
+ * the format max (255 for 1-byte length, 65535 for 2-byte). We always trim
+ * trailing NULs — they're meaningless in tag values and only appear due to
+ * the editable-padding scheme.
+ */
+function decodePascalText(bytes: Uint8Array): string {
+  let end = bytes.length;
+  while (end > 0 && bytes[end - 1] === 0) end--;
+  return utf8.decode(bytes.subarray(0, end));
+}
+
+const cmpAsciiCI = (a: string, b: string): number => {
+  const x = a.toLowerCase();
+  const y = b.toLowerCase();
+  return x < y ? -1 : x > y ? 1 : 0;
+};
+
+/** Read header from a Blob. Reads ~8 KB; tags+content types fit comfortably. */
+async function readHeader(blob: Blob): Promise {
+  // 64 KB is plenty even for slobs with hundreds of tags / content types.
+  const headBytes = new Uint8Array(
+    await blob.slice(0, Math.min(blob.size, 64 * 1024)).arrayBuffer(),
+  );
+  const c = new ByteCursor(headBytes);
+  const magic = c.read(MAGIC.length);
+  for (let i = 0; i < MAGIC.length; i++) {
+    if (magic[i] !== MAGIC[i]) throw new Error('Not a Slob file (bad magic)');
+  }
+  const uuid = c.read(16);
+  const encoding = c.text(1);
+  if (encoding.toLowerCase() !== 'utf-8') {
+    // slob.py supports any Python codec, but everything in the wild is utf-8.
+    throw new Error(`Unsupported Slob encoding: ${encoding}`);
+  }
+  const compression = c.text(1);
+  const tagCount = c.byte();
+  const tags: Record = {};
+  for (let i = 0; i < tagCount; i++) {
+    const k = c.text(1);
+    const v = c.text(1);
+    tags[k] = v;
+  }
+  const ctCount = c.byte();
+  const contentTypes: string[] = [];
+  for (let i = 0; i < ctCount; i++) contentTypes.push(c.text(2));
+  const blobCount = c.u32();
+  const storeOffset = c.u64();
+  const fileSize = c.u64();
+  const refsOffset = c.pos;
+  return {
+    uuid: new Uint8Array(uuid),
+    encoding,
+    compression,
+    tags,
+    contentTypes,
+    blobCount,
+    storeOffset,
+    fileSize,
+    refsOffset,
+  };
+}
+
+export interface SlobReaderOpts {
+  slob: Blob;
+  /** Decompressed-bin LRU size. Defaults to 16. */
+  binCacheSize?: number;
+  /** Decoded-ref LRU size. Defaults to 1024. */
+  refCacheSize?: number;
+}
+
+export class SlobReader {
+  header: SlobHeader = {
+    uuid: new Uint8Array(),
+    encoding: 'utf-8',
+    compression: '',
+    tags: {},
+    contentTypes: [],
+    blobCount: 0,
+    storeOffset: 0,
+    fileSize: 0,
+    refsOffset: 0,
+  };
+  /** Number of refs (text-key → blob mappings). */
+  refCount = 0;
+
+  private blob: Blob | null = null;
+  /** ItemList header offset for refs (count then 8-byte positions). */
+  private refsDataOffset = 0;
+  /** Position of each ref within the refs data area; one Float64-style entry per ref but stored as two i32s (hi, lo) for >4 GB safety. */
+  private refPositions: Float64Array = new Float64Array(0);
+
+  /** Bin count + per-bin position metadata. */
+  private binCount = 0;
+  private binDataOffset = 0;
+  private binPositions: Float64Array = new Float64Array(0);
+
+  private refCache: LRU;
+  private binCache: LRU;
+
+  constructor(opts?: { binCacheSize?: number; refCacheSize?: number }) {
+    this.refCache = new LRU(opts?.refCacheSize ?? 1024);
+    this.binCache = new LRU(
+      opts?.binCacheSize ?? 16,
+    );
+  }
+
+  async load(opts: SlobReaderOpts): Promise {
+    if (opts.refCacheSize !== undefined) {
+      this.refCache = new LRU(opts.refCacheSize);
+    }
+    if (opts.binCacheSize !== undefined) {
+      this.binCache = new LRU(
+        opts.binCacheSize,
+      );
+    }
+    this.blob = opts.slob;
+    this.header = await readHeader(opts.slob);
+
+    if (this.header.compression !== 'zlib') {
+      throw new Error(
+        `Unsupported Slob compression "${this.header.compression}". v1 supports zlib only.`,
+      );
+    }
+    if (this.header.fileSize !== opts.slob.size) {
+      throw new Error(
+        `Slob file size mismatch: header says ${this.header.fileSize}, file is ${opts.slob.size}`,
+      );
+    }
+
+    // Refs ItemList: 4-byte count, then count × u64 positions.
+    const refsHeader = new Uint8Array(
+      await opts.slob.slice(this.header.refsOffset, this.header.refsOffset + 4).arrayBuffer(),
+    );
+    this.refCount = readU32BE(refsHeader, 0);
+    const refPosStart = this.header.refsOffset + 4;
+    const refPosBytes = new Uint8Array(
+      await opts.slob.slice(refPosStart, refPosStart + this.refCount * 8).arrayBuffer(),
+    );
+    this.refPositions = new Float64Array(this.refCount);
+    for (let i = 0; i < this.refCount; i++) {
+      const o = i * 8;
+      const hi = readU32BE(refPosBytes, o);
+      const lo = readU32BE(refPosBytes, o + 4);
+      this.refPositions[i] = hi * 0x1_0000_0000 + lo;
+    }
+    this.refsDataOffset = refPosStart + this.refCount * 8;
+
+    // Store ItemList: 4-byte bin_count, then bin_count × u64 positions.
+    const storeHeader = new Uint8Array(
+      await opts.slob.slice(this.header.storeOffset, this.header.storeOffset + 4).arrayBuffer(),
+    );
+    this.binCount = readU32BE(storeHeader, 0);
+    const binPosStart = this.header.storeOffset + 4;
+    const binPosBytes = new Uint8Array(
+      await opts.slob.slice(binPosStart, binPosStart + this.binCount * 8).arrayBuffer(),
+    );
+    this.binPositions = new Float64Array(this.binCount);
+    for (let i = 0; i < this.binCount; i++) {
+      const o = i * 8;
+      const hi = readU32BE(binPosBytes, o);
+      const lo = readU32BE(binPosBytes, o + 4);
+      this.binPositions[i] = hi * 0x1_0000_0000 + lo;
+    }
+    this.binDataOffset = binPosStart + this.binCount * 8;
+  }
+
+  /**
+   * Decode one ref. Each ref is variable-length: 2-byte BE key length + key
+   * bytes + 4-byte BE bin_index + 2-byte BE item_index + 1-byte fragment
+   * length + fragment bytes. We read a generous slice (max key 65535 +
+   * max fragment 255 + 7 fixed bytes), then truncate.
+   */
+  private async decodeRef(i: number): Promise {
+    const cached = this.refCache.get(i);
+    if (cached) return cached;
+    if (!this.blob) throw new Error('slob blob not loaded');
+    const start = this.refsDataOffset + this.refPositions[i]!;
+    // Maximum ref size: 2 + 65535 + 4 + 2 + 1 + 255 = 65799. Realistic
+    // headwords are ≤ 200 bytes; reading 4 KB covers virtually all of
+    // them with one Blob slice.
+    const slice = new Uint8Array(
+      await this.blob.slice(start, Math.min(start + 4 * 1024, this.blob.size)).arrayBuffer(),
+    );
+    let pos = 0;
+    const keyLen = (slice[pos]! << 8) | slice[pos + 1]!;
+    pos += 2;
+    let keyBytes: Uint8Array;
+    if (pos + keyLen <= slice.length) {
+      keyBytes = slice.subarray(pos, pos + keyLen);
+      pos += keyLen;
+    } else {
+      // Tail spilled past our 4 KB read — fall back to a precise re-read.
+      const need = 2 + keyLen + 4 + 2 + 1 + 255;
+      const exact = new Uint8Array(
+        await this.blob.slice(start, Math.min(start + need, this.blob.size)).arrayBuffer(),
+      );
+      keyBytes = exact.subarray(2, 2 + keyLen);
+      pos = 2 + keyLen;
+      const binIndex = readU32BE(exact, pos);
+      pos += 4;
+      const itemIndex = (exact[pos]! << 8) | exact[pos + 1]!;
+      pos += 2;
+      const fragLen = exact[pos]!;
+      pos += 1;
+      const fragment = decodePascalText(exact.subarray(pos, pos + fragLen));
+      const ref: SlobRef = {
+        key: decodePascalText(keyBytes),
+        binIndex,
+        itemIndex,
+        fragment,
+      };
+      this.refCache.set(i, ref);
+      return ref;
+    }
+    const binIndex = readU32BE(slice, pos);
+    pos += 4;
+    const itemIndex = (slice[pos]! << 8) | slice[pos + 1]!;
+    pos += 2;
+    const fragLen = slice[pos]!;
+    pos += 1;
+    const fragment = decodePascalText(slice.subarray(pos, pos + fragLen));
+
+    const ref: SlobRef = {
+      key: decodePascalText(keyBytes),
+      binIndex,
+      itemIndex,
+      fragment,
+    };
+    this.refCache.set(i, ref);
+    return ref;
+  }
+
+  /** Look up the first ref whose key case-insensitively matches `word`. */
+  async findRef(word: string): Promise {
+    if (!this.refCount) return undefined;
+    let lo = 0;
+    let hi = this.refCount - 1;
+    while (lo <= hi) {
+      const mid = (lo + hi) >>> 1;
+      const r = await this.decodeRef(mid);
+      const cmp = cmpAsciiCI(word, r.key);
+      if (cmp === 0) return r;
+      if (cmp > 0) lo = mid + 1;
+      else hi = mid - 1;
+    }
+    // Binary search misses can happen when the slob was sorted with ICU
+    // collation (locale-aware) but our key compare is byte-wise: keys with
+    // diacritics or non-ASCII codepoints may end up adjacent to but not
+    // exactly at the binary-search bracket. Scan a tiny window.
+    const NEAR_WINDOW = 8;
+    const wlo = Math.max(0, lo - NEAR_WINDOW);
+    const whi = Math.min(this.refCount - 1, lo + NEAR_WINDOW);
+    for (let i = wlo; i <= whi; i++) {
+      const r = await this.decodeRef(i);
+      if (cmpAsciiCI(word, r.key) === 0) return r;
+    }
+    return undefined;
+  }
+
+  /**
+   * Decompress a bin and cache it. The bin lays out as:
+   *   bin_item_count : u32 BE
+   *   ct_ids         : 1 byte × bin_item_count
+   *   compressed_len : u32 BE
+   *   compressed     : bytes
+   */
+  private async getBin(binIndex: number): Promise<{ contentTypeIds: Uint8Array; raw: Uint8Array }> {
+    const cached = this.binCache.get(binIndex);
+    if (cached) return cached;
+    if (!this.blob) throw new Error('slob blob not loaded');
+
+    const start = this.binDataOffset + this.binPositions[binIndex]!;
+    // Read the small leading header first to learn bin_item_count + comp_len.
+    const headSlice = new Uint8Array(
+      await this.blob.slice(start, Math.min(start + 4 * 1024, this.blob.size)).arrayBuffer(),
+    );
+    const binItemCount = readU32BE(headSlice, 0);
+    if (4 + binItemCount + 4 > headSlice.length) {
+      // 4 KB wasn't enough — re-read precise size.
+      const need = 4 + binItemCount + 4;
+      const exact = new Uint8Array(await this.blob.slice(start, start + need).arrayBuffer());
+      // Use exact for the lengths only; we still need to read the body.
+      const ctIds = exact.subarray(4, 4 + binItemCount);
+      const compLen = readU32BE(exact, 4 + binItemCount);
+      const compStart = start + 4 + binItemCount + 4;
+      const comp = new Uint8Array(
+        await this.blob.slice(compStart, compStart + compLen).arrayBuffer(),
+      );
+      const raw = unzlibSync(comp);
+      const out = { contentTypeIds: ctIds.slice(), raw };
+      this.binCache.set(binIndex, out);
+      return out;
+    }
+    const ctIds = headSlice.subarray(4, 4 + binItemCount);
+    const compLen = readU32BE(headSlice, 4 + binItemCount);
+    const compStartInSlice = 4 + binItemCount + 4;
+    let compBytes: Uint8Array;
+    if (compStartInSlice + compLen <= headSlice.length) {
+      compBytes = headSlice.subarray(compStartInSlice, compStartInSlice + compLen);
+    } else {
+      const compStart = start + compStartInSlice;
+      compBytes = new Uint8Array(
+        await this.blob.slice(compStart, compStart + compLen).arrayBuffer(),
+      );
+    }
+    const raw = unzlibSync(compBytes);
+    const out = { contentTypeIds: ctIds.slice(), raw };
+    this.binCache.set(binIndex, out);
+    return out;
+  }
+
+  /** Read the blob referenced by a ref. */
+  async readBlob(ref: SlobRef): Promise {
+    const bin = await this.getBin(ref.binIndex);
+    const itemCount = bin.contentTypeIds.length;
+    if (ref.itemIndex >= itemCount) {
+      throw new Error(`Slob item_index ${ref.itemIndex} out of range (bin has ${itemCount})`);
+    }
+    // Decompressed bin: u32 BE positions × itemCount, then items.
+    const pos = readU32BE(bin.raw, ref.itemIndex * 4);
+    const dataOffset = itemCount * 4;
+    const itemStart = dataOffset + pos;
+    const itemLen = readU32BE(bin.raw, itemStart);
+    const data = bin.raw.subarray(itemStart + 4, itemStart + 4 + itemLen);
+
+    const ctId = bin.contentTypeIds[ref.itemIndex]!;
+    const contentType = this.header.contentTypes[ctId] ?? '';
+    return { contentType, data };
+  }
+
+  /** Number of refs — exposed for tests. */
+  get refsCount(): number {
+    return this.refCount;
+  }
+}
+
+/** Strip trailing NULs from header tag values (slob editable-padding). */
+export const cleanTag = (v: string): string => v.replace(/\0+$/u, '');
diff --git a/apps/readest-app/src/services/dictionaries/stardictReader.ts b/apps/readest-app/src/services/dictionaries/stardictReader.ts
index 978c472a..d42b0534 100644
--- a/apps/readest-app/src/services/dictionaries/stardictReader.ts
+++ b/apps/readest-app/src/services/dictionaries/stardictReader.ts
@@ -53,7 +53,7 @@
  *   Init heap after:  ~420 KB (idx offsets) + chunk metadata (~few KB)
  *                     + LRU chunk cache (≤ ~1 MB after warmup).
  */
-import { gunzipSync, Inflate } from 'fflate';
+import { LRU, loadDictBody, type DictBody } from './dictZip';
 
 export interface StarDictEntry {
   word: string;
@@ -63,11 +63,6 @@ export interface StarDictEntry {
 
 const decoder = new TextDecoder('utf-8');
 
-const GZIP_MAGIC = [0x1f, 0x8b];
-
-const isGzip = (bytes: Uint8Array): boolean =>
-  bytes.length >= 2 && bytes[0] === GZIP_MAGIC[0] && bytes[1] === GZIP_MAGIC[1];
-
 /** Parse the key=value `.ifo` text into a record. */
 export function parseIfo(text: string): Record {
   const out: Record = {};
@@ -161,200 +156,9 @@ const cmpAscii = (a: string, b: string): number => {
   return x < y ? -1 : x > y ? 1 : 0;
 };
 
-/**
- * Bounded LRU. Tiny enough we don't bother with a real linked-list
- * implementation; reusing the Map insertion order is fine.
- */
-class LRU {
-  private readonly max: number;
-  private readonly map = new Map();
-
-  constructor(max: number) {
-    this.max = max;
-  }
-
-  get(key: K): V | undefined {
-    const v = this.map.get(key);
-    if (v !== undefined) {
-      // Move to end (most-recently-used).
-      this.map.delete(key);
-      this.map.set(key, v);
-    }
-    return v;
-  }
-
-  set(key: K, value: V): void {
-    if (this.map.has(key)) this.map.delete(key);
-    this.map.set(key, value);
-    if (this.map.size > this.max) {
-      // Evict oldest.
-      const first = this.map.keys().next().value as K | undefined;
-      if (first !== undefined) this.map.delete(first);
-    }
-  }
-}
-
-// ---------------------------------------------------------------------------
-// DictZip parsing + lazy chunk decompression.
-//
-// A `.dict.dz` file is a standard gzip with a FEXTRA "RA" subfield
-// listing the compressed length of each chunk. Each chunk's uncompressed
-// length is exactly `chlen` (the last may be shorter). Chunks are
-// separated by `Z_FULL_FLUSH` (BFINAL=0 + sync marker), so each chunk's
-// deflate stream is non-terminating — `inflateSync` rejects them with
-// "unexpected EOF". fflate's streaming `Inflate.push(bytes, false)`
-// handles them correctly and emits chunk bytes via `ondata`.
-// ---------------------------------------------------------------------------
-
-interface DictZipMeta {
-  /** Uncompressed bytes per chunk (last may be shorter). */
-  chlen: number;
-  /** Compressed bytes per chunk; sums to `compressedDataSize`. */
-  chunkSizes: number[];
-  /** Byte offset within the file where compressed chunk data begins. */
-  compressedDataOffset: number;
-}
-
-/**
- * Parse the gzip header + FEXTRA RA subfield. Returns `null` for
- * non-gzip files or gzip files without an RA subfield (those need
- * whole-file gunzip).
- */
-function parseDictZipHeader(bytes: Uint8Array): DictZipMeta | null {
-  if (bytes.length < 12 || bytes[0] !== 0x1f || bytes[1] !== 0x8b || bytes[2] !== 0x08) {
-    return null;
-  }
-  const flg = bytes[3]!;
-  const hasFEXTRA = (flg & 0b100) !== 0;
-  if (!hasFEXTRA) return null;
-
-  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
-  const xlen = view.getUint16(10, true);
-  let chlen = 0;
-  let chcnt = 0;
-  const chunkSizes: number[] = [];
-
-  let p = 12;
-  while (p + 4 <= 12 + xlen) {
-    const si1 = bytes[p]!;
-    const si2 = bytes[p + 1]!;
-    const slen = view.getUint16(p + 2, true);
-    if (si1 === 0x52 && si2 === 0x41) {
-      // RA subfield: ver(2) + chlen(2) + chcnt(2) + chcnt × chunkSize(2).
-      const ver = view.getUint16(p + 4, true);
-      if (ver !== 1) return null;
-      chlen = view.getUint16(p + 6, true);
-      chcnt = view.getUint16(p + 8, true);
-      for (let i = 0; i < chcnt; i++) {
-        chunkSizes.push(view.getUint16(p + 10 + 2 * i, true));
-      }
-    }
-    p += 4 + slen;
-  }
-  if (chcnt === 0 || chunkSizes.length !== chcnt) return null;
-
-  // Skip past FEXTRA + optional FNAME, FCOMMENT, FHCRC.
-  let offset = 12 + xlen;
-  if (flg & 0b1000) {
-    while (offset < bytes.length && bytes[offset] !== 0) offset++;
-    offset++;
-  }
-  if (flg & 0b10000) {
-    while (offset < bytes.length && bytes[offset] !== 0) offset++;
-    offset++;
-  }
-  if (flg & 0b10) offset += 2;
-
-  return { chlen, chunkSizes, compressedDataOffset: offset };
-}
-
-/**
- * Inflate a single DictZip chunk via fflate's streaming `Inflate`. The
- * chunk's deflate stream ends at `Z_FULL_FLUSH` (BFINAL=0 — `inflateSync`
- * would reject), so we push with `final=false` and capture the data
- * emitted by `ondata`. Returns `null` on failure.
- */
-function inflateChunkStreaming(chunkBytes: Uint8Array): Uint8Array | null {
-  let result: Uint8Array | null = null;
-  try {
-    const inf = new Inflate();
-    inf.ondata = (data, _final) => {
-      // The first `ondata` carries this chunk's uncompressed bytes; any
-      // subsequent calls would be from feeding further input (we don't).
-      if (result === null) result = data;
-    };
-    inf.push(chunkBytes, false);
-  } catch {
-    return null;
-  }
-  if (!result || (result as Uint8Array).length === 0) return null;
-  return result;
-}
-
-class DictZipChunkedDict {
-  private blob: Blob;
-  private meta: DictZipMeta;
-  private chunkOffsets: number[];
-  /** Total uncompressed dict size, derived from chunk count × chlen (last chunk may be shorter; we don't know its exact size yet). */
-  private chunkCache: LRU;
-
-  constructor(blob: Blob, meta: DictZipMeta, cacheSize: number) {
-    this.blob = blob;
-    this.meta = meta;
-    this.chunkCache = new LRU(cacheSize);
-    // Precompute starting compressed offset of each chunk for slice reads.
-    const offsets: number[] = [];
-    let acc = meta.compressedDataOffset;
-    for (const cs of meta.chunkSizes) {
-      offsets.push(acc);
-      acc += cs;
-    }
-    this.chunkOffsets = offsets;
-  }
-
-  /** Read `size` uncompressed bytes starting at `offset`. */
-  async read(offset: number, size: number): Promise {
-    const chlen = this.meta.chlen;
-    const startChunk = Math.floor(offset / chlen);
-    const endChunk = Math.floor((offset + size - 1) / chlen);
-
-    if (startChunk === endChunk) {
-      const chunk = await this.getChunk(startChunk);
-      const local = offset - startChunk * chlen;
-      return chunk.subarray(local, local + size);
-    }
-    // Spans multiple chunks — concatenate.
-    const parts: Uint8Array[] = [];
-    let totalLen = 0;
-    for (let i = startChunk; i <= endChunk; i++) {
-      const chunk = await this.getChunk(i);
-      parts.push(chunk);
-      totalLen += chunk.length;
-    }
-    const combined = new Uint8Array(totalLen);
-    let pos = 0;
-    for (const p of parts) {
-      combined.set(p, pos);
-      pos += p.length;
-    }
-    const local = offset - startChunk * chlen;
-    return combined.subarray(local, local + size);
-  }
-
-  private async getChunk(i: number): Promise {
-    const cached = this.chunkCache.get(i);
-    if (cached) return cached;
-    const start = this.chunkOffsets[i]!;
-    const compressedSize = this.meta.chunkSizes[i]!;
-    const compressed = new Uint8Array(
-      await this.blob.slice(start, start + compressedSize).arrayBuffer(),
-    );
-    const inflated = inflateChunkStreaming(compressed);
-    if (!inflated) throw new Error(`Failed to inflate DictZip chunk ${i}`);
-    this.chunkCache.set(i, inflated);
-    return inflated;
-  }
-}
+// DictZip parsing + lazy chunk decompression has been extracted to
+// `dictZip.ts` so the DICT (dictd) reader can share the same code path. The
+// `loadDictBody` factory below replaces the old per-class probe + fallback.
 
 export interface StarDictReaderOpts {
   ifo: Blob;
@@ -377,12 +181,10 @@ export interface StarDictReaderOpts {
 export class StarDictReader {
   ifo: Record = {};
 
-  // Dict-reading mode. Exactly one of these is populated at init:
-  //   - `dictChunked`: lazy chunk decompression (proper DictZip files).
-  //   - `dictBuffer`: whole-file gunzip in memory (fallback when FEXTRA
-  //     is absent or chunk-probe fails).
-  private dictChunked: DictZipChunkedDict | null = null;
-  private dictBuffer: Uint8Array = new Uint8Array();
+  // Dict body — a `DictBody` from `dictZip.ts` that internally chooses lazy
+  // chunk decompression for proper DictZip files and falls back to whole-
+  // file gunzip when the FEXTRA is absent or the probe fails.
+  private body: DictBody | null = null;
 
   // .idx state — populated eagerly at init.
   private idxBlob: Blob | null = null;
@@ -414,17 +216,10 @@ export class StarDictReader {
       this.synCache = new LRU(cacheSize);
     }
 
-    // Read the small files in parallel. We deliberately skip
-    // `opts.dict.arrayBuffer()` here — the dict is read in fragments
-    // below depending on whether lazy chunk mode is viable.
-    const [ifoBuf, idxOffsetsBuf, synOffsetsBuf, dictHeadBuf] = await Promise.all([
+    const [ifoBuf, idxOffsetsBuf, synOffsetsBuf] = await Promise.all([
       opts.ifo.arrayBuffer(),
       opts.idxOffsets ? opts.idxOffsets.arrayBuffer() : Promise.resolve(undefined),
       opts.synOffsets ? opts.synOffsets.arrayBuffer() : Promise.resolve(undefined),
-      // Read just the gzip header + FEXTRA region (much less than the
-      // whole file). 64 KB is enormously generous for a FEXTRA RA
-      // subfield — even thousands of chunks would fit in a few KB.
-      opts.dict.slice(0, Math.min(opts.dict.size, 64 * 1024)).arrayBuffer(),
     ]);
 
     this.ifo = parseIfo(decoder.decode(new Uint8Array(ifoBuf)));
@@ -448,27 +243,9 @@ export class StarDictReader {
     this.idxCount = idxOffsets.length;
     this.idxBlob = opts.idx;
 
-    // Try lazy chunk mode for `.dict.dz`. Parse FEXTRA, probe-inflate
-    // chunk 0; on success, retain only the metadata + Blob. On failure,
-    // fall back to whole-file gunzip.
-    const dictHead = new Uint8Array(dictHeadBuf);
-    if (isGzip(dictHead)) {
-      const meta = parseDictZipHeader(dictHead);
-      if (meta && (await this.probeChunkInflate(opts.dict, meta))) {
-        this.dictChunked = new DictZipChunkedDict(
-          opts.dict,
-          meta,
-          /* chunk LRU size */ Math.max(8, Math.floor(cacheSize / 16)),
-        );
-      } else {
-        // FEXTRA missing or chunk probe failed — gunzip the whole file.
-        const dictBytes = new Uint8Array(await opts.dict.arrayBuffer());
-        this.dictBuffer = gunzipSync(dictBytes);
-      }
-    } else {
-      // Raw .dict (no compression): keep bytes as-is.
-      this.dictBuffer = new Uint8Array(await opts.dict.arrayBuffer());
-    }
+    this.body = await loadDictBody(opts.dict, {
+      chunkCacheSize: Math.max(8, Math.floor(cacheSize / 16)),
+    });
 
     // .syn: keep the Blob, accept its sidecar eagerly if provided. If not,
     // the offset table is built lazily on first synonym fallback.
@@ -485,32 +262,10 @@ export class StarDictReader {
     }
   }
 
-  /**
-   * Read chunk 0 of a candidate DictZip and try to inflate it via the
-   * streaming inflater. If that succeeds and produces ≤ chlen bytes, the
-   * file's chunks are independently decompressible — lazy mode is viable.
-   */
-  private async probeChunkInflate(blob: Blob, meta: DictZipMeta): Promise {
-    if (meta.chunkSizes.length === 0) return false;
-    const cs = meta.chunkSizes[0]!;
-    const start = meta.compressedDataOffset;
-    const compressed = new Uint8Array(await blob.slice(start, start + cs).arrayBuffer());
-    const inflated = inflateChunkStreaming(compressed);
-    return !!inflated && inflated.length > 0 && inflated.length <= meta.chlen;
-  }
-
-  /**
-   * Resolve an entry's bytes from the dict.
-   *
-   * Lazy-chunk mode: reads the chunks containing the entry's uncompressed
-   * range from the dict Blob and streaming-inflates them.
-   * Whole-file mode: in-memory subarray.
-   */
+  /** Resolve an entry's bytes from the dict body. */
   async read(entry: StarDictEntry): Promise {
-    if (this.dictChunked) {
-      return this.dictChunked.read(entry.offset, entry.size);
-    }
-    return this.dictBuffer.subarray(entry.offset, entry.offset + entry.size);
+    if (!this.body) throw new Error('dict body not loaded');
+    return this.body.read(entry.offset, entry.size);
   }
 
   /** Number of entries — exposed for tests. */
diff --git a/apps/readest-app/src/services/dictionaries/types.ts b/apps/readest-app/src/services/dictionaries/types.ts
index 87b9f5ac..e1963ad7 100644
--- a/apps/readest-app/src/services/dictionaries/types.ts
+++ b/apps/readest-app/src/services/dictionaries/types.ts
@@ -7,7 +7,7 @@
  * order; each provider writes lookup output into a per-tab container.
  */
 
-export type DictionaryProviderKind = 'builtin' | 'stardict' | 'mdict';
+export type DictionaryProviderKind = 'builtin' | 'stardict' | 'mdict' | 'dict' | 'slob' | 'web';
 
 export interface DictionaryLookupContext {
   /** Source language hint, e.g. book primary language code (`en`, `zh`). */
@@ -48,13 +48,14 @@ export interface DictionaryProvider {
  */
 export interface ImportedDictionary {
   id: string;
-  kind: 'stardict' | 'mdict';
-  /** Display name, derived from `.ifo` `bookname` or `.mdx` header `Title`. */
+  kind: 'stardict' | 'mdict' | 'dict' | 'slob';
+  /** Display name, derived from `.ifo` `bookname`, `.mdx` `Title`, slob `label`, or DICT `00databaseshort`. */
   name: string;
   /** Subdirectory under `'Dictionaries'` containing this bundle's files. */
   bundleDir: string;
   /** Filenames inside `bundleDir`. The exact set varies by `kind`. */
   files: {
+    // StarDict bundle.
     ifo?: string;
     idx?: string;
     dict?: string;
@@ -67,8 +68,14 @@ export interface ImportedDictionary {
     idxOffsets?: string;
     /** Same idea for `.syn`. */
     synOffsets?: string;
+    // MDict bundle.
     mdx?: string;
     mdd?: string[];
+    // DICT (dictd) bundle. `dict` above doubles as the body filename
+    // (`name.dict` or `name.dict.dz`); `index` is the dictd `.index` file.
+    index?: string;
+    // Slob bundle: a single self-contained `.slob` file.
+    slob?: string;
   };
   /** Source language code if known. */
   lang?: string;
@@ -92,6 +99,28 @@ export interface ImportedDictionary {
   unsupportedReason?: string;
 }
 
+/**
+ * A web-search "provider" template — a URL with a `%WORD%` placeholder
+ * (URL-encoded substitution at lookup time). Built-in templates (Google,
+ * Urban Dictionary, Merriam-Webster) are hardcoded in the registry and
+ * reference the IDs in {@link BUILTIN_WEB_SEARCH_IDS}; user-added templates
+ * live in {@link DictionarySettings.webSearches} with IDs of the form
+ * `web:`.
+ *
+ * Web-search providers don't fetch upstream — the popup just renders an
+ * "Open in [name]" link that opens the resolved URL externally. Iframe
+ * embedding is blocked by every major dictionary site (X-Frame-Options).
+ */
+export interface WebSearchEntry {
+  id: string;
+  /** Display name shown in the tab strip and the settings list. */
+  name: string;
+  /** URL with `%WORD%` placeholder, e.g. `https://example.com/?q=%WORD%`. */
+  urlTemplate: string;
+  /** Soft-delete marker; only set on user-added entries. */
+  deletedAt?: number;
+}
+
 export interface DictionarySettings {
   /** Provider id order shown in the popup tab strip. Includes builtin ids. */
   providerOrder: string[];
@@ -99,6 +128,11 @@ export interface DictionarySettings {
   providerEnabled: Record;
   /** Last-used tab id; `undefined` falls back to first enabled provider. */
   defaultProviderId?: string;
+  /**
+   * User-defined web search templates. Built-in templates (Google, Urban,
+   * Merriam-Webster) are hardcoded in the registry and not stored here.
+   */
+  webSearches?: WebSearchEntry[];
 }
 
 /** Stable ids for the built-in providers. */
@@ -108,3 +142,18 @@ export const BUILTIN_PROVIDER_IDS = {
 } as const;
 
 export type BuiltinProviderId = (typeof BUILTIN_PROVIDER_IDS)[keyof typeof BUILTIN_PROVIDER_IDS];
+
+/**
+ * Stable ids for the built-in web-search templates. The `web:builtin:*`
+ * prefix lets the registry recognize and dispatch them without a settings
+ * lookup; user-added templates live in `settings.webSearches` with ids of
+ * the form `web:`.
+ */
+export const BUILTIN_WEB_SEARCH_IDS = {
+  google: 'web:builtin:google',
+  urban: 'web:builtin:urban',
+  merriamWebster: 'web:builtin:merriam-webster',
+} as const;
+
+export type BuiltinWebSearchId =
+  (typeof BUILTIN_WEB_SEARCH_IDS)[keyof typeof BUILTIN_WEB_SEARCH_IDS];
diff --git a/apps/readest-app/src/services/dictionaries/webSearchTemplates.ts b/apps/readest-app/src/services/dictionaries/webSearchTemplates.ts
new file mode 100644
index 00000000..b30d2fbd
--- /dev/null
+++ b/apps/readest-app/src/services/dictionaries/webSearchTemplates.ts
@@ -0,0 +1,63 @@
+/**
+ * Built-in web-search templates.
+ *
+ * Three universally-useful entries (Google, Urban Dictionary,
+ * Merriam-Webster). They are seeded into `providerOrder` but **disabled by
+ * default** — users opt in from the settings list. Custom URL templates
+ * live in `settings.webSearches`; this file is only the immutable
+ * built-ins.
+ *
+ * The `%WORD%` placeholder marks where the looked-up word goes; the
+ * provider URL-encodes it at substitution time. (GoldenDict users may know
+ * `%GDWORD%` from there — we use `%WORD%` for brevity, but accept neither
+ * verbatim variant beyond what's documented here.)
+ */
+import { BUILTIN_WEB_SEARCH_IDS, type WebSearchEntry } from './types';
+
+export interface BuiltinWebSearchTemplate extends WebSearchEntry {
+  /** Localizable display name key — passed through `_(...)` at render time. */
+  nameKey: string;
+}
+
+export const BUILTIN_WEB_SEARCHES: BuiltinWebSearchTemplate[] = [
+  {
+    id: BUILTIN_WEB_SEARCH_IDS.google,
+    name: 'Google',
+    nameKey: 'Google',
+    urlTemplate: 'https://www.google.com/search?q=define:%WORD%&hl=en',
+  },
+  {
+    id: BUILTIN_WEB_SEARCH_IDS.urban,
+    name: 'Urban Dictionary',
+    nameKey: 'Urban Dictionary',
+    urlTemplate: 'https://www.urbandictionary.com/define.php?term=%WORD%',
+  },
+  {
+    id: BUILTIN_WEB_SEARCH_IDS.merriamWebster,
+    name: 'Merriam-Webster',
+    nameKey: 'Merriam-Webster',
+    urlTemplate: 'https://www.merriam-webster.com/dictionary/%WORD%',
+  },
+];
+
+const BUILTIN_BY_ID = new Map(BUILTIN_WEB_SEARCHES.map((t) => [t.id, t]));
+
+export const getBuiltinWebSearch = (id: string): BuiltinWebSearchTemplate | undefined =>
+  BUILTIN_BY_ID.get(id);
+
+/**
+ * Substitute `%WORD%` (case-insensitive) in a URL template with the
+ * URL-encoded form of `word`. Also handles the doubly-escaped form
+ * `%25WORD%25` that may appear if a user pasted a URL through a tool that
+ * percent-encoded the placeholder.
+ */
+export const substituteUrlTemplate = (template: string, word: string): string => {
+  const encoded = encodeURIComponent(word);
+  return template.replace(/%25WORD%25/gi, encoded).replace(/%WORD%/gi, encoded);
+};
+
+/** Returns true if a template contains a usable `%WORD%` placeholder. */
+export const isValidUrlTemplate = (template: string): boolean => {
+  if (!/^https?:\/\//i.test(template.trim())) return false;
+  return /%WORD%/i.test(template) || /%25WORD%25/i.test(template);
+};
diff --git a/apps/readest-app/src/store/customDictionaryStore.ts b/apps/readest-app/src/store/customDictionaryStore.ts
index 6a2f8f34..1f03143b 100644
--- a/apps/readest-app/src/store/customDictionaryStore.ts
+++ b/apps/readest-app/src/store/customDictionaryStore.ts
@@ -1,15 +1,39 @@
 import { create } from 'zustand';
 import { EnvConfigType } from '@/services/environment';
-import type { DictionarySettings, ImportedDictionary } from '@/services/dictionaries/types';
-import { BUILTIN_PROVIDER_IDS } from '@/services/dictionaries/types';
+import type {
+  DictionarySettings,
+  ImportedDictionary,
+  WebSearchEntry,
+} from '@/services/dictionaries/types';
+import { BUILTIN_PROVIDER_IDS, BUILTIN_WEB_SEARCH_IDS } from '@/services/dictionaries/types';
 import { useSettingsStore } from './settingsStore';
 
+/**
+ * Built-in web-search ids are seeded into `providerOrder` but disabled by
+ * default — users opt in. This preserves the principle that we don't push
+ * users onto external pages without consent, while still surfacing the
+ * options in the settings list.
+ */
+const BUILTIN_WEB_ORDER = [
+  BUILTIN_WEB_SEARCH_IDS.google,
+  BUILTIN_WEB_SEARCH_IDS.urban,
+  BUILTIN_WEB_SEARCH_IDS.merriamWebster,
+];
+
 const DEFAULT_DICTIONARY_SETTINGS: DictionarySettings = {
-  providerOrder: [BUILTIN_PROVIDER_IDS.wiktionary, BUILTIN_PROVIDER_IDS.wikipedia],
+  providerOrder: [
+    BUILTIN_PROVIDER_IDS.wiktionary,
+    BUILTIN_PROVIDER_IDS.wikipedia,
+    ...BUILTIN_WEB_ORDER,
+  ],
   providerEnabled: {
     [BUILTIN_PROVIDER_IDS.wiktionary]: true,
     [BUILTIN_PROVIDER_IDS.wikipedia]: true,
+    [BUILTIN_WEB_SEARCH_IDS.google]: false,
+    [BUILTIN_WEB_SEARCH_IDS.urban]: false,
+    [BUILTIN_WEB_SEARCH_IDS.merriamWebster]: false,
   },
+  webSearches: [],
 };
 
 interface DictionaryStoreState {
@@ -32,6 +56,13 @@ interface DictionaryStoreState {
   /** Persist the last-used tab id so the popup re-opens on it. */
   setDefaultProviderId(id: string | undefined): void;
 
+  /** Add a custom web search (id is generated). Appended + enabled by default. */
+  addWebSearch(name: string, urlTemplate: string): WebSearchEntry;
+  /** Update an existing custom web search; no-op if id is unknown or built-in. */
+  updateWebSearch(id: string, patch: { name?: string; urlTemplate?: string }): void;
+  /** Soft-delete a custom web search and remove from order/enabled. */
+  removeWebSearch(id: string): boolean;
+
   /** Hydrate from `settings.customDictionaries` + `settings.dictionarySettings` + check on-disk availability. */
   loadCustomDictionaries(envConfig: EnvConfigType): Promise;
   /** Persist current state back into settings (which then syncs to cloud). */
@@ -123,6 +154,66 @@ export const useCustomDictionaryStore = create((set, get)
     }));
   },
 
+  addWebSearch: (name, urlTemplate) => {
+    const trimmedName = name.trim();
+    const trimmedUrl = urlTemplate.trim();
+    const id = `web:${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
+    const entry: WebSearchEntry = { id, name: trimmedName, urlTemplate: trimmedUrl };
+    set((state) => {
+      const list = state.settings.webSearches ?? [];
+      const order = state.settings.providerOrder.includes(id)
+        ? state.settings.providerOrder
+        : [...state.settings.providerOrder, id];
+      const enabled = { ...state.settings.providerEnabled, [id]: true };
+      return {
+        settings: {
+          ...state.settings,
+          webSearches: [...list, entry],
+          providerOrder: order,
+          providerEnabled: enabled,
+        },
+      };
+    });
+    return entry;
+  },
+
+  updateWebSearch: (id, patch) => {
+    if (id.startsWith('web:builtin:')) return;
+    set((state) => {
+      const list = state.settings.webSearches ?? [];
+      if (!list.some((t) => t.id === id)) return state;
+      const next = list.map((t) =>
+        t.id === id
+          ? {
+              ...t,
+              name: patch.name?.trim() ?? t.name,
+              urlTemplate: patch.urlTemplate?.trim() ?? t.urlTemplate,
+            }
+          : t,
+      );
+      return { settings: { ...state.settings, webSearches: next } };
+    });
+  },
+
+  removeWebSearch: (id) => {
+    if (id.startsWith('web:builtin:')) return false;
+    const list = get().settings.webSearches ?? [];
+    if (!list.some((t) => t.id === id)) return false;
+    set((state) => ({
+      settings: {
+        ...state.settings,
+        webSearches: (state.settings.webSearches ?? []).map((t) =>
+          t.id === id ? { ...t, deletedAt: Date.now() } : t,
+        ),
+        providerOrder: state.settings.providerOrder.filter((p) => p !== id),
+        providerEnabled: Object.fromEntries(
+          Object.entries(state.settings.providerEnabled).filter(([k]) => k !== id),
+        ),
+      },
+    }));
+    return true;
+  },
+
   loadCustomDictionaries: async (envConfig) => {
     try {
       const { settings } = useSettingsStore.getState();
@@ -137,15 +228,25 @@ export const useCustomDictionaryStore = create((set, get)
         }),
       );
       // Merge defaults to back-fill any missing keys (e.g. new builtin added in a release).
+      // For providerOrder, we append any newly-defaulted ids (like the
+      // built-in web searches added in this release) so existing users see
+      // them appear at the end of the list.
+      const persistedOrder = persistedSettings.providerOrder;
+      const orderSet = new Set(persistedOrder);
+      const merged: string[] = persistedOrder.length
+        ? [...persistedOrder]
+        : [...DEFAULT_DICTIONARY_SETTINGS.providerOrder];
+      for (const id of DEFAULT_DICTIONARY_SETTINGS.providerOrder) {
+        if (!orderSet.has(id)) merged.push(id);
+      }
       const settingsMerged: DictionarySettings = {
-        providerOrder: persistedSettings.providerOrder.length
-          ? persistedSettings.providerOrder
-          : DEFAULT_DICTIONARY_SETTINGS.providerOrder,
+        providerOrder: merged,
         providerEnabled: {
           ...DEFAULT_DICTIONARY_SETTINGS.providerEnabled,
           ...persistedSettings.providerEnabled,
         },
         defaultProviderId: persistedSettings.defaultProviderId,
+        webSearches: persistedSettings.webSearches ?? [],
       };
       set({ dictionaries, settings: settingsMerged });
     } catch (error) {