feat(dict): faster MDict/StarDict import + lazy lookup; raw .dict; UX (#4334)
Make the dictionary import path usable on large bundles and bring the multi-device flow up to par. Import perf - Skip `MDX.create()` at import time. The factory triggers full init — decompresses every key block and sorts millions of keys with localeCompare just to expose the header. Replace with a tiny `readMdxHeader()` that only reads the small XML header for Title / Encoding / Encrypted (saves ~17 s on a 250 MB MDX on web). - `partialMD5`: read the 9 sample slices in parallel rather than sequentially. Each freshly-picked-File slice round-trip on Chrome costs ~100 ms cold; parallelisation collapses 9 of them into one. - Native fast-path in `nativeAppService.writeFile`: when the source is a `NativeFile`, delegate to Tauri's `copyFile` rather than streaming the file through `NativeFile.stream()`. Streaming a 250 MB body through 1 MB IPC chunks on Android took ~100 s; native copy is bound by disk throughput instead. Exposes `NativeFile.getNativeLocation()` for the FS layer to use the underlying path + baseDir directly. Lazy lookup - Pass `lazy: true` to `MDX.create` / `MDD.create`. The js-mdict change in this PR skips the upfront decompress-every-block + sort during init (~80 s on the same 250 MB bundle) and decodes only the relevant key block on demand per lookup. First-lookup main-thread block drops from ~81 s to ~230 ms. (Closes #4228.) Raw .dict - Drop the import-time gate that flagged non-gzip dict bodies as `unsupported`. The runtime body loader (`loadDictBody`) already probes the gzip header and falls through to a passthrough buffer for raw files, so the gate was the only thing preventing raw `.dict` bundles from importing on devices that received them via cloud sync. (Closes #4179, partially addresses #4248.) Import-flow UX - `handleImport` now always surfaces a toast for every non-cancelled attempt: picker errors, missing app service, no-op imports, and unsupported-but-imported bundles each get their own message instead of failing silently. - Call `markAvailableByContentId(newDict.contentId)` after add/replace so the "Bundle is missing on this device" warning clears immediately — no need to close-and-reopen the panel. System Dictionary - Drop the cascading toggle behavior in `setEnabled`. Each provider's enabled flag persists independently; exclusivity is enforced at lookup time. Toggling System on/off no longer wipes the user's preferred set of in-app providers. - Render non-system rows as read-only when System is on (toggle still shows what's queued to restore; tooltip explains the lock). - `isSystemDictionaryEnabled` short-circuits to `false` on platforms where the handoff isn't implemented. `providerEnabled` is whole- field synced across devices, so a flag set on macOS would otherwise leak to a Windows device with no way to look up a word. js-mdict - Submodule bump to e6dbc99 which adds the opt-in `lazy: true` `MDictOptions` flag (skip `_readKeyBlocks` + post-init sort; new `lookupKeyBlockByWordLazy` path on `MDX` and `MDD`). Eager mode is unchanged and every existing js-mdict test still passes. i18n - 208 new translations across 33 locales for the new UX strings. Closes #4228 Closes #4248 Closes #4179 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1760,5 +1760,15 @@
|
||||
"Send Reedy feedback": "إرسال ملاحظات Reedy",
|
||||
"OpenRouter API Key": "مفتاح OpenRouter API",
|
||||
"OpenRouter Base URL": "رابط OpenRouter الأساسي",
|
||||
"OpenRouter Model": "نموذج OpenRouter"
|
||||
"OpenRouter Model": "نموذج OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "قم بتعطيل قاموس النظام أولاً لتغيير هذا.",
|
||||
"App service is not available": "خدمة التطبيق غير متوفرة",
|
||||
"Unsupported dictionary: {{reason}}": "قاموس غير مدعوم: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_zero": "لا توجد قواميس غير مدعومة ومعطلة",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "قاموس واحد غير مدعوم ومعطل",
|
||||
"{{count}} dictionary is unsupported and disabled_two": "قاموسان غير مدعومين ومعطلان",
|
||||
"{{count}} dictionary is unsupported and disabled_few": "{{count}} قواميس غير مدعومة ومعطلة",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} قاموساً غير مدعوماً ومعطلاً",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} قاموس غير مدعوم ومعطل",
|
||||
"No new dictionaries were imported": "لم يتم استيراد أي قواميس جديدة"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy ফিডব্যাক পাঠান",
|
||||
"OpenRouter API Key": "OpenRouter API কী",
|
||||
"OpenRouter Base URL": "OpenRouter বেস URL",
|
||||
"OpenRouter Model": "OpenRouter মডেল"
|
||||
"OpenRouter Model": "OpenRouter মডেল",
|
||||
"Disable System Dictionary first to change this.": "এটি পরিবর্তন করতে প্রথমে সিস্টেম অভিধান নিষ্ক্রিয় করুন।",
|
||||
"App service is not available": "অ্যাপ পরিষেবা উপলব্ধ নয়",
|
||||
"Unsupported dictionary: {{reason}}": "অসমর্থিত অভিধান: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}}টি অভিধান অসমর্থিত এবং নিষ্ক্রিয়",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}}টি অভিধান অসমর্থিত এবং নিষ্ক্রিয়",
|
||||
"No new dictionaries were imported": "কোনো নতুন অভিধান আমদানি করা হয়নি"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "Reedy ལ་བསམ་ཚུལ་གཏོང་",
|
||||
"OpenRouter API Key": "OpenRouter API ལྡེ་མིག",
|
||||
"OpenRouter Base URL": "OpenRouter རྩ་བའི་ URL",
|
||||
"OpenRouter Model": "OpenRouter དཔེ་གཟུགས"
|
||||
"OpenRouter Model": "OpenRouter དཔེ་གཟུགས",
|
||||
"Disable System Dictionary first to change this.": "འདི་བརྗེ་བཅོས་བྱེད་པར་ཐོག་མར་མ་ལག་ཚིག་མཛོད་སྤོ་ཟུར།",
|
||||
"App service is not available": "ཉེར་སྤྱོད་ཞབས་ཞུ་ཐོབ་མི་ཐུབ།",
|
||||
"Unsupported dictionary: {{reason}}": "རྒྱབ་སྐྱོར་མེད་པའི་ཚིག་མཛོད། {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "ཚིག་མཛོད་ {{count}} ལ་རྒྱབ་སྐྱོར་མེད་ཅིང་སྤོ་ཟུར་བྱས་ཡོད།",
|
||||
"No new dictionaries were imported": "ཚིག་མཛོད་གསར་པ་ནང་འདྲེན་མ་བྱུང་།"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy-Feedback senden",
|
||||
"OpenRouter API Key": "OpenRouter-API-Schlüssel",
|
||||
"OpenRouter Base URL": "OpenRouter-Basis-URL",
|
||||
"OpenRouter Model": "OpenRouter-Modell"
|
||||
"OpenRouter Model": "OpenRouter-Modell",
|
||||
"Disable System Dictionary first to change this.": "Deaktivieren Sie zuerst das Systemwörterbuch, um dies zu ändern.",
|
||||
"App service is not available": "App-Dienst ist nicht verfügbar",
|
||||
"Unsupported dictionary: {{reason}}": "Nicht unterstütztes Wörterbuch: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} Wörterbuch wird nicht unterstützt und ist deaktiviert",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} Wörterbücher werden nicht unterstützt und sind deaktiviert",
|
||||
"No new dictionaries were imported": "Keine neuen Wörterbücher importiert"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Αποστολή σχολίων Reedy",
|
||||
"OpenRouter API Key": "Κλειδί API OpenRouter",
|
||||
"OpenRouter Base URL": "Βασικό URL OpenRouter",
|
||||
"OpenRouter Model": "Μοντέλο OpenRouter"
|
||||
"OpenRouter Model": "Μοντέλο OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Απενεργοποιήστε πρώτα το Λεξικό Συστήματος για να το αλλάξετε.",
|
||||
"App service is not available": "Η υπηρεσία εφαρμογής δεν είναι διαθέσιμη",
|
||||
"Unsupported dictionary: {{reason}}": "Μη υποστηριζόμενο λεξικό: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} λεξικό δεν υποστηρίζεται και είναι απενεργοποιημένο",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} λεξικά δεν υποστηρίζονται και είναι απενεργοποιημένα",
|
||||
"No new dictionaries were imported": "Δεν εισήχθησαν νέα λεξικά"
|
||||
}
|
||||
|
||||
@@ -1670,5 +1670,12 @@
|
||||
"Send Reedy feedback": "Enviar comentarios sobre Reedy",
|
||||
"OpenRouter API Key": "Clave de API de OpenRouter",
|
||||
"OpenRouter Base URL": "URL base de OpenRouter",
|
||||
"OpenRouter Model": "Modelo de OpenRouter"
|
||||
"OpenRouter Model": "Modelo de OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Desactiva primero el Diccionario del Sistema para cambiar esto.",
|
||||
"App service is not available": "El servicio de la aplicación no está disponible",
|
||||
"Unsupported dictionary: {{reason}}": "Diccionario no compatible: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} diccionario no es compatible y está deshabilitado",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} diccionarios no son compatibles y están deshabilitados",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} diccionarios no son compatibles y están deshabilitados",
|
||||
"No new dictionaries were imported": "No se importaron diccionarios nuevos"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "ارسال بازخورد Reedy",
|
||||
"OpenRouter API Key": "کلید API OpenRouter",
|
||||
"OpenRouter Base URL": "نشانی پایه OpenRouter",
|
||||
"OpenRouter Model": "مدل OpenRouter"
|
||||
"OpenRouter Model": "مدل OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "ابتدا فرهنگ لغت سیستم را غیرفعال کنید تا این را تغییر دهید.",
|
||||
"App service is not available": "سرویس برنامه در دسترس نیست",
|
||||
"Unsupported dictionary: {{reason}}": "فرهنگ لغت پشتیبانینشده: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} فرهنگ لغت پشتیبانی نمیشود و غیرفعال است",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} فرهنگ لغت پشتیبانی نمیشوند و غیرفعالاند",
|
||||
"No new dictionaries were imported": "هیچ فرهنگ لغت جدیدی وارد نشد"
|
||||
}
|
||||
|
||||
@@ -1670,5 +1670,12 @@
|
||||
"Send Reedy feedback": "Envoyer un commentaire Reedy",
|
||||
"OpenRouter API Key": "Clé API OpenRouter",
|
||||
"OpenRouter Base URL": "URL de base OpenRouter",
|
||||
"OpenRouter Model": "Modèle OpenRouter"
|
||||
"OpenRouter Model": "Modèle OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Désactivez d’abord le dictionnaire système pour modifier ceci.",
|
||||
"App service is not available": "Le service de l’application n’est pas disponible",
|
||||
"Unsupported dictionary: {{reason}}": "Dictionnaire non pris en charge : {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} dictionnaire n’est pas pris en charge et est désactivé",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} dictionnaires ne sont pas pris en charge et sont désactivés",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} dictionnaires ne sont pas pris en charge et sont désactivés",
|
||||
"No new dictionaries were imported": "Aucun nouveau dictionnaire n’a été importé"
|
||||
}
|
||||
|
||||
@@ -1670,5 +1670,12 @@
|
||||
"Send Reedy feedback": "שלח משוב Reedy",
|
||||
"OpenRouter API Key": "מפתח API של OpenRouter",
|
||||
"OpenRouter Base URL": "כתובת בסיס OpenRouter",
|
||||
"OpenRouter Model": "מודל OpenRouter"
|
||||
"OpenRouter Model": "מודל OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "השבת תחילה את מילון המערכת כדי לשנות זאת.",
|
||||
"App service is not available": "שירות האפליקציה אינו זמין",
|
||||
"Unsupported dictionary: {{reason}}": "מילון לא נתמך: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} מילון אינו נתמך ומושבת",
|
||||
"{{count}} dictionary is unsupported and disabled_two": "{{count}} מילונים אינם נתמכים ומושבתים",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} מילונים אינם נתמכים ומושבתים",
|
||||
"No new dictionaries were imported": "לא יובאו מילונים חדשים"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy फ़ीडबैक भेजें",
|
||||
"OpenRouter API Key": "OpenRouter API कुंजी",
|
||||
"OpenRouter Base URL": "OpenRouter बेस URL",
|
||||
"OpenRouter Model": "OpenRouter मॉडल"
|
||||
"OpenRouter Model": "OpenRouter मॉडल",
|
||||
"Disable System Dictionary first to change this.": "इसे बदलने के लिए पहले सिस्टम शब्दकोश को अक्षम करें।",
|
||||
"App service is not available": "ऐप सेवा उपलब्ध नहीं है",
|
||||
"Unsupported dictionary: {{reason}}": "असमर्थित शब्दकोश: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} शब्दकोश असमर्थित और अक्षम है",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} शब्दकोश असमर्थित और अक्षम हैं",
|
||||
"No new dictionaries were imported": "कोई नया शब्दकोश आयात नहीं किया गया"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy visszajelzés küldése",
|
||||
"OpenRouter API Key": "OpenRouter API-kulcs",
|
||||
"OpenRouter Base URL": "OpenRouter alap URL",
|
||||
"OpenRouter Model": "OpenRouter modell"
|
||||
"OpenRouter Model": "OpenRouter modell",
|
||||
"Disable System Dictionary first to change this.": "Először tiltsa le a rendszerszótárt a módosításhoz.",
|
||||
"App service is not available": "Az alkalmazás szolgáltatása nem érhető el",
|
||||
"Unsupported dictionary: {{reason}}": "Nem támogatott szótár: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} szótár nem támogatott, és le van tiltva",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} szótár nem támogatott, és le van tiltva",
|
||||
"No new dictionaries were imported": "Nem importáltunk új szótárakat"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "Kirim umpan balik Reedy",
|
||||
"OpenRouter API Key": "Kunci API OpenRouter",
|
||||
"OpenRouter Base URL": "URL Dasar OpenRouter",
|
||||
"OpenRouter Model": "Model OpenRouter"
|
||||
"OpenRouter Model": "Model OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Nonaktifkan Kamus Sistem terlebih dahulu untuk mengubah ini.",
|
||||
"App service is not available": "Layanan aplikasi tidak tersedia",
|
||||
"Unsupported dictionary: {{reason}}": "Kamus tidak didukung: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} kamus tidak didukung dan dinonaktifkan",
|
||||
"No new dictionaries were imported": "Tidak ada kamus baru yang diimpor"
|
||||
}
|
||||
|
||||
@@ -1670,5 +1670,12 @@
|
||||
"Send Reedy feedback": "Invia feedback Reedy",
|
||||
"OpenRouter API Key": "Chiave API OpenRouter",
|
||||
"OpenRouter Base URL": "URL di base OpenRouter",
|
||||
"OpenRouter Model": "Modello OpenRouter"
|
||||
"OpenRouter Model": "Modello OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Disattiva prima il Dizionario di Sistema per modificare questa opzione.",
|
||||
"App service is not available": "Il servizio dell’app non è disponibile",
|
||||
"Unsupported dictionary: {{reason}}": "Dizionario non supportato: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} dizionario non è supportato ed è disattivato",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} dizionari non sono supportati e sono disattivati",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} dizionari non sono supportati e sono disattivati",
|
||||
"No new dictionaries were imported": "Nessun nuovo dizionario importato"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "Reedy のフィードバックを送信",
|
||||
"OpenRouter API Key": "OpenRouter API キー",
|
||||
"OpenRouter Base URL": "OpenRouter ベース URL",
|
||||
"OpenRouter Model": "OpenRouter モデル"
|
||||
"OpenRouter Model": "OpenRouter モデル",
|
||||
"Disable System Dictionary first to change this.": "変更するにはまずシステム辞書を無効にしてください。",
|
||||
"App service is not available": "アプリサービスは利用できません",
|
||||
"Unsupported dictionary: {{reason}}": "非対応の辞書:{{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} 件の辞書が非対応のため無効になっています",
|
||||
"No new dictionaries were imported": "新しい辞書は読み込まれませんでした"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "Reedy 피드백 보내기",
|
||||
"OpenRouter API Key": "OpenRouter API 키",
|
||||
"OpenRouter Base URL": "OpenRouter 기본 URL",
|
||||
"OpenRouter Model": "OpenRouter 모델"
|
||||
"OpenRouter Model": "OpenRouter 모델",
|
||||
"Disable System Dictionary first to change this.": "변경하려면 먼저 시스템 사전을 비활성화하세요.",
|
||||
"App service is not available": "앱 서비스를 사용할 수 없습니다",
|
||||
"Unsupported dictionary: {{reason}}": "지원되지 않는 사전: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}}개의 사전이 지원되지 않아 비활성화되었습니다",
|
||||
"No new dictionaries were imported": "가져온 새 사전이 없습니다"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "Hantar maklum balas Reedy",
|
||||
"OpenRouter API Key": "Kunci API OpenRouter",
|
||||
"OpenRouter Base URL": "URL Asas OpenRouter",
|
||||
"OpenRouter Model": "Model OpenRouter"
|
||||
"OpenRouter Model": "Model OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Lumpuhkan Kamus Sistem dahulu untuk mengubahnya.",
|
||||
"App service is not available": "Perkhidmatan aplikasi tidak tersedia",
|
||||
"Unsupported dictionary: {{reason}}": "Kamus tidak disokong: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} kamus tidak disokong dan dilumpuhkan",
|
||||
"No new dictionaries were imported": "Tiada kamus baharu diimport"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy-feedback verzenden",
|
||||
"OpenRouter API Key": "OpenRouter-API-sleutel",
|
||||
"OpenRouter Base URL": "OpenRouter-basis-URL",
|
||||
"OpenRouter Model": "OpenRouter-model"
|
||||
"OpenRouter Model": "OpenRouter-model",
|
||||
"Disable System Dictionary first to change this.": "Schakel eerst het Systeemwoordenboek uit om dit te wijzigen.",
|
||||
"App service is not available": "App-service is niet beschikbaar",
|
||||
"Unsupported dictionary: {{reason}}": "Niet-ondersteund woordenboek: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} woordenboek wordt niet ondersteund en is uitgeschakeld",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} woordenboeken worden niet ondersteund en zijn uitgeschakeld",
|
||||
"No new dictionaries were imported": "Er zijn geen nieuwe woordenboeken geïmporteerd"
|
||||
}
|
||||
|
||||
@@ -1700,5 +1700,13 @@
|
||||
"Send Reedy feedback": "Wyślij opinię o Reedy",
|
||||
"OpenRouter API Key": "Klucz API OpenRouter",
|
||||
"OpenRouter Base URL": "Bazowy URL OpenRouter",
|
||||
"OpenRouter Model": "Model OpenRouter"
|
||||
"OpenRouter Model": "Model OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Najpierw wyłącz Słownik systemowy, aby to zmienić.",
|
||||
"App service is not available": "Usługa aplikacji jest niedostępna",
|
||||
"Unsupported dictionary: {{reason}}": "Nieobsługiwany słownik: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} słownik jest nieobsługiwany i wyłączony",
|
||||
"{{count}} dictionary is unsupported and disabled_few": "{{count}} słowniki są nieobsługiwane i wyłączone",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} słowników jest nieobsługiwanych i wyłączonych",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} słowników jest nieobsługiwanych i wyłączonych",
|
||||
"No new dictionaries were imported": "Nie zaimportowano żadnych nowych słowników"
|
||||
}
|
||||
|
||||
@@ -1670,5 +1670,12 @@
|
||||
"Send Reedy feedback": "Enviar feedback Reedy",
|
||||
"OpenRouter API Key": "Chave de API OpenRouter",
|
||||
"OpenRouter Base URL": "URL base OpenRouter",
|
||||
"OpenRouter Model": "Modelo OpenRouter"
|
||||
"OpenRouter Model": "Modelo OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Desative primeiro o Dicionário do Sistema para alterar isto.",
|
||||
"App service is not available": "O serviço do aplicativo não está disponível",
|
||||
"Unsupported dictionary: {{reason}}": "Dicionário não suportado: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} dicionário não é suportado e está desativado",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} dicionários não são suportados e estão desativados",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} dicionários não são suportados e estão desativados",
|
||||
"No new dictionaries were imported": "Nenhum dicionário novo foi importado"
|
||||
}
|
||||
|
||||
@@ -1670,5 +1670,12 @@
|
||||
"Send Reedy feedback": "Enviar feedback Reedy",
|
||||
"OpenRouter API Key": "Chave de API OpenRouter",
|
||||
"OpenRouter Base URL": "URL base OpenRouter",
|
||||
"OpenRouter Model": "Modelo OpenRouter"
|
||||
"OpenRouter Model": "Modelo OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Desative primeiro o Dicionário do Sistema para alterar isto.",
|
||||
"App service is not available": "O serviço da aplicação não está disponível",
|
||||
"Unsupported dictionary: {{reason}}": "Dicionário não suportado: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} dicionário não é suportado e está desativado",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} dicionários não são suportados e estão desativados",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} dicionários não são suportados e estão desativados",
|
||||
"No new dictionaries were imported": "Nenhum dicionário novo foi importado"
|
||||
}
|
||||
|
||||
@@ -1670,5 +1670,12 @@
|
||||
"Send Reedy feedback": "Trimite feedback Reedy",
|
||||
"OpenRouter API Key": "Cheie API OpenRouter",
|
||||
"OpenRouter Base URL": "URL de bază OpenRouter",
|
||||
"OpenRouter Model": "Model OpenRouter"
|
||||
"OpenRouter Model": "Model OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Dezactivați mai întâi Dicționarul de sistem pentru a modifica această opțiune.",
|
||||
"App service is not available": "Serviciul aplicației nu este disponibil",
|
||||
"Unsupported dictionary: {{reason}}": "Dicționar nesuportat: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} dicționar nu este acceptat și este dezactivat",
|
||||
"{{count}} dictionary is unsupported and disabled_few": "{{count}} dicționare nu sunt acceptate și sunt dezactivate",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} de dicționare nu sunt acceptate și sunt dezactivate",
|
||||
"No new dictionaries were imported": "Nu au fost importate dicționare noi"
|
||||
}
|
||||
|
||||
@@ -1700,5 +1700,13 @@
|
||||
"Send Reedy feedback": "Отправить отзыв о Reedy",
|
||||
"OpenRouter API Key": "Ключ API OpenRouter",
|
||||
"OpenRouter Base URL": "Базовый URL OpenRouter",
|
||||
"OpenRouter Model": "Модель OpenRouter"
|
||||
"OpenRouter Model": "Модель OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Сначала отключите системный словарь, чтобы изменить это.",
|
||||
"App service is not available": "Служба приложения недоступна",
|
||||
"Unsupported dictionary: {{reason}}": "Неподдерживаемый словарь: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} словарь не поддерживается и отключён",
|
||||
"{{count}} dictionary is unsupported and disabled_few": "{{count}} словаря не поддерживаются и отключены",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} словарей не поддерживаются и отключены",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} словарей не поддерживаются и отключены",
|
||||
"No new dictionaries were imported": "Новые словари не были импортированы"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy ප්රතිපෝෂණ යවන්න",
|
||||
"OpenRouter API Key": "OpenRouter API යතුර",
|
||||
"OpenRouter Base URL": "OpenRouter මූල URL",
|
||||
"OpenRouter Model": "OpenRouter ආකෘතිය"
|
||||
"OpenRouter Model": "OpenRouter ආකෘතිය",
|
||||
"Disable System Dictionary first to change this.": "මෙය වෙනස් කිරීමට පළමුව පද්ධති ශබ්දකෝෂය අක්රිය කරන්න.",
|
||||
"App service is not available": "යෙදුම් සේවාව ලබා ගත නොහැක",
|
||||
"Unsupported dictionary: {{reason}}": "සහාය නොදක්වන ශබ්දකෝෂය: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "ශබ්දකෝෂ {{count}}ක් සහාය නොදක්වන අතර අක්රිය කර ඇත",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "ශබ්දකෝෂ {{count}}ක් සහාය නොදක්වන අතර අක්රිය කර ඇත",
|
||||
"No new dictionaries were imported": "නව ශබ්දකෝෂ ආයාත කර නැත"
|
||||
}
|
||||
|
||||
@@ -1700,5 +1700,13 @@
|
||||
"Send Reedy feedback": "Pošlji povratne informacije Reedy",
|
||||
"OpenRouter API Key": "API-ključ OpenRouter",
|
||||
"OpenRouter Base URL": "Osnovni URL OpenRouter",
|
||||
"OpenRouter Model": "Model OpenRouter"
|
||||
"OpenRouter Model": "Model OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Najprej onemogočite sistemski slovar, da to spremenite.",
|
||||
"App service is not available": "Storitev aplikacije ni na voljo",
|
||||
"Unsupported dictionary: {{reason}}": "Nepodprt slovar: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} slovar ni podprt in je onemogočen",
|
||||
"{{count}} dictionary is unsupported and disabled_two": "{{count}} slovarja nista podprta in sta onemogočena",
|
||||
"{{count}} dictionary is unsupported and disabled_few": "{{count}} slovarji niso podprti in so onemogočeni",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} slovarjev ni podprtih in so onemogočeni",
|
||||
"No new dictionaries were imported": "Novi slovarji niso bili uvoženi"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Skicka Reedy-feedback",
|
||||
"OpenRouter API Key": "OpenRouter-API-nyckel",
|
||||
"OpenRouter Base URL": "OpenRouter-bas-URL",
|
||||
"OpenRouter Model": "OpenRouter-modell"
|
||||
"OpenRouter Model": "OpenRouter-modell",
|
||||
"Disable System Dictionary first to change this.": "Inaktivera Systemordbok först för att ändra detta.",
|
||||
"App service is not available": "App-tjänsten är inte tillgänglig",
|
||||
"Unsupported dictionary: {{reason}}": "Ordbok stöds inte: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} ordbok stöds inte och är inaktiverad",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} ordböcker stöds inte och är inaktiverade",
|
||||
"No new dictionaries were imported": "Inga nya ordböcker importerades"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy கருத்தை அனுப்பு",
|
||||
"OpenRouter API Key": "OpenRouter API விசை",
|
||||
"OpenRouter Base URL": "OpenRouter அடிப்படை URL",
|
||||
"OpenRouter Model": "OpenRouter மாதிரி"
|
||||
"OpenRouter Model": "OpenRouter மாதிரி",
|
||||
"Disable System Dictionary first to change this.": "இதை மாற்ற, முதலில் கணினி அகராதியை முடக்கவும்.",
|
||||
"App service is not available": "ஆப் சேவை கிடைக்கவில்லை",
|
||||
"Unsupported dictionary: {{reason}}": "ஆதரிக்கப்படாத அகராதி: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} அகராதி ஆதரிக்கப்படவில்லை மற்றும் முடக்கப்பட்டுள்ளது",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} அகராதிகள் ஆதரிக்கப்படவில்லை மற்றும் முடக்கப்பட்டுள்ளன",
|
||||
"No new dictionaries were imported": "புதிய அகராதிகள் எதுவும் இறக்குமதி செய்யப்படவில்லை"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "ส่งความคิดเห็นเกี่ยวกับ Reedy",
|
||||
"OpenRouter API Key": "คีย์ API OpenRouter",
|
||||
"OpenRouter Base URL": "URL พื้นฐาน OpenRouter",
|
||||
"OpenRouter Model": "โมเดล OpenRouter"
|
||||
"OpenRouter Model": "โมเดล OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "ปิดใช้งานพจนานุกรมระบบก่อนเพื่อเปลี่ยนการตั้งค่านี้",
|
||||
"App service is not available": "บริการแอปไม่พร้อมใช้งาน",
|
||||
"Unsupported dictionary: {{reason}}": "พจนานุกรมที่ไม่รองรับ: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "พจนานุกรม {{count}} รายการไม่รองรับและถูกปิดใช้งาน",
|
||||
"No new dictionaries were imported": "ไม่มีพจนานุกรมใหม่ถูกนำเข้า"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy geri bildirimi gönder",
|
||||
"OpenRouter API Key": "OpenRouter API Anahtarı",
|
||||
"OpenRouter Base URL": "OpenRouter Temel URL",
|
||||
"OpenRouter Model": "OpenRouter Modeli"
|
||||
"OpenRouter Model": "OpenRouter Modeli",
|
||||
"Disable System Dictionary first to change this.": "Bunu değiştirmek için önce Sistem Sözlüğü’nü devre dışı bırakın.",
|
||||
"App service is not available": "Uygulama hizmeti kullanılamıyor",
|
||||
"Unsupported dictionary: {{reason}}": "Desteklenmeyen sözlük: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} sözlük desteklenmiyor ve devre dışı bırakıldı",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} sözlük desteklenmiyor ve devre dışı bırakıldı",
|
||||
"No new dictionaries were imported": "Yeni sözlük içe aktarılmadı"
|
||||
}
|
||||
|
||||
@@ -1700,5 +1700,13 @@
|
||||
"Send Reedy feedback": "Надіслати відгук про Reedy",
|
||||
"OpenRouter API Key": "Ключ API OpenRouter",
|
||||
"OpenRouter Base URL": "Базова URL-адреса OpenRouter",
|
||||
"OpenRouter Model": "Модель OpenRouter"
|
||||
"OpenRouter Model": "Модель OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Спочатку вимкніть Системний словник, щоб змінити це.",
|
||||
"App service is not available": "Служба застосунку недоступна",
|
||||
"Unsupported dictionary: {{reason}}": "Непідтримуваний словник: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} словник не підтримується і вимкнено",
|
||||
"{{count}} dictionary is unsupported and disabled_few": "{{count}} словники не підтримуються і вимкнено",
|
||||
"{{count}} dictionary is unsupported and disabled_many": "{{count}} словників не підтримуються і вимкнено",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} словників не підтримуються і вимкнено",
|
||||
"No new dictionaries were imported": "Нові словники не імпортовано"
|
||||
}
|
||||
|
||||
@@ -1640,5 +1640,11 @@
|
||||
"Send Reedy feedback": "Reedy haqida fikr yuborish",
|
||||
"OpenRouter API Key": "OpenRouter API kaliti",
|
||||
"OpenRouter Base URL": "OpenRouter asosiy URL",
|
||||
"OpenRouter Model": "OpenRouter modeli"
|
||||
"OpenRouter Model": "OpenRouter modeli",
|
||||
"Disable System Dictionary first to change this.": "Buni o‘zgartirish uchun avval Tizim lug‘atini o‘chiring.",
|
||||
"App service is not available": "Ilova xizmati mavjud emas",
|
||||
"Unsupported dictionary: {{reason}}": "Qo‘llab-quvvatlanmaydigan lug‘at: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_one": "{{count}} ta lug‘at qo‘llab-quvvatlanmaydi va o‘chirilgan",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} ta lug‘at qo‘llab-quvvatlanmaydi va o‘chirilgan",
|
||||
"No new dictionaries were imported": "Yangi lug‘atlar import qilinmadi"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "Gửi phản hồi Reedy",
|
||||
"OpenRouter API Key": "Khóa API OpenRouter",
|
||||
"OpenRouter Base URL": "URL cơ sở OpenRouter",
|
||||
"OpenRouter Model": "Mô hình OpenRouter"
|
||||
"OpenRouter Model": "Mô hình OpenRouter",
|
||||
"Disable System Dictionary first to change this.": "Tắt Từ điển hệ thống trước để thay đổi điều này.",
|
||||
"App service is not available": "Dịch vụ ứng dụng không khả dụng",
|
||||
"Unsupported dictionary: {{reason}}": "Từ điển không được hỗ trợ: {{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} từ điển không được hỗ trợ và đã bị vô hiệu hóa",
|
||||
"No new dictionaries were imported": "Không có từ điển mới nào được nhập"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "发送 Reedy 反馈",
|
||||
"OpenRouter API Key": "OpenRouter API 密钥",
|
||||
"OpenRouter Base URL": "OpenRouter 基础 URL",
|
||||
"OpenRouter Model": "OpenRouter 模型"
|
||||
"OpenRouter Model": "OpenRouter 模型",
|
||||
"Disable System Dictionary first to change this.": "请先禁用系统词典再修改此项。",
|
||||
"App service is not available": "应用服务不可用",
|
||||
"Unsupported dictionary: {{reason}}": "不支持的词典:{{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} 个词典不受支持,已停用",
|
||||
"No new dictionaries were imported": "未导入任何新词典"
|
||||
}
|
||||
|
||||
@@ -1610,5 +1610,10 @@
|
||||
"Send Reedy feedback": "傳送 Reedy 意見回饋",
|
||||
"OpenRouter API Key": "OpenRouter API 金鑰",
|
||||
"OpenRouter Base URL": "OpenRouter 基礎 URL",
|
||||
"OpenRouter Model": "OpenRouter 模型"
|
||||
"OpenRouter Model": "OpenRouter 模型",
|
||||
"Disable System Dictionary first to change this.": "請先停用系統字典以變更此項。",
|
||||
"App service is not available": "應用程式服務無法使用",
|
||||
"Unsupported dictionary: {{reason}}": "不支援的字典:{{reason}}",
|
||||
"{{count}} dictionary is unsupported and disabled_other": "{{count}} 個字典不受支援,已停用",
|
||||
"No new dictionaries were imported": "未匯入任何新字典"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Importer accepts raw (uncompressed) `.dict` bodies, not just `.dict.dz`.
|
||||
* The runtime body loader (`loadDictBody`) probes the gzip header and falls
|
||||
* through to a passthrough buffer for raw files, so there's no reason to
|
||||
* gate the import.
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { gunzipSync } from 'fflate';
|
||||
import { importDictionaries } from '@/services/dictionaries/dictionaryService';
|
||||
import {
|
||||
IFO_FIXTURE_NAME,
|
||||
IDX_FIXTURE_NAME,
|
||||
readIfoFile,
|
||||
readIdxFile,
|
||||
readDictFile,
|
||||
} from './_stardictFixtures';
|
||||
import type { FileSystem } from '@/types/system';
|
||||
|
||||
function createMockFs(): FileSystem {
|
||||
return {
|
||||
resolvePath: vi
|
||||
.fn()
|
||||
.mockReturnValue({ baseDir: 0, basePrefix: async () => '', fp: 'x', base: 'Dictionaries' }),
|
||||
getURL: vi.fn().mockReturnValue('url'),
|
||||
getBlobURL: vi.fn().mockResolvedValue('blob:url'),
|
||||
getImageURL: vi.fn().mockResolvedValue('image:url'),
|
||||
openFile: vi.fn().mockResolvedValue(new File([], 'unused')),
|
||||
copyFile: vi.fn().mockResolvedValue(undefined),
|
||||
readFile: vi.fn().mockResolvedValue(''),
|
||||
writeFile: vi.fn().mockResolvedValue(undefined),
|
||||
removeFile: vi.fn().mockResolvedValue(undefined),
|
||||
readDir: vi.fn().mockResolvedValue([]),
|
||||
createDir: vi.fn().mockResolvedValue(undefined),
|
||||
removeDir: vi.fn().mockResolvedValue(undefined),
|
||||
exists: vi.fn().mockResolvedValue(false),
|
||||
stats: vi.fn().mockResolvedValue({
|
||||
isFile: true,
|
||||
isDirectory: false,
|
||||
size: 0,
|
||||
mtime: null,
|
||||
atime: null,
|
||||
birthtime: null,
|
||||
}),
|
||||
getPrefix: vi.fn().mockResolvedValue('Readest/Dictionaries'),
|
||||
};
|
||||
}
|
||||
|
||||
async function readRawDictFile(): Promise<File> {
|
||||
// Inflate the fixture .dict.dz to get a raw .dict body. Same content as
|
||||
// what the dictzip-compressed bundle would expand to, but stored without
|
||||
// the gzip wrapper — exactly the case this test guards.
|
||||
const dz = await readDictFile();
|
||||
const dzBytes = new Uint8Array(await dz.arrayBuffer());
|
||||
const raw = gunzipSync(dzBytes);
|
||||
return new File([new Uint8Array(raw)], 'cmudict.dict');
|
||||
}
|
||||
|
||||
describe('importDictionaries — StarDict raw .dict', () => {
|
||||
it('imports a raw .dict bundle without flagging it unsupported', async () => {
|
||||
const fs = createMockFs();
|
||||
const ifo = await readIfoFile();
|
||||
const idx = await readIdxFile();
|
||||
const dict = await readRawDictFile();
|
||||
|
||||
const result = await importDictionaries(fs, [{ file: ifo }, { file: idx }, { file: dict }]);
|
||||
|
||||
expect(result.orphanFiles).toEqual([]);
|
||||
expect(result.imported).toHaveLength(1);
|
||||
const entry = result.imported[0]!;
|
||||
expect(entry.kind).toBe('stardict');
|
||||
expect(entry.files.dict).toBe('cmudict.dict');
|
||||
expect(entry.files.ifo).toBe(IFO_FIXTURE_NAME);
|
||||
expect(entry.files.idx).toBe(IDX_FIXTURE_NAME);
|
||||
expect(entry.unsupported).toBeUndefined();
|
||||
expect(entry.unsupportedReason).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -103,6 +103,10 @@ const builtinLabel = (id: string, _: (key: string) => string): string => {
|
||||
interface SortableRowProps {
|
||||
row: ProviderRow;
|
||||
enabled: boolean;
|
||||
/** True when System Dictionary is on and this row is a non-system provider.
|
||||
* The toggle reflects the persisted enabled flag (so the user sees what
|
||||
* will come back when System is turned off) but is rendered read-only. */
|
||||
lockedBySystem: boolean;
|
||||
isDeleteMode: boolean;
|
||||
isEditMode: boolean;
|
||||
onToggle: (id: string, next: boolean) => void;
|
||||
@@ -115,6 +119,7 @@ interface SortableRowProps {
|
||||
const SortableRow: React.FC<SortableRowProps> = ({
|
||||
row,
|
||||
enabled,
|
||||
lockedBySystem,
|
||||
isDeleteMode,
|
||||
isEditMode,
|
||||
onToggle,
|
||||
@@ -183,11 +188,15 @@ const SortableRow: React.FC<SortableRowProps> = ({
|
||||
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle toggle-sm shrink-0'
|
||||
className={clsx(
|
||||
'toggle toggle-sm shrink-0',
|
||||
lockedBySystem && 'cursor-not-allowed opacity-60',
|
||||
)}
|
||||
checked={enabled}
|
||||
onChange={() => onToggle(row.id, !enabled)}
|
||||
disabled={row.disabled}
|
||||
disabled={row.disabled || lockedBySystem}
|
||||
aria-label={enabled ? _('Disable') : _('Enable')}
|
||||
title={lockedBySystem ? _('Disable System Dictionary first to change this.') : undefined}
|
||||
/>
|
||||
|
||||
{/* Edit pencil — parity with the trailing delete X, but for the
|
||||
@@ -247,6 +256,7 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
removeWebSearch,
|
||||
saveCustomDictionaries,
|
||||
loadCustomDictionaries,
|
||||
markAvailableByContentId,
|
||||
} = useCustomDictionaryStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -453,18 +463,41 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
setImporting(true);
|
||||
try {
|
||||
const result = await selectFiles({ type: 'dictionaries', multiple: true });
|
||||
if (result.error || result.files.length === 0) return;
|
||||
if (result.error) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
message: _('Failed to import dictionary: {{message}}', { message: result.error }),
|
||||
timeout: 4000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// User cancelled the picker — staying silent is the right call here.
|
||||
if (result.files.length === 0) return;
|
||||
const importResult = await appService?.importDictionaries(result.files, dictionaries);
|
||||
if (!importResult) return;
|
||||
if (!importResult) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
message: _('Failed to import dictionary: {{message}}', {
|
||||
message: _('App service is not available'),
|
||||
}),
|
||||
timeout: 4000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let added = 0;
|
||||
for (const dict of importResult.imported) {
|
||||
addDictionary(dict);
|
||||
// The freshly imported bundle exists on disk now; clear any lingering
|
||||
// `unavailable` flag on an in-memory entry with the same contentId
|
||||
// (e.g. when a prior import lost its bundle dir for any reason).
|
||||
if (dict.contentId) markAvailableByContentId(dict.contentId);
|
||||
if (appService) void queueDictionaryBinaryUpload(dict, appService);
|
||||
added += 1;
|
||||
}
|
||||
let replaced = 0;
|
||||
for (const { oldIds, newDict } of importResult.replacements) {
|
||||
replaceDictionaries(oldIds, newDict);
|
||||
if (newDict.contentId) markAvailableByContentId(newDict.contentId);
|
||||
if (appService) void queueDictionaryBinaryUpload(newDict, appService);
|
||||
// Invalidate any cached provider instances for the replaced ids so
|
||||
// their next lookup picks up the new bundle's files.
|
||||
@@ -488,6 +521,26 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
timeout: 2500,
|
||||
});
|
||||
}
|
||||
// Bundles that landed in the library but are marked unsupported (e.g.
|
||||
// record-block-encrypted MDX, raw .dict without DictZip). They show
|
||||
// disabled in the list — surface the first reason so the user knows
|
||||
// their "imported" toast doesn't mean it's usable.
|
||||
const unsupportedDicts = [
|
||||
...importResult.imported,
|
||||
...importResult.replacements.map((r) => r.newDict),
|
||||
].filter((d) => d.unsupported);
|
||||
if (unsupportedDicts.length > 0) {
|
||||
const firstReason = unsupportedDicts.find((d) => d.unsupportedReason)?.unsupportedReason;
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'warning',
|
||||
message: firstReason
|
||||
? _('Unsupported dictionary: {{reason}}', { reason: firstReason })
|
||||
: _('{{count}} dictionary is unsupported and disabled', {
|
||||
count: unsupportedDicts.length,
|
||||
}),
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
if (importResult.orphanFiles.length > 0) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'warning',
|
||||
@@ -497,6 +550,13 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
timeout: 4000,
|
||||
});
|
||||
}
|
||||
if (added === 0 && replaced === 0 && importResult.orphanFiles.length === 0) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('No new dictionaries were imported'),
|
||||
timeout: 2500,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
@@ -652,6 +712,15 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
key={row.id}
|
||||
row={row}
|
||||
enabled={settings.providerEnabled[row.id] !== false}
|
||||
// System-dictionary handoff is exclusive at lookup time, so
|
||||
// while it's on the other providers' toggles only express a
|
||||
// "what to restore when System is off" choice. Render them
|
||||
// read-only so the user can't accidentally clear that
|
||||
// restoration state.
|
||||
lockedBySystem={
|
||||
settings.providerEnabled[BUILTIN_PROVIDER_IDS.systemDictionary] === true &&
|
||||
row.id !== BUILTIN_PROVIDER_IDS.systemDictionary
|
||||
}
|
||||
isDeleteMode={isDeleteMode}
|
||||
isEditMode={isEditMode}
|
||||
onToggle={handleToggle}
|
||||
|
||||
@@ -25,9 +25,6 @@ import {
|
||||
shouldMintReincarnationForLiveReimport,
|
||||
} from './dictionaryDedup';
|
||||
|
||||
/** GZIP magic bytes — used to detect DictZip-compressed `.dict` files. */
|
||||
const GZIP_MAGIC = [0x1f, 0x8b, 0x08];
|
||||
|
||||
interface SourceFile {
|
||||
/** Filename including extension, e.g. `oald7.idx`. */
|
||||
name: string;
|
||||
@@ -195,12 +192,6 @@ function parseIfo(text: string): Record<string, string> {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Detect the GZIP magic at the start of a Blob. */
|
||||
async function isGzip(file: File): Promise<boolean> {
|
||||
const head = new Uint8Array(await file.slice(0, 3).arrayBuffer());
|
||||
return head[0] === GZIP_MAGIC[0] && head[1] === GZIP_MAGIC[1] && head[2] === GZIP_MAGIC[2];
|
||||
}
|
||||
|
||||
async function writeBundleFile(
|
||||
fs: FileSystem,
|
||||
bundleDir: string,
|
||||
@@ -265,25 +256,23 @@ async function importStarDictBundle(
|
||||
const name = ifo['bookname'] || group.stem;
|
||||
const lang = ifo['lang'] || ifo['idxoffsetlang'] || undefined;
|
||||
|
||||
// v1 scope: only DictZip-compressed `.dict.dz` and single-type sametypesequence ∈ {m, h, x, t}.
|
||||
// Bundles outside this surface as `unsupported` so the popup hides them
|
||||
// and the settings UI shows a clear reason; the import itself still succeeds.
|
||||
// Supported runtime surface: DictZip-compressed `.dict.dz` *or* raw `.dict`
|
||||
// (the body is opened via `loadDictBody`, which probes the gzip header and
|
||||
// falls through to a passthrough buffer for raw files). Restriction is on
|
||||
// the entry shape: single-type `sametypesequence` ∈ {m, h, x, t}. Bundles
|
||||
// outside this surface as `unsupported` so the popup hides them and the
|
||||
// settings UI shows a clear reason; the import itself still succeeds.
|
||||
let unsupported = false;
|
||||
let unsupportedReason: string | undefined;
|
||||
if (!(await isGzip(dictFile))) {
|
||||
const seq = ifo['sametypesequence'];
|
||||
if (!seq || seq.length !== 1) {
|
||||
unsupported = true;
|
||||
unsupportedReason = 'Raw .dict files are not supported in v1; please use .dict.dz format.';
|
||||
} else {
|
||||
const seq = ifo['sametypesequence'];
|
||||
if (!seq || seq.length !== 1) {
|
||||
unsupported = true;
|
||||
unsupportedReason = seq
|
||||
? `Multi-type sametypesequence "${seq}" is not supported in v1.`
|
||||
: 'StarDict bundles without sametypesequence are not supported in v1.';
|
||||
} else if (!'mhxt'.includes(seq)) {
|
||||
unsupported = true;
|
||||
unsupportedReason = `StarDict entry type "${seq}" is not supported in v1.`;
|
||||
}
|
||||
unsupportedReason = seq
|
||||
? `Multi-type sametypesequence "${seq}" is not supported in v1.`
|
||||
: 'StarDict bundles without sametypesequence are not supported in v1.';
|
||||
} else if (!'mhxt'.includes(seq)) {
|
||||
unsupported = true;
|
||||
unsupportedReason = `StarDict entry type "${seq}" is not supported in v1.`;
|
||||
}
|
||||
|
||||
// Stardict primary = .ifo (small text; partialMD5 is effectively full-hash).
|
||||
@@ -312,6 +301,54 @@ async function importStarDictBundle(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read just the XML header from an MDX/MDD file and pull out the fields we
|
||||
* need at import time (Title, Encoding, Encrypted bitmap). Avoids triggering
|
||||
* the multi-second full-init path inside `js-mdict`.
|
||||
*
|
||||
* MDX layout (V2):
|
||||
* bytes [0..4) — big-endian uint32 N: byte length of the UTF-16 LE
|
||||
* encoded XML header
|
||||
* bytes [4..4+N) — UTF-16 LE XML string (ends with \x00\x00)
|
||||
* bytes [4+N..8+N) — adler32 checksum (ignored here)
|
||||
*/
|
||||
async function readMdxHeader(file: File): Promise<{
|
||||
Title?: string;
|
||||
Encoding?: string;
|
||||
encrypt: number;
|
||||
}> {
|
||||
const sizeBuf = new Uint8Array(await file.slice(0, 4).arrayBuffer());
|
||||
if (sizeBuf.length < 4) {
|
||||
throw new Error('MDX header truncated');
|
||||
}
|
||||
const dv = new DataView(sizeBuf.buffer);
|
||||
const headerByteSize = dv.getUint32(0, false); // big-endian
|
||||
// Sanity-check: real MDX headers are typically a few hundred bytes to a few
|
||||
// kilobytes. 1 MB is already absurd — likely a corrupt or non-MDX file.
|
||||
if (headerByteSize === 0 || headerByteSize > 1024 * 1024) {
|
||||
throw new Error(`MDX header size out of range: ${headerByteSize}`);
|
||||
}
|
||||
const xmlBuf = await file.slice(4, 4 + headerByteSize).arrayBuffer();
|
||||
const xml = new TextDecoder('utf-16le').decode(xmlBuf).replace(/ | ||||