This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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": "匯入失敗"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user