fix(opds): more accurate error message when downloading books, closes #2656 (#3529)

This commit is contained in:
Huang Xin
2026-03-13 15:26:54 +08:00
committed by GitHub
parent d06e1849cc
commit ed5bbd25d6
36 changed files with 174 additions and 58 deletions
+4
View File
@@ -75,6 +75,10 @@ Platform-specific code lives in `src-tauri/src/{macos,windows,android,ios}/`. Cu
- No lookbehind regex in build output — verified by `check:lookbehind-regex`.
- Run `pnpm build-check` (builds both targets + runs all checks) before submitting.
### i18n
See [docs/i18n.md](docs/i18n.md) for the key-as-content translation approach, `stubTranslation` usage in non-React modules, and extraction workflow.
### Safe Area Insets
See [docs/safe-area-insets.md](docs/safe-area-insets.md) for rules on handling top/bottom insets for UI elements near screen edges.
+48
View File
@@ -0,0 +1,48 @@
## i18n Guide
Readest uses a **key-as-content** approach — English strings are the translation keys. The English locale (`en/translation.json`) is empty because keys serve as content. Other locales contain actual translations.
### In React Components
```tsx
import { useTranslation } from '@/hooks/useTranslation';
const _ = useTranslation();
_('Progress synced');
```
### In Non-React Modules
Two-step process:
**1. Declaration** — Use `stubTranslation` to mark strings for scanner extraction (returns key as-is, does NOT translate):
```ts
import { stubTranslation as _ } from '@/utils/misc';
// These calls only register keys for extraction
_('Reveal in Finder');
_('Reveal in Explorer');
```
**2. Usage** — In the React component that consumes the value, apply the real `_()` from `useTranslation`:
```tsx
const _ = useTranslation();
const label = _(getRevealLabel()); // translates at runtime
```
### Extraction & Translation
```bash
pnpm i18n:extract # Scans codebase, adds new keys with __STRING_NOT_TRANSLATED__
```
- Translation files: `public/locales/<locale>/translation.json`
- Only `_('KEY')` and `_('KEY', options)` patterns are recognized by i18next-scanner
### Rules
- `stubTranslation` is for extraction only — always apply `_()` from `useTranslation` in the component for runtime translation.
- Fallback: when no translation exists, the English key itself is displayed.
- Error messages: register keys with `stubTranslation` in utility modules (e.g. `src/services/errors.ts`), return the English key from helpers, wrap with `_()` in the component.
@@ -1117,5 +1117,6 @@
"Decrease font size": "تصغير حجم الخط",
"Increase font size": "تكبير حجم الخط",
"Focus": "تركيز",
"Theme color": "لون السمة"
"Theme color": "لون السمة",
"Import failed": "فشل الاستيراد"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "ফন্টের আকার কমান",
"Increase font size": "ফন্টের আকার বাড়ান",
"Focus": "ফোকাস",
"Theme color": "থিম রঙ"
"Theme color": "থিম রঙ",
"Import failed": "আমদানি ব্যর্থ"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "ཡིག་གཟུགས་ཆུང་དུ་གཏོང་བ",
"Increase font size": "ཡིག་གཟུགས་ཆེ་རུ་གཏོང་བ",
"Focus": "དམིགས་གཏད",
"Theme color": "བརྗོད་དོན་ཚོན་མདོག"
"Theme color": "བརྗོད་དོན་ཚོན་མདོག",
"Import failed": "ནང་འདྲེན་མི་ཐུབ།"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "Schriftgröße verkleinern",
"Increase font size": "Schriftgröße vergrößern",
"Focus": "Fokus",
"Theme color": "Themenfarbe"
"Theme color": "Themenfarbe",
"Import failed": "Import fehlgeschlagen"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "Μείωση μεγέθους γραμματοσειράς",
"Increase font size": "Αύξηση μεγέθους γραμματοσειράς",
"Focus": "Εστίαση",
"Theme color": "Χρώμα θέματος"
"Theme color": "Χρώμα θέματος",
"Import failed": "Η εισαγωγή απέτυχε"
}
@@ -1081,5 +1081,6 @@
"Decrease font size": "Reducir tamaño de fuente",
"Increase font size": "Aumentar tamaño de fuente",
"Focus": "Enfoque",
"Theme color": "Color del tema"
"Theme color": "Color del tema",
"Import failed": "Error de importación"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "کاهش اندازه قلم",
"Increase font size": "افزایش اندازه قلم",
"Focus": "تمرکز",
"Theme color": "رنگ پوسته"
"Theme color": "رنگ پوسته",
"Import failed": "وارد کردن ناموفق بود"
}
@@ -1081,5 +1081,6 @@
"Decrease font size": "Réduire la taille de police",
"Increase font size": "Augmenter la taille de police",
"Focus": "Focus",
"Theme color": "Couleur du thème"
"Theme color": "Couleur du thème",
"Import failed": "Échec de l'importation"
}
@@ -1081,5 +1081,6 @@
"Decrease font size": "הקטן גודל גופן",
"Increase font size": "הגדל גודל גופן",
"Focus": "מיקוד",
"Theme color": "צבע ערכת נושא"
"Theme color": "צבע ערכת נושא",
"Import failed": "הייבוא נכשל"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "फ़ॉन्ट आकार घटाएँ",
"Increase font size": "फ़ॉन्ट आकार बढ़ाएँ",
"Focus": "फ़ोकस",
"Theme color": "थीम रंग"
"Theme color": "थीम रंग",
"Import failed": "आयात विफल"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "Perkecil ukuran font",
"Increase font size": "Perbesar ukuran font",
"Focus": "Fokus",
"Theme color": "Warna tema"
"Theme color": "Warna tema",
"Import failed": "Impor gagal"
}
@@ -1081,5 +1081,6 @@
"Decrease font size": "Riduci dimensione carattere",
"Increase font size": "Aumenta dimensione carattere",
"Focus": "Messa a fuoco",
"Theme color": "Colore del tema"
"Theme color": "Colore del tema",
"Import failed": "Importazione fallita"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "文字サイズを縮小",
"Increase font size": "文字サイズを拡大",
"Focus": "フォーカス",
"Theme color": "テーマカラー"
"Theme color": "テーマカラー",
"Import failed": "インポートに失敗しました"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "글꼴 크기 줄이기",
"Increase font size": "글꼴 크기 늘리기",
"Focus": "초점",
"Theme color": "테마 색상"
"Theme color": "테마 색상",
"Import failed": "가져오기 실패"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "Kecilkan saiz fon",
"Increase font size": "Besarkan saiz fon",
"Focus": "Fokus",
"Theme color": "Warna tema"
"Theme color": "Warna tema",
"Import failed": "Import gagal"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "Lettergrootte verkleinen",
"Increase font size": "Lettergrootte vergroten",
"Focus": "Focus",
"Theme color": "Themakleur"
"Theme color": "Themakleur",
"Import failed": "Import mislukt"
}
@@ -1093,5 +1093,6 @@
"Decrease font size": "Zmniejsz rozmiar czcionki",
"Increase font size": "Zwiększ rozmiar czcionki",
"Focus": "Fokus",
"Theme color": "Kolor motywu"
"Theme color": "Kolor motywu",
"Import failed": "Import nie powiódł się"
}
@@ -1081,5 +1081,6 @@
"Decrease font size": "Diminuir tamanho da fonte",
"Increase font size": "Aumentar tamanho da fonte",
"Focus": "Foco",
"Theme color": "Cor do tema"
"Theme color": "Cor do tema",
"Import failed": "Falha na importação"
}
@@ -1093,5 +1093,6 @@
"Decrease font size": "Уменьшить размер шрифта",
"Increase font size": "Увеличить размер шрифта",
"Focus": "Фокус",
"Theme color": "Цвет темы"
"Theme color": "Цвет темы",
"Import failed": "Ошибка импорта"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "අකුරු ප්‍රමාණය අඩු කරන්න",
"Increase font size": "අකුරු ප්‍රමාණය වැඩි කරන්න",
"Focus": "අවධානය",
"Theme color": "තේමා වර්ණය"
"Theme color": "තේමා වර්ණය",
"Import failed": "ආයාත කිරීම අසාර්ථකයි"
}
@@ -1093,5 +1093,6 @@
"Decrease font size": "Pomanjšaj pisavo",
"Increase font size": "Povečaj pisavo",
"Focus": "Fokus",
"Theme color": "Barva teme"
"Theme color": "Barva teme",
"Import failed": "Uvoz ni uspel"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "Minska teckenstorlek",
"Increase font size": "Öka teckenstorlek",
"Focus": "Fokus",
"Theme color": "Temafärg"
"Theme color": "Temafärg",
"Import failed": "Importen misslyckades"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "எழுத்துரு அளவைக் குறை",
"Increase font size": "எழுத்துரு அளவை அதிகரி",
"Focus": "குவியம்",
"Theme color": "தீம் நிறம்"
"Theme color": "தீம் நிறம்",
"Import failed": "இறக்குமதி தோல்வி"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "ลดขนาดตัวอักษร",
"Increase font size": "เพิ่มขนาดตัวอักษร",
"Focus": "โฟกัส",
"Theme color": "สีธีม"
"Theme color": "สีธีม",
"Import failed": "นำเข้าล้มเหลว"
}
@@ -1069,5 +1069,6 @@
"Decrease font size": "Yazı tipi boyutunu küçült",
"Increase font size": "Yazı tipi boyutunu büyüt",
"Focus": "Odak",
"Theme color": "Tema rengi"
"Theme color": "Tema rengi",
"Import failed": "İçe aktarma başarısız"
}
@@ -1093,5 +1093,6 @@
"Decrease font size": "Зменшити розмір шрифту",
"Increase font size": "Збільшити розмір шрифту",
"Focus": "Фокус",
"Theme color": "Колір теми"
"Theme color": "Колір теми",
"Import failed": "Помилка імпорту"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "Giảm cỡ chữ",
"Increase font size": "Tăng cỡ chữ",
"Focus": "Tiêu điểm",
"Theme color": "Màu chủ đề"
"Theme color": "Màu chủ đề",
"Import failed": "Nhập thất bại"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "缩小字号",
"Increase font size": "放大字号",
"Focus": "焦点",
"Theme color": "主题颜色"
"Theme color": "主题颜色",
"Import failed": "导入失败"
}
@@ -1057,5 +1057,6 @@
"Decrease font size": "縮小字型",
"Increase font size": "放大字型",
"Focus": "焦點",
"Theme color": "主題顏色"
"Theme color": "主題顏色",
"Import failed": "匯入失敗"
}
+2 -12
View File
@@ -12,6 +12,7 @@ import { Book } from '@/types/book';
import { AppService, DeleteAction } from '@/types/system';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
import { formatAuthors, formatTitle, getPrimaryLanguage, listFormater } from '@/utils/book';
import { getImportErrorMessage } from '@/services/errors';
import { eventDispatcher } from '@/utils/event';
import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
@@ -495,14 +496,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const { library } = useLibraryStore.getState();
const failedImports: Array<{ filename: string; errorMessage: string }> = [];
const successfulImports: string[] = [];
const errorMap: [string, string][] = [
['No chapters detected', _('No chapters detected')],
['Failed to parse EPUB', _('Failed to parse the EPUB file')],
['Unsupported format', _('This book format is not supported')],
['Failed to open file', _('Failed to open the book file')],
['Invalid or empty book file', _('The book file is empty')],
['Unsupported or corrupted book file', _('The book file is corrupted')],
];
const processFile = async (selectedFile: SelectedFile): Promise<Book | null> => {
const file = selectedFile.file || selectedFile.path;
@@ -530,10 +523,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
} catch (error) {
const filename = typeof file === 'string' ? file : file.name;
const baseFilename = getFilename(filename);
const errorMessage =
error instanceof Error
? errorMap.find(([str]) => error.message.includes(str))?.[1] || error.message
: '';
const errorMessage = error instanceof Error ? _(getImportErrorMessage(error.message)) : '';
failedImports.push({ filename: baseFilename, errorMessage });
console.error('Failed to import book:', filename, error);
return null;
@@ -9,6 +9,7 @@ import { OPDSLink, OPDSPublication, REL, SYMBOL } from '@/types/opds';
import { useTranslation } from '@/hooks/useTranslation';
import { getFileExtFromMimeType } from '@/libs/document';
import { formatDate, formatLanguage } from '@/utils/book';
import { getImportErrorMessage, ImportError } from '@/services/errors';
import { eventDispatcher } from '@/utils/event';
import { navigateToReader } from '@/utils/nav';
import { CachedImage } from '@/components/CachedImage';
@@ -96,10 +97,19 @@ export function PublicationView({
eventDispatcher.dispatch('toast', { type: 'success', message: _('Download completed') });
} catch (error) {
console.error('Download failed:', error);
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Download failed') + `:\n${href}`,
});
if (error instanceof ImportError) {
const friendlyMsg = _(getImportErrorMessage(error.message));
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Import failed') + `:\n${friendlyMsg}`,
timeout: 5000,
});
} else {
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Download failed') + `:\n${href}`,
});
}
} finally {
setDownloading(false);
setProgress(null);
+14 -8
View File
@@ -36,6 +36,7 @@ import {
needsProxy,
probeFilename,
} from './utils/opdsReq';
import { ImportError } from '@/services/errors';
import { READEST_OPDS_USER_AGENT } from '@/services/constants';
import { FeedView } from './components/FeedView';
import { PublicationView } from './components/PublicationView';
@@ -463,15 +464,20 @@ export default function BrowserPage() {
}
const { library, setLibrary } = useLibraryStore.getState();
const book = await appService.importBook(dstFilePath, library);
if (user && book && !book.uploadedAt && settings.autoUpload) {
setTimeout(() => {
transferManager.queueUpload(book);
}, 3000);
try {
const book = await appService.importBook(dstFilePath, library);
if (user && book && !book.uploadedAt && settings.autoUpload) {
setTimeout(() => {
transferManager.queueUpload(book);
}, 3000);
}
setLibrary(library);
appService.saveLibraryBooks(library);
return book;
} catch (importError) {
console.error('Import error:', importError);
throw new ImportError(importError);
}
setLibrary(library);
appService.saveLibraryBooks(library);
return book;
}
} catch (e) {
console.error('Download error:', e);
+4 -4
View File
@@ -80,7 +80,7 @@ import {
import { ClosableFile } from '@/utils/file';
import { ProgressHandler } from '@/utils/transfer';
import { TxtToEpubConverter } from '@/utils/txt';
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
import { BookFileNotFoundError } from './errors';
import { CustomTextureInfo } from '@/styles/textures';
import { CustomFont, CustomFontInfo } from '@/styles/fonts';
import { parseFontInfo } from '@/utils/font';
@@ -395,7 +395,7 @@ export abstract class BaseAppService implements AppService {
loadedBook.metadata.title = getBaseFilename(filename);
}
} catch (error) {
throw new Error(`Failed to open the book: ${(error as Error).message || error}`);
throw new Error(`Failed to open the book file: ${(error as Error).message || error}`);
}
const hash = await partialMD5(fileobj);
@@ -769,10 +769,10 @@ export abstract class BaseAppService implements AppService {
if (bookFile) {
file = await this.fs.openFile(`${bookDir}/${bookFile.path}`, 'Books');
} else {
throw new Error(BOOK_FILE_NOT_FOUND_ERROR);
throw new BookFileNotFoundError();
}
} else {
throw new Error(BOOK_FILE_NOT_FOUND_ERROR);
throw new BookFileNotFoundError();
}
}
return { book, file };
+30 -1
View File
@@ -1 +1,30 @@
export const BOOK_FILE_NOT_FOUND_ERROR = 'Book file not found';
import { stubTranslation as _ } from '@/utils/misc';
export class BookFileNotFoundError extends Error {
constructor() {
super('Book file not found');
this.name = 'BookFileNotFoundError';
}
}
export class ImportError extends Error {
constructor(cause: unknown) {
const msg = cause instanceof Error ? cause.message : String(cause);
super(msg);
this.name = 'ImportError';
}
}
const IMPORT_ERROR_MAP: [string, string][] = [
['No chapters detected', _('No chapters detected')],
['Failed to parse EPUB', _('Failed to parse the EPUB file')],
['Unsupported format', _('This book format is not supported')],
['Failed to open file', _('Failed to open the book file')],
['Invalid or empty book file', _('The book file is empty')],
['Unsupported or corrupted book file', _('The book file is corrupted')],
];
export const getImportErrorMessage = (errorMsg: string): string => {
const match = IMPORT_ERROR_MAP.find(([str]) => errorMsg.includes(str));
return match ? match[1] : errorMsg;
};