forked from akai/readest
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 531dbe5f16 | |||
| 70b94d8986 | |||
| 21795e5cd3 | |||
| 52242d6886 | |||
| f361698e05 | |||
| 9595aa56e0 | |||
| caa0d719c5 | |||
| 45bd355981 | |||
| 52ac74bbad | |||
| 6a44f609ba | |||
| ca5c860594 | |||
| 80cab8e56d | |||
| 888f4afde9 | |||
| 05afaab5fd | |||
| ff962a1f02 | |||
| 62df631dd1 | |||
| c9647276b1 | |||
| 9ecb9b24d2 | |||
| f0ab05bbde | |||
| f1a08565e3 | |||
| b8ddb5475e | |||
| 74401fc1bb | |||
| 29df8522fa | |||
| 9f958a44e2 | |||
| 0e516f6e56 | |||
| b71b246601 | |||
| 94843902ac | |||
| f67930feb1 | |||
| 5e048ddab1 | |||
| e68dedd10d | |||
| d22b8bec1e | |||
| e9c5ebb696 | |||
| 956c71cd7b | |||
| b4207bd742 | |||
| ec26ef4f29 | |||
| b872868136 | |||
| 797fe9c604 | |||
| 8ed9290659 | |||
| 8e6451863f | |||
| c688612888 | |||
| b3333c384c | |||
| 84349ab12d | |||
| c4e3315642 | |||
| bbfc82e50d | |||
| 966f5e2acd | |||
| 45e5f0fa61 | |||
| 3d4d1482aa | |||
| 5f897f648f | |||
| 26fec924fc | |||
| af9cf33936 | |||
| 52df478f21 | |||
| 97555a7e88 | |||
| c7e82825f5 | |||
| bb82ab6c8a | |||
| e2faa9ad75 | |||
| f310305834 | |||
| 5a072e7d1f | |||
| 64675820f1 | |||
| 1e942a23b4 | |||
| a92c357982 | |||
| 1ebf5e7b52 | |||
| c11f79e843 | |||
| 87f0240b0a | |||
| 2905506017 | |||
| 1936136596 | |||
| e0cf7e8d9f | |||
| 0177a03a90 | |||
| f5df80f154 | |||
| 76b239f382 | |||
| 2f430bf2ff | |||
| 0e8605d298 | |||
| 7445d5befe | |||
| e41bcfbac9 | |||
| 91bc4ddec7 | |||
| e8f70b896e | |||
| 1f4f5f8a6d | |||
| 764acf59de | |||
| eab83c6d3f | |||
| 7f62cdcab9 | |||
| e4ab06abcd | |||
| fef0aa6947 | |||
| 2935eacb1c | |||
| ad9bb58cd4 | |||
| 871d6467c3 | |||
| 3f19ce24de | |||
| 8280585e70 | |||
| 1f7483b911 | |||
| b2bb92036b | |||
| 3831f24ff7 | |||
| 3bec89c85d | |||
| 3dfe6bd65e | |||
| b00d321817 | |||
| 1af55e24da | |||
| f7b2a2432c | |||
| db3dee025b | |||
| 25352efece | |||
| 1297e33c62 | |||
| 8274c6bc4f | |||
| ed5bbd25d6 | |||
| d06e1849cc | |||
| 34ad6e6749 | |||
| b163012488 | |||
| f9a3559086 | |||
| 674a0bf16d | |||
| 9b53ccc9de | |||
| 7a605a357a | |||
| af649edd0d | |||
| 64a71e25e4 | |||
| 51a3767940 | |||
| a8572ab7c6 | |||
| 907b672313 | |||
| 8df9f909b4 | |||
| bcd7a90f27 | |||
| 5646e0cfb5 | |||
| fad27d74a0 | |||
| 5fbbfa523b | |||
| 569c6707a4 | |||
| 99f8a29326 | |||
| 04737d6f35 | |||
| 8850e6c00f | |||
| 93b96d64eb | |||
| 9f8894c1e0 | |||
| dac4f2e8ec | |||
| 54bc1514df | |||
| 97cab2d70b | |||
| 13588b4a65 | |||
| 0f5bd5ca1c | |||
| a74c1a56a9 | |||
| 1e9bd1d821 | |||
| 02cc0a9ebb | |||
| 5273ef75dc | |||
| bd957a4eb8 | |||
| 38552a0c2e | |||
| 0609e828b1 | |||
| 35ade2c855 | |||
| b68c14da1f | |||
| 5c41394961 | |||
| 5685944599 | |||
| 94e761f681 | |||
| 7f636a2072 | |||
| 480b8b98a3 | |||
| d1fb67316f | |||
| f0e9ddd2ae | |||
| b16a4445ae | |||
| 515d47e64d | |||
| 3fd78c4ed8 | |||
| 152a95941a | |||
| bf5805910d | |||
| 431d14f4a4 | |||
| 0d2e5b7c76 | |||
| e949476d27 | |||
| 0afff573a1 | |||
| d4bb61f12b | |||
| 09c45b4615 | |||
| d8eef87bf0 |
@@ -1,5 +1,7 @@
|
||||
name: PR checks
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
permissions:
|
||||
@@ -10,21 +12,30 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: '-C target-cpu=skylake'
|
||||
SCCACHE_GHA_ENABLED: 'true'
|
||||
RUSTC_WRAPPER: sccache
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'true'
|
||||
- name: setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- name: Install minimal stable with clippy and rustfmt
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Cache apt packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: apt-rust-lint-${{ runner.os }}
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev
|
||||
- name: Format
|
||||
- name: Format check
|
||||
working-directory: apps/readest-app/src-tauri
|
||||
run: cargo fmt --check
|
||||
- name: Clippy Check
|
||||
@@ -33,33 +44,28 @@ jobs:
|
||||
|
||||
build_web_app:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- platform: 'web'
|
||||
- platform: 'tauri'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: cache Next.js build
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/readest-app/.next/cache
|
||||
key: nextjs-${{ matrix.config.platform }}-${{ github.sha }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
key: nextjs-web-${{ github.sha }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
nextjs-${{ matrix.config.platform }}-${{ github.sha }}-
|
||||
nextjs-${{ matrix.config.platform }}-
|
||||
nextjs-web-${{ github.sha }}-
|
||||
nextjs-web-
|
||||
|
||||
- name: install Dependencies
|
||||
working-directory: apps/readest-app
|
||||
@@ -70,24 +76,80 @@ jobs:
|
||||
run: |
|
||||
pnpm format:check || (pnpm format && git diff && exit 1)
|
||||
|
||||
- name: run tests
|
||||
- name: install playwright browsers
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm test -- --watch=false
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: run web tests
|
||||
working-directory: apps/readest-app
|
||||
run: pnpm test:pr:web
|
||||
|
||||
- name: run lint
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm lint
|
||||
|
||||
- name: build the web App
|
||||
if: matrix.config.platform == 'web'
|
||||
- name: build the web app
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build-web && pnpm check:all
|
||||
|
||||
- name: build the Tauri App
|
||||
if: matrix.config.platform == 'tauri'
|
||||
build_tauri_app:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: 'true'
|
||||
RUSTC_WRAPPER: sccache
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: cache Next.js build
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/readest-app/.next/cache
|
||||
key: nextjs-tauri-${{ github.sha }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
nextjs-tauri-${{ github.sha }}-
|
||||
nextjs-tauri-
|
||||
|
||||
- name: install Dependencies
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build && pnpm check:all
|
||||
pnpm install && pnpm setup-vendors
|
||||
|
||||
- name: setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: tauri-cargo
|
||||
cache-all-crates: 'true'
|
||||
|
||||
- name: Cache apt packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: apt-tauri-${{ runner.os }}
|
||||
- name: install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev xvfb
|
||||
|
||||
- name: run tauri tests
|
||||
working-directory: apps/readest-app
|
||||
run: xvfb-run pnpm test:pr:tauri
|
||||
|
||||
@@ -124,11 +124,6 @@ jobs:
|
||||
release: linux
|
||||
arch: aarch64
|
||||
rust_target: aarch64-unknown-linux-gnu
|
||||
- os: ubuntu-22.04-arm
|
||||
release: linux
|
||||
arch: armhf
|
||||
rust_target: arm-unknown-linux-gnueabihf
|
||||
args: '--target arm-unknown-linux-gnueabihf --bundles deb'
|
||||
- os: macos-latest
|
||||
release: macos
|
||||
arch: aarch64
|
||||
@@ -154,12 +149,12 @@ jobs:
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: setup Java (for Android build only)
|
||||
@@ -171,7 +166,7 @@ jobs:
|
||||
|
||||
- name: setup Android SDK (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
- name: install NDK (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
@@ -208,6 +203,7 @@ jobs:
|
||||
echo 'PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/share/pkgconfig' >> $GITHUB_ENV
|
||||
echo 'PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf' >> $GITHUB_ENV
|
||||
echo 'CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc' >> $GITHUB_ENV
|
||||
echo 'CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS=--cfg=io_uring_skip_arch_check' >> $GITHUB_ENV
|
||||
|
||||
- name: create .env.local file for Next.js
|
||||
run: |
|
||||
|
||||
@@ -41,9 +41,15 @@ next-env.d.ts
|
||||
target
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/metadata/android/en-US/changelogs
|
||||
|
||||
*.koplugin.zip
|
||||
|
||||
# nix
|
||||
result*
|
||||
|
||||
.playwright-mcp/
|
||||
|
||||
.claude/worktrees
|
||||
.claude/settings.local.json
|
||||
|
||||
|
||||
@@ -10,3 +10,12 @@
|
||||
[submodule "packages/simplecc-wasm"]
|
||||
path = packages/simplecc-wasm
|
||||
url = https://github.com/readest/simplecc-wasm.git
|
||||
[submodule "apps/readest-app/src-tauri/plugins/tauri-plugin-turso"]
|
||||
path = apps/readest-app/src-tauri/plugins/tauri-plugin-turso
|
||||
url = https://github.com/readest/tauri-plugin-turso.git
|
||||
[submodule "apps/readest-app/.claude/skills/gstack"]
|
||||
path = apps/readest-app/.claude/skills/gstack
|
||||
url = https://github.com/garrytan/gstack.git
|
||||
[submodule "packages/qcms"]
|
||||
path = packages/qcms
|
||||
url = https://github.com/mozilla/pdf.js.qcms.git
|
||||
|
||||
Regular → Executable
@@ -22,9 +22,13 @@ gen
|
||||
# Submodules (External Repos)
|
||||
packages
|
||||
|
||||
# Claude Code Skills & Config
|
||||
apps/readest-app/.claude
|
||||
|
||||
# Vendored Assets (Generated/External Code)
|
||||
apps/readest-app/public/*.js
|
||||
apps/readest-app/public/vendor
|
||||
apps/readest-app/src-tauri/plugins/tauri-plugin-turso
|
||||
|
||||
# Environment & Editor
|
||||
.env
|
||||
|
||||
Generated
+3336
-702
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -2,10 +2,10 @@
|
||||
members = [
|
||||
"apps/readest-app/src-tauri",
|
||||
"packages/tauri/crates/tauri",
|
||||
"packages/tauri/crates/tauri-utils",
|
||||
"packages/tauri/crates/tauri-build",
|
||||
"packages/tauri-plugins/plugins/deep-link",
|
||||
"packages/tauri-plugins/plugins/single-instance"
|
||||
"packages/tauri-plugins/plugins/fs"
|
||||
]
|
||||
exclude = [
|
||||
"packages/qcms"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -24,6 +24,9 @@ glob = "0.3"
|
||||
zbus = "5.9"
|
||||
dunce = "1"
|
||||
url = "2"
|
||||
tar = "0.4.45"
|
||||
nix = "0.20.2"
|
||||
glib = "0.20.0"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Bilingify LLC"]
|
||||
@@ -36,7 +39,4 @@ rust-version = "1.77.2"
|
||||
|
||||
[patch.crates-io]
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri-utils = { path = "packages/tauri/crates/tauri-utils" }
|
||||
tauri-build = { path = "packages/tauri/crates/tauri-build" }
|
||||
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
|
||||
tauri-plugin-single-instance = { path = "packages/tauri-plugins/plugins/single-instance" }
|
||||
tauri-plugin-fs = { path = "packages/tauri-plugins/plugins/fs" }
|
||||
|
||||
@@ -45,38 +45,38 @@
|
||||
|
||||
<div align="left">✅ Implemented</div>
|
||||
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF (experimental) | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience and use instant mode for quicker interactions. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. | ✅ |
|
||||
| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| **Accessibility** | Provides full keyboard navigation and supports for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ |
|
||||
| **Visual & Focus Aids** | Reading ruler, paragraph-by-paragraph reading mode, and speed reading features. | ✅ |
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience and use instant mode for quicker interactions. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. | ✅ |
|
||||
| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | ✅ |
|
||||
| **Accessibility** | Provides full keyboard navigation and supports for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ |
|
||||
| **Visual & Focus Aids** | Reading ruler, paragraph-by-paragraph reading mode, and speed reading features. | ✅ |
|
||||
|
||||
## Planned Features
|
||||
|
||||
<div align="left">🛠 Building</div>
|
||||
<div align="left">🔄 Planned</div>
|
||||
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🛠 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------- | -------------------------------------------------------------------------- | ------------ |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🛠 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
|
||||
|
||||
Stay tuned for continuous improvements and updates! Contributions and suggestions are always welcome—let's build the ultimate reading experience together. 😊
|
||||
|
||||
@@ -122,8 +122,8 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
For the best experience to build Readest for yourself, use a recent version of Node.js and Rust. Refer to the [Tauri documentation](https://v2.tauri.app/start/prerequisites/) for details on setting up the development environment prerequisites on different platforms.
|
||||
|
||||
```bash
|
||||
nvm install v22
|
||||
nvm use v22
|
||||
nvm install v24
|
||||
nvm use v24
|
||||
npm install -g pnpm
|
||||
rustup update
|
||||
```
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Readest does not currently maintain separate release channels. Security updates are provided only for the latest release series.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.10.x | :white_check_mark: |
|
||||
| < 0.10 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report suspected vulnerabilities privately. Do not open a public GitHub
|
||||
issue or discussion for security-sensitive reports.
|
||||
|
||||
Use GitHub's private vulnerability reporting for this repository:
|
||||
|
||||
<https://github.com/readest/readest/security/advisories/new>
|
||||
|
||||
When submitting a report, include:
|
||||
|
||||
- A clear description of the issue and the affected component
|
||||
- Steps to reproduce, proof of concept, or a minimal test case
|
||||
- The versions, platforms, or environments you tested
|
||||
- Any suggested remediation or mitigating details, if available
|
||||
|
||||
What to expect after you report:
|
||||
|
||||
- We will aim to acknowledge receipt within 3 business days.
|
||||
- We may contact you for additional details, reproduction steps, or validation.
|
||||
- If the report is accepted, we will work on a fix and coordinate disclosure.
|
||||
- If the report is declined, we will explain why, for example if the behavior is
|
||||
expected, unsupported, or not reproducible.
|
||||
|
||||
Please keep vulnerability details private until a fix is available and the
|
||||
maintainers have approved disclosure.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Readest Project Memory
|
||||
|
||||
## Key Reference Documents
|
||||
- [Bug Fixing Patterns](bug-patterns.md) - Common bug categories, root causes, and fix strategies
|
||||
- [CSS & Style Fixes](css-style-fixes.md) - EPUB CSS override patterns and the style.ts pipeline
|
||||
- [TTS Fixes](tts-fixes.md) - Text-to-Speech architecture and bug patterns
|
||||
- [Layout & UI Fixes](layout-ui-fixes.md) - Safe insets, z-index, platform-specific UI issues
|
||||
- [Platform Compat Fixes](platform-compat-fixes.md) - Android, iOS, Linux, macOS platform-specific bugs
|
||||
- [Annotator & Reader Fixes](annotator-reader-fixes.md) - Highlight, selection, accessibility bugs
|
||||
|
||||
## Critical Files (Most Bug-Prone)
|
||||
- `src/utils/style.ts` - Central EPUB CSS transformation hub (14+ bug fixes)
|
||||
- `packages/foliate-js/paginator.js` - Page layout, image sizing, backgrounds
|
||||
- `src/services/tts/TTSController.ts` - TTS state machine, section tracking
|
||||
- `src/hooks/useSafeAreaInsets.ts` - Safe area inset management
|
||||
- `src/app/reader/components/FoliateViewer.tsx` - Reader view orchestration
|
||||
- `src/app/reader/components/annotator/Annotator.tsx` - Annotation lifecycle
|
||||
|
||||
## Feature Notes
|
||||
- [D-pad Navigation](dpad-navigation.md) — Android TV remote / keyboard arrow navigation design, key files, and pitfalls
|
||||
|
||||
## Architecture Notes
|
||||
- foliate-js is a git submodule at `packages/foliate-js/`
|
||||
- Multiview paginator: loads adjacent sections in background, multiple View/Overlayer instances per book
|
||||
- Style overrides: `getLayoutStyles()` (always), `getColorStyles()` (when overriding color)
|
||||
- `transformStylesheet()` does regex-based EPUB CSS rewriting at load time
|
||||
- TTS uses independent section tracking (`#ttsSectionIndex`) decoupled from view
|
||||
- Safe area insets flow: Native plugin -> useSafeAreaInsets hook -> component styles
|
||||
- Dropdown menus use `DropdownContext` (not blur-based) for screen reader compat
|
||||
|
||||
## Workflow
|
||||
- [Always rebase before PR](feedback_pr_rebase.md) — rebase onto origin/main before creating PRs
|
||||
- [New branch per PR](feedback_pr_new_branch.md) — always create a fresh branch from main for each new PR/issue
|
||||
- [Upgrade gstack locally](feedback_gstack_upgrade.md) — always upgrade from the project's .claude/skills/gstack, not global
|
||||
@@ -0,0 +1,90 @@
|
||||
# Annotator & Reader Fixes Reference
|
||||
|
||||
## Annotation System Architecture
|
||||
|
||||
### Key Components
|
||||
- `Annotator.tsx` - Annotation lifecycle, popup display, style/color management
|
||||
- `AnnotationRangeEditor.tsx` - Drag handles for adjusting selection range
|
||||
- `MagnifierLoupe.tsx` - Magnifying glass during handle drag (mobile only)
|
||||
- `useTextSelector.ts` - Text selection detection and processing
|
||||
- `useAnnotationEditor.ts` - Editing existing annotations
|
||||
- `useInstantAnnotation.ts` - Creating new annotations on selection
|
||||
|
||||
### Highlight Rendering
|
||||
- Highlights rendered by foliate-js `Overlayer` (SVG overlayer in paginator shadow DOM, not iframe)
|
||||
- Each view in multiview paginator has its own `Overlayer` instance with unique clipPath ID
|
||||
- `Overlayer.add()` stores range + draw function; `redraw()` recalculates positions from stored ranges
|
||||
- Colors stored as color names mapped to custom hex via `globalReadSettings.customHighlightColors`
|
||||
- Sidebar uses `color-mix()` CSS function with custom colors, not Tailwind utility classes (#3273)
|
||||
- Rounded highlight style supported via `vertical` option passed to overlayer (#3208)
|
||||
|
||||
### Multiview Overlayer Pitfalls
|
||||
- **Duplicate SVG IDs**: Each overlayer creates `<clipPath>` for loupe hole — IDs MUST be unique per instance or `url(#id)` resolves to wrong element, clipping everything
|
||||
- **docLoadHandler scope**: `FoliateViewer.tsx` re-adds annotations on `load` event — MUST filter by `detail.index` (loaded section), not re-add ALL annotations (overwrites drag edits)
|
||||
- **MagnifierLoupe lifecycle**: Don't destroy/recreate loupe on every drag tick — `hideLoupe()` should only run on unmount, `showLoupe()` fast path updates position only
|
||||
- **Stale closures in useTextSelector**: `getProgress()` must be called inside callbacks, not captured at hook top-level (useFoliateEvents deps are `[view]` only)
|
||||
|
||||
## Fix History
|
||||
|
||||
| Issue | Problem | Root Cause | Fix |
|
||||
|-------|---------|------------|-----|
|
||||
| #3286 | Selection stuck on first annotation | `initializedRef` guard blocked re-computation | Remove guard, consolidate style/color effects |
|
||||
| #3273 | Custom colors not in sidebar | Hardcoded Tailwind classes | Use inline `style` with `color-mix()` |
|
||||
| #3234 | Letter-by-letter selection on mobile | No word boundary snapping | Add `snapRangeToWords()` using `Intl.Segmenter` |
|
||||
| #3208 | Hard rectangular highlights | No border radius support | Pass `vertical` option, update foliate-js |
|
||||
| #3002 | Can't see text under finger | No magnification UI | New `MagnifierLoupe` component using `view.renderer.showLoupe()` |
|
||||
| #3082 | No page numbers on annotations | `pageNumber` field missing | Add `pageNumber` to BookNote type, compute on create |
|
||||
| #3225 | Android tools unresponsive | Premature `makeSelection()` call | Remove premature re-selection in Android path |
|
||||
|
||||
## Common Annotation Bugs
|
||||
|
||||
### Selection Issues
|
||||
- **Word snapping**: Uses `Intl.Segmenter` with `granularity: 'word'` to snap selection to word boundaries
|
||||
- **Android re-selection**: Don't call `makeSelection(sel, index, true)` immediately on pointer-up; let the popup flow complete
|
||||
- **Range editor handles**: Remove `initializedRef` guards that prevent re-computation when switching annotations
|
||||
|
||||
### Color/Style Issues
|
||||
- **Custom colors in sidebar**: Use inline `style={{ backgroundColor: 'color-mix(...)' }}` not Tailwind classes
|
||||
- **Style synchronization**: Consolidate `selectedStyle` and `selectedColor` into one `useEffect`
|
||||
- **Switching annotations**: Must call `setShowAnnotPopup(false)` and `setEditingAnnotation(null)` before setting up new annotation
|
||||
|
||||
## Reader/Content Fixes
|
||||
|
||||
### Progress Display
|
||||
- Use physical `view.renderer.page` and `view.renderer.pages` for page counts (#3213, #3200)
|
||||
- Last page shows 100% by fixing boundary condition (#3383)
|
||||
- FB2 subsections need special handling for progress (#3136)
|
||||
|
||||
### Translation View (#3078)
|
||||
- Problem: Page jumps back during full-text translation
|
||||
- Root cause: DOM mutations from sequential translation insertions cause paginator relayout
|
||||
- Fix: Batch DOM updates with 50ms timer, use bounded concurrent queue (max 5), show loading overlay
|
||||
|
||||
### TOC Navigation (#3124)
|
||||
- Problem: Expanding TOC chapter scrolls back to current chapter
|
||||
- Fix: Only scroll-into-view on navigation, not on expand/collapse
|
||||
|
||||
## Accessibility (a11y) Fixes
|
||||
|
||||
### Screen Reader (TalkBack) Support
|
||||
- **Page indicator updates** (#2276): Add focus handlers on `<p>` elements that call `view.goTo(cfi)` to update position
|
||||
- **Navigation buttons** (#3036): Always show prev/next buttons when screen reader active; `PageNavigationButtons.tsx`
|
||||
- **Dropdown menus** (#3035): Use `DropdownContext` with overlay dismiss instead of blur-based closing
|
||||
|
||||
### Dropdown Architecture for a11y
|
||||
- `DropdownContext` (`src/context/DropdownContext.tsx`) manages which dropdown is open globally
|
||||
- Uses `useId()` for unique identification
|
||||
- One dropdown open at a time
|
||||
- `<Overlay>` for dismissal (tap/click outside) instead of `onBlur`
|
||||
- `<details>` element with `open={isOpen}` for semantic structure
|
||||
- No auto-focus-first-item (conflicts with TalkBack)
|
||||
|
||||
## E-ink Readability
|
||||
- Use `not-eink:` Tailwind variant for colors and opacity (#3258)
|
||||
- Don't use `text-primary` (blue) or low opacity on e-ink
|
||||
- Highlights use foreground color in dark mode e-ink (#3299)
|
||||
|
||||
## Key Utility Functions
|
||||
- `snapRangeToWords()` in `src/utils/sel.ts` - Word boundary snapping
|
||||
- `handleAccessibilityEvents()` in `src/utils/a11y.ts` - Screen reader focus handling
|
||||
- `color-mix()` CSS function for custom highlight colors with opacity
|
||||
@@ -0,0 +1,119 @@
|
||||
# Bug Fixing Patterns & Strategies
|
||||
|
||||
## Common Root Cause Categories
|
||||
|
||||
### 1. Overly Broad CSS Selectors
|
||||
**Pattern:** A CSS rule targets too many elements, causing unintended visual side effects.
|
||||
**Examples:**
|
||||
- `hr { mix-blend-mode: multiply }` applied to ALL hr elements instead of only decorative ones (#3086)
|
||||
- `p img { mix-blend-mode }` applied to block images, not just inline (#3112)
|
||||
- `svg, img { height: auto; width: auto }` overrode explicit HTML width/height attributes (#3274)
|
||||
- Background-color override applied unconditionally instead of only when user enabled color override (#3316)
|
||||
|
||||
**Fix Strategy:** Narrow selectors with class qualifiers (`.background-img`, `.has-text-siblings`) or attribute pseudo-selectors (`:where(:not([width]))`). Check if the rule should be conditional on a user setting.
|
||||
|
||||
### 2. Conditional vs Unconditional Style Overrides
|
||||
**Pattern:** CSS rules meant for "Override Book Color/Layout" mode are placed in the always-active stylesheet.
|
||||
**Examples:**
|
||||
- Calibre `.calibre { color: unset }` was in `getLayoutStyles()` instead of `getColorStyles()` (#3448)
|
||||
- Image background-color override applied without checking `overrideColor` flag (#3316, #3377)
|
||||
|
||||
**Fix Strategy:** Move rules to the correct conditional block: `getColorStyles()` for color overrides, `getLayoutStyles()` for layout overrides. Check the `overrideColor`/`overrideLayout` flags.
|
||||
|
||||
### 3. Missing EPUB Stylesheet Transformations
|
||||
**Pattern:** EPUB stylesheets contain CSS that conflicts with app functionality.
|
||||
**Examples:**
|
||||
- `user-select: none` prevents text selection (#3370) -> regex replace in `transformStylesheet()`
|
||||
- `font-family: serif/sans-serif` on body bypasses user font (#3334) -> detect and unset
|
||||
- Hardcoded Calibre backgrounds persist in dark mode (#3448) -> unset in color override
|
||||
|
||||
**Fix Strategy:** Add regex-based transformation passes in `transformStylesheet()` in `style.ts`.
|
||||
|
||||
### 4. Stale State / Refs Not Reset
|
||||
**Pattern:** A `useRef` or state variable is set once and never properly reset, blocking re-entry.
|
||||
**Examples:**
|
||||
- TTS `ttsOnRef` prevented restarting TTS from a new location (#3292)
|
||||
- `initializedRef` in AnnotationRangeEditor prevented handle position updates (#3286)
|
||||
- `view.tts` not nulled on shutdown prevented clean TTS restart (#3400)
|
||||
- TTS safety timeout fired after pause, advancing to next sentence (#3244)
|
||||
|
||||
**Fix Strategy:** Check all refs/guards in the affected flow. Ensure cleanup in shutdown/unmount. Remove overly aggressive guards that prevent re-entry.
|
||||
|
||||
### 5. Platform API Differences
|
||||
**Pattern:** A Web API behaves differently or is unavailable on certain platforms.
|
||||
**Examples:**
|
||||
- `navigator.getGamepads()` returns null on older Android WebView (#3245)
|
||||
- `CompressionStream` unavailable on some Android versions (#3255)
|
||||
- `btoa()` throws on non-ASCII characters (#3436)
|
||||
- View Transitions API unsupported in WebKitGTK/Linux (#3417)
|
||||
- `document.startViewTransition()` crashes on Linux
|
||||
|
||||
**Fix Strategy:** Always check API availability before use. Add fallback paths. Use feature detection, not platform detection when possible.
|
||||
|
||||
### 6. Safe Area Inset Issues
|
||||
**Pattern:** UI elements overlap system bars (status bar, navigation bar, notch) on mobile.
|
||||
**Examples:**
|
||||
- Zoom controls behind status bar (#3426)
|
||||
- Android navigation bar overlap (#3466)
|
||||
- iPad sidebar insets incorrect (#3395)
|
||||
- Reader page layout jump after system UI change (#3469)
|
||||
|
||||
**Fix Strategy:** Use `gridInsets` and `statusBarHeight` from `useSafeAreaInsets`. Use `env(safe-area-inset-*)` CSS functions. Call `onUpdateInsets()` after system UI visibility changes. See `docs/safe-area-insets.md`.
|
||||
|
||||
### 7. Z-Index Layering Issues
|
||||
**Pattern:** Interactive elements rendered behind other layers, becoming unclickable.
|
||||
**Examples:**
|
||||
- Navigation buttons invisible on mobile (#3201) -> added `z-10`
|
||||
- Annotation nav bar too prominent (#3386) -> reduced from `z-30` to `z-10`
|
||||
- Page nav buttons behind TTS control (#3184)
|
||||
|
||||
**Fix Strategy:** Check z-index ordering. Use minimum necessary z-index. Reference the z-index hierarchy in the codebase.
|
||||
|
||||
### 8. Event Handling Race Conditions
|
||||
**Pattern:** Timing issues between pointer events, native menus, and React state updates.
|
||||
**Examples:**
|
||||
- macOS context menu steals pointer event loop (#3324) -> 100ms setTimeout delay
|
||||
- Traffic light buttons flicker due to timeout race (#3488, #3129)
|
||||
- Android tool buttons unresponsive due to premature re-selection (#3225)
|
||||
|
||||
**Fix Strategy:** Add small delays before native menu calls. Check event state machine consistency. Remove premature re-triggers on Android.
|
||||
|
||||
### 9. foliate-js Rendering Issues
|
||||
**Pattern:** Bugs in the lower-level EPUB renderer (paginator.js, epub.js).
|
||||
**Examples:**
|
||||
- Image size not constrained in double-page mode (#3432)
|
||||
- Background not shown in scrolled mode (#3344)
|
||||
- Section content cached incorrectly after mode switch (#3242, #3206)
|
||||
- Swipe sensitivity too low for non-animated paging (#3310)
|
||||
|
||||
**Fix Strategy:** Check both `columnize()` and `scrolled()` code paths in paginator.js. Verify CSS variables (`--available-width`, `--available-height`) are computed correctly. Test in both paginated and scrolled modes.
|
||||
|
||||
### 10. Progress/Navigation Calculation Errors
|
||||
**Pattern:** Page counts, progress percentages, or position tracking are wrong.
|
||||
**Examples:**
|
||||
- Progress shows 99.9% at last page (#3383) -> boundary condition
|
||||
- Pages left shows estimated instead of physical count (#3213, #3200)
|
||||
- FB2 subsection progress wrong (#3136) -> nested structure not handled
|
||||
- TOC auto-scrolls on expand (#3124) -> scroll-into-view triggered too broadly
|
||||
|
||||
**Fix Strategy:** Use physical `view.renderer.page`/`view.renderer.pages` instead of estimated section metadata. Check boundary conditions (0-indexed vs 1-indexed, inclusive vs exclusive).
|
||||
|
||||
### 11. Multiview Paginator Side Effects
|
||||
**Pattern:** The multiview paginator (e925e9d+) loads adjacent sections in background. Events from these loads can interfere with user interactions on the primary section.
|
||||
**Examples:**
|
||||
- `load` event from adjacent section triggers `docLoadHandler` which re-adds ALL annotations, overwriting drag edits
|
||||
- Multiple overlayers with duplicate SVG `<clipPath>` IDs cause `url(#id)` to resolve to wrong element
|
||||
- `MagnifierLoupe` destroying/recreating body clone on every drag tick triggers ResizeObserver → expand → redraw
|
||||
|
||||
**Fix Strategy:** Scope event handlers to the loaded section's index. Use unique IDs for SVG elements across overlayer instances. Minimize iframe DOM mutations during drag operations.
|
||||
|
||||
## Debugging Workflow
|
||||
|
||||
1. **Identify the category** from the issue description
|
||||
2. **Check `style.ts`** first for any CSS-related visual bugs
|
||||
3. **Check foliate-js** for rendering/layout bugs
|
||||
4. **Check platform-specific code** for mobile/desktop differences
|
||||
5. **Write a failing test** before implementing the fix
|
||||
6. **Test in both paginated and scrolled modes** for layout changes
|
||||
7. **Test on multiple platforms** for any UI change
|
||||
8. **Run `pnpm build-check`** before submitting
|
||||
@@ -0,0 +1,74 @@
|
||||
# CSS & Style Fixes Reference
|
||||
|
||||
## The `style.ts` Pipeline (`src/utils/style.ts`)
|
||||
|
||||
This is the most bug-prone file in the codebase (14+ fixes). It handles all EPUB CSS transformations.
|
||||
|
||||
### Key Functions
|
||||
|
||||
#### `getLayoutStyles()`
|
||||
- Always-active styles applied to every EPUB section
|
||||
- Controls: line-height, hyphens, image sizing, table display
|
||||
- Rules here should NOT be conditional on user settings
|
||||
- Common mistake: putting color-related rules here instead of `getColorStyles()`
|
||||
|
||||
#### `getColorStyles()`
|
||||
- Conditionally applied when user enables "Override Book Color"
|
||||
- Controls: foreground/background colors, mix-blend modes, image backgrounds
|
||||
- Gate rules on `overrideColor` flag
|
||||
|
||||
#### `transformStylesheet()`
|
||||
- Regex-based rewriting of EPUB CSS at load time
|
||||
- Runs on every stylesheet loaded from the EPUB
|
||||
- Used to neutralize problematic EPUB CSS declarations
|
||||
|
||||
#### `applyTableStyle()`
|
||||
- Post-render function that scales tables to fit available width
|
||||
- Uses `getComputedStyle()` (not inline `style.width`) to read actual width
|
||||
- Has two scaling paths: column-width-based and parent-container-based
|
||||
|
||||
### Fix History by Issue
|
||||
|
||||
| Issue | Problem | Fix in style.ts |
|
||||
|-------|---------|-----------------|
|
||||
| #3494 | Line spacing not on `<li>` | Added `li` CSS rule for `line-height` and `hyphens` |
|
||||
| #3448 | Calibre colors persist | Moved `.calibre` unset to `getColorStyles()`, added `background-color: unset` |
|
||||
| #3441 | Body padding/margin | Added `padding: unset; margin: unset` to body in `getLayoutStyles()` |
|
||||
| #3316 | Image bg unconditional | Made `background-color` rule conditional on `overrideColor` |
|
||||
| #3377 | Image bg override | Same pattern as #3316, only override when `overrideColor` is true |
|
||||
| #3334 | Generic font-family | `transformStylesheet()` replaces `font-family: serif/sans-serif` with `unset` on body |
|
||||
| #3370 | user-select: none | `transformStylesheet()` replaces all `user-select: none` with `unset` |
|
||||
| #3284 | Table scaling | Added fallback when `totalTableWidth` is 0 but parent has width |
|
||||
| #3351 | Table display broken | Added `display: table !important` to table rule |
|
||||
| #3274 | Image dimensions | Changed selectors to `:where(:not([width]))` and `:where(:not([height]))` |
|
||||
| #3205 | Table width reading | Changed from `style.width` to `getComputedStyle().width`, fixed CSS var unit |
|
||||
| #3112 | Mix-blend on all images | Narrowed selector to `.has-text-siblings` class |
|
||||
| #3086 | Mix-blend on hr | Narrowed selector to `hr.background-img` |
|
||||
| #3012 | Vertical alignment | Fixed available dimensions (subtract insets), replaced `100vw/vh` with CSS vars |
|
||||
|
||||
### Common Patterns
|
||||
|
||||
1. **Adding new element rules:** Copy the pattern from `p` rules (e.g., adding `li` for #3494)
|
||||
2. **EPUB CSS neutralization:** Add regex in `transformStylesheet()` to replace problematic declarations
|
||||
3. **Conditional overrides:** Use `overrideColor`/`overrideLayout` flags to gate rules
|
||||
4. **Selector narrowing:** Use class qualifiers or attribute pseudo-selectors to avoid over-matching
|
||||
5. **Table fixes:** Always use `getComputedStyle()`, not inline style. Check both width paths.
|
||||
|
||||
### CSS Variables from foliate-js
|
||||
|
||||
- `--available-width` - Usable content width (set by paginator.js)
|
||||
- `--available-height` - Usable content height
|
||||
- `--full-width` - Full viewport width (numeric, multiply by 1px)
|
||||
- `--full-height` - Full viewport height (numeric, multiply by 1px)
|
||||
- `--overlayer-highlight-opacity` - Highlight transparency (default 0.3)
|
||||
|
||||
### foliate-js Rendering (`packages/foliate-js/paginator.js`)
|
||||
|
||||
Key functions:
|
||||
- `columnize()` - Paginated layout path
|
||||
- `scrolled()` - Scrolled layout path
|
||||
- `setImageSize()` - Constrains image dimensions to available space
|
||||
- `#replaceBackground()` - Transfers EPUB backgrounds to paginator layer
|
||||
- `snap()` - Swipe gesture detection for page turning
|
||||
|
||||
Common issue: A fix applied to `columnize()` but not `scrolled()` (or vice versa). Always check both paths.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: D-pad Navigation Design
|
||||
description: Android TV / Bluetooth remote D-pad navigation architecture, key files, and pitfalls encountered during implementation
|
||||
type: project
|
||||
---
|
||||
|
||||
## D-pad Navigation Architecture
|
||||
|
||||
D-pad support enables Bluetooth remote controller navigation on Android TV (and keyboard arrow navigation on desktop).
|
||||
|
||||
### Key Files
|
||||
|
||||
- `src/app/reader/hooks/useSpatialNavigation.ts` — Reader toolbar D-pad navigation. Left/Right navigates between buttons, Up/Down moves between header↔footer. Auto-focuses first button on show. Uses focus-probe technique for visibility detection.
|
||||
- `src/app/library/hooks/useSpatialNavigation.ts` — Library grid D-pad navigation. Arrow keys move between BookshelfItem elements. ArrowDown from outside bookshelf (e.g. header) enters the grid via window-level listener.
|
||||
- `src/helpers/shortcuts.ts` — `onToggleToolbar` (Enter key) toggles reader toolbar visibility.
|
||||
- `src/app/reader/hooks/useBookShortcuts.ts` — `toggleToolbar` handler shows/hides header+footer bars. Skips when a `<button>` is focused (lets native click fire).
|
||||
- `src/__tests__/hooks/useSpatialNavigation.test.tsx` — Unit tests for reader toolbar navigation.
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- **No third-party library**: Tried `@noriginmedia/norigin-spatial-navigation` but it failed due to init timing issues (React child effects run before parent effects) and conflicts with the existing `useShortcuts` system. Custom solution is simpler and more reliable.
|
||||
- **Two `useSpatialNavigation` hooks**: Same name in different directories — library version handles grid navigation, reader version handles toolbar button navigation. Different navigation patterns but same concept.
|
||||
- **Platform-agnostic hooks**: Both `useSpatialNavigation` hooks work on all platforms, not just Android.
|
||||
- **Focus-probe for visibility**: `offsetParent` is unreliable for detecting visible buttons (returns null inside `position: fixed` containers on mobile). Instead, try `btn.focus()` and check if `document.activeElement === btn` — this correctly handles all hiding methods (display:none, visibility:hidden, fixed positioning).
|
||||
|
||||
### Pitfalls
|
||||
|
||||
1. **WebView spatial navigation conflict**: Android WebView has built-in spatial navigation that intercepts D-pad arrow keys and moves DOM focus between `tabIndex>=0` elements. Added `tabIndex={-1}` to non-interactive overlay elements (HeaderBar trigger, ProgressBar, FooterBar trigger, SectionInfo) to prevent focus theft.
|
||||
|
||||
2. **`eventDispatcher.dispatchSync` short-circuits**: When multiple handlers are registered for `native-key-down`, the first handler returning `true` stops propagation. The FooterBar's Back handler fires before the Reader's. Both must independently call `blur()` — can't rely on the Reader's handler running.
|
||||
|
||||
3. **Must blur on toolbar dismiss**: When Back/Escape dismisses the toolbar, the focused button must be blurred. Otherwise `document.activeElement` remains a hidden button, and `toggleToolbar` skips Enter when `activeElement.tagName === 'BUTTON'`. Blur is called in FooterBar's handleKeyDown (for Back and Escape) and in Reader's handleKeyDown.
|
||||
|
||||
4. **Arrow key trapping must use `stopPropagation`**: Without it, arrow keys bubble to `window` where `useShortcuts` handles them as page turns. The toolbar keydown handler on the container div calls `e.stopPropagation()` + `e.preventDefault()` to prevent this.
|
||||
|
||||
5. **Library grid needs window-level listener**: The bookshelf container keydown handler only fires when focus is inside it. A separate `window` keydown listener handles ArrowDown from the header into the grid (when focus is outside the container).
|
||||
|
||||
6. **Auto-focus race on toolbar show**: Both header and footer bars auto-focus their first button when `isVisible` becomes true simultaneously. The last effect to run wins. This is acceptable — user can navigate between them with Up/Down.
|
||||
|
||||
7. **`offsetParent` null in fixed containers**: On mobile, `.footer-bar` uses `position: fixed`. All child buttons have `offsetParent === null`, making `offsetParent`-based visibility checks useless. The focus-probe approach (try focus, check activeElement) is the reliable alternative.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: gstack upgrade location
|
||||
description: Always upgrade gstack from the project directory (.claude/skills/gstack), not from a global install
|
||||
type: feedback
|
||||
---
|
||||
|
||||
When upgrading gstack, always run the upgrade from the current project's `.claude/skills/gstack` directory (local-git install), not from a global install path.
|
||||
|
||||
**Why:** The project uses a local-git gstack install at `apps/readest-app/.claude/skills/gstack`. Previous mistakes upgraded a global copy while the project's local copy stayed outdated.
|
||||
|
||||
**How to apply:** When `/gstack-upgrade` is invoked, ensure the `cd` and `git reset --hard origin/main && ./setup` happen inside the project's `.claude/skills/gstack` directory.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Always use a new branch for new PRs
|
||||
description: Each new PR/issue should get its own fresh branch from main, never reuse an existing feature branch
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Always create a new branch from main for each new PR or issue. Never reuse an existing feature branch for unrelated work.
|
||||
|
||||
**Why:** The user corrected this when a storage fix was committed on the `feat/full-sync-annotations` branch instead of a dedicated branch. Mixing unrelated changes on the same branch makes PRs harder to review and manage.
|
||||
|
||||
**How to apply:** Before committing fixes, create a new branch like `fix/<topic>` from `origin/main`. Only reuse a branch if the work is directly related to that branch's existing purpose.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Always rebase before PR
|
||||
description: Rebase to origin/main before creating pull requests
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Always rebase the branch onto origin/main before creating a pull request.
|
||||
|
||||
**Why:** The user wants PRs to be up-to-date with main to avoid merge conflicts and keep a clean history.
|
||||
|
||||
**How to apply:** Before running `gh pr create`, always run `git fetch origin && git rebase origin/main` first. If there are conflicts, resolve them before proceeding.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Layout & UI Fixes Reference
|
||||
|
||||
## Safe Area Insets
|
||||
|
||||
### Architecture
|
||||
- Native plugins push inset values: iOS (`NativeBridgePlugin.swift`), Android (`NativeBridgePlugin.kt`)
|
||||
- `useSafeAreaInsets` hook (`src/hooks/useSafeAreaInsets.ts`) reads and caches values
|
||||
- Components use `gridInsets` for positioning relative to safe areas
|
||||
- CSS: `env(safe-area-inset-top/bottom/left/right)` for CSS-level insets
|
||||
|
||||
### Common Issues
|
||||
- **Stale insets after system UI change** (#3469): Call `onUpdateInsets()` after `setSystemUIVisibility()`
|
||||
- **iPad sidebar insets wrong** (#3395): Different inset handling needed for sidebar vs main view
|
||||
- **Android nav bar overlap** (#3466): Use `calc(env(safe-area-inset-bottom) + 16px)` for Android bottom padding
|
||||
- **Zoom controls behind status bar** (#3426): Pass `gridInsets` through component chain, use `Math.max(gridInsets.top, statusBarHeight)`
|
||||
|
||||
### Rules (see also `docs/safe-area-insets.md`)
|
||||
- Always pass `gridInsets` to overlay/floating components near screen edges
|
||||
- On Android, account for system navigation bar with `env(safe-area-inset-bottom)`
|
||||
- After toggling system UI visibility, force-refresh insets via `onUpdateInsets()`
|
||||
|
||||
## Z-Index Hierarchy
|
||||
- Navigation buttons: `z-10` (when visible)
|
||||
- Annotation nav bar: `z-10` (reduced from z-30 in #3386)
|
||||
- TTS control button: ensure above page nav buttons
|
||||
- Floating overlays: check against `gridInsets` positioning
|
||||
|
||||
## macOS Traffic Lights
|
||||
- Managed by `trafficLightStore.ts` and `HeaderBar.tsx`
|
||||
- **Fullscreen check required** (#3129): `await currentWindow.isFullscreen()` before hiding
|
||||
- **Timeout for visibility toggle** (#3488): Use 100ms delay to prevent flickering
|
||||
- **Sidebar interaction** (#3488): Check `getIsSideBarVisible()` before hiding traffic lights
|
||||
- Never hide traffic lights when sidebar is open
|
||||
|
||||
## Touch/Input Issues
|
||||
- **Slider hit area on iOS** (#3382): Use min-h-12, strip browser appearance with CSS
|
||||
- **Context menu on macOS** (#3324): 100ms delay before `onContextMenu()` to let pointer events complete
|
||||
- **Swipe sensitivity** (#3310): Use average velocity (distance/time) instead of instantaneous velocity for non-animated paging
|
||||
- **Touchpad natural scrolling** (#3127): Respect system natural scrolling setting in `usePagination.ts`
|
||||
|
||||
## Dialog/Menu Layout
|
||||
- **Dialog header** (#3352): Use `px-2 sm:pe-3 sm:ps-2` padding to align with border radius
|
||||
- **Settings alignment** (#3151): Use `Menu` component instead of raw `div` for consistent styling
|
||||
- **Dropdown for screen readers** (#3035): Use `DropdownContext` with overlay dismiss, not blur-based closing
|
||||
|
||||
## Component-Specific Fixes
|
||||
|
||||
### HeaderBar.tsx
|
||||
- Traffic light visibility management
|
||||
- Sidebar toggle persistent position (#3193)
|
||||
- Library button placement
|
||||
|
||||
### PageNavigationButtons.tsx
|
||||
- z-10 when visible (#3201)
|
||||
- Always shown for screen readers (#3036)
|
||||
- Toggle via `showPageNavButtons` setting
|
||||
|
||||
### ProgressInfo.tsx
|
||||
- Use physical `page`/`pages` from renderer, not estimated values (#3213, #3200)
|
||||
- CSS classes: `time-left-label`, `pages-left-label`, `progress-info-label` (#3343)
|
||||
|
||||
### ReadingRuler.tsx
|
||||
- Remove `containerStyle` from overlay so dimmed area covers full screen (#3304)
|
||||
|
||||
### NavigationBar.tsx
|
||||
- Handle `gridInsets` internally, not via pre-computed `navPadding` (#3466)
|
||||
|
||||
### ContentNavBar.tsx (annotation search results)
|
||||
- Floating buttons with drop shadow, not full-width bar (#3386)
|
||||
- z-10 z-index
|
||||
@@ -0,0 +1,85 @@
|
||||
# Platform Compatibility Fixes Reference
|
||||
|
||||
## Android
|
||||
|
||||
### WebView API Issues
|
||||
- **`navigator.getGamepads()`** returns null on older WebView (#3245): Always null-check before `.some()`
|
||||
- **`CompressionStream`** unavailable on some Android WebView versions (#3255): Add fallback zip compression path
|
||||
- **Annotation tools unresponsive** (#3225): Don't call `makeSelection()` immediately on pointer-up; let popup flow complete naturally
|
||||
- **Safe inset updates** (#3469): Call `onUpdateInsets()` after `setSystemUIVisibility()`
|
||||
- **Navigation bar overlap** (#3466): Use `calc(env(safe-area-inset-bottom) + 16px)` for bottom padding
|
||||
|
||||
### General Android Rules
|
||||
- Test with older WebView versions (97+)
|
||||
- Always check API availability before calling Web APIs
|
||||
- Touch event handling differs from iOS - avoid premature re-selection
|
||||
|
||||
## iOS
|
||||
|
||||
### Common Issues
|
||||
- **Slider touch dead zones** (#3382): Strip native appearance, use larger hit areas (min-h-12)
|
||||
- **Safe area insets stale** (#3395): Native Swift plugin must push updated insets
|
||||
- **Section content caching** (#3242, #3206): Don't cache section content in foliate-js when updating subitems; cached content retains stale styles after mode switch
|
||||
- **CompressionStream** (#3255): Also broken on iOS 15.x; zip.js has its own native API disable
|
||||
- **zip.js native API** (#3170): Disable native `CompressionStream`/`DecompressionStream` on iOS 15.x
|
||||
|
||||
### iOS-Specific Code
|
||||
- `src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift`
|
||||
- Slider CSS: `-webkit-appearance: none; appearance: none` in globals.css
|
||||
- `useSafeAreaInsets.ts` hook
|
||||
|
||||
## macOS
|
||||
|
||||
### Traffic Lights (Window Controls)
|
||||
- Check `isFullscreen()` before hiding (#3129)
|
||||
- Use timeouts (100ms) for visibility transitions (#3488)
|
||||
- Don't hide when sidebar is open (#3488)
|
||||
|
||||
### Input Issues
|
||||
- **Context menu steals event loop** (#3324): 100ms setTimeout before calling menu.popup()
|
||||
- **Touchpad natural scrolling** (#3127): Respect system setting in `usePagination.ts`
|
||||
- **Two-finger swipe** (#3127): Support trackpad two-finger swipe for pagination
|
||||
|
||||
### macOS-Specific Code
|
||||
- `src-tauri/src/macos/` - Platform-specific Rust code
|
||||
- `src/store/trafficLightStore.ts`
|
||||
- `src/hooks/useTrafficLight.ts`
|
||||
|
||||
## Linux
|
||||
|
||||
### WebKitGTK Issues
|
||||
- **View Transitions API unsupported** (#3417): Feature-detect `document.startViewTransition` before calling
|
||||
- Use `useAppRouter.ts` to avoid transitions on Linux
|
||||
|
||||
## E-ink Devices
|
||||
|
||||
### Legibility Issues
|
||||
- **Low contrast colors** (#3258): Use `not-eink:` Tailwind variant prefix for colors/opacity
|
||||
- **Links invisible** (#3258): Don't apply `text-primary` (blue) on e-ink; use default text color
|
||||
- **Opacity too low** (#3258): Don't apply `opacity-60`/`opacity-75` on e-ink devices
|
||||
- **Highlight visibility** (#3299): Use foreground color for highlights in dark mode e-ink
|
||||
|
||||
### E-ink CSS Pattern
|
||||
```
|
||||
// Instead of:
|
||||
className="text-primary opacity-60"
|
||||
// Use:
|
||||
className="not-eink:text-primary not-eink:opacity-60"
|
||||
```
|
||||
|
||||
## Docker/Self-Hosting
|
||||
- **Missing submodules** (#3233): Run `git submodule update --init --recursive` before build
|
||||
- Simplecc WASM module must be initialized
|
||||
|
||||
## OPDS
|
||||
- **Non-ASCII credentials** (#3436): Use `TextEncoder` + manual Base64 instead of `btoa()`
|
||||
- **Author parsing** (#3120): Handle varied metadata structures in OPDS 2.0 feeds
|
||||
- **Responsive layout** (#3418): Ensure catalog and download button layout works on small screens
|
||||
|
||||
## Cross-Platform Testing Checklist
|
||||
1. Android (old WebView + current)
|
||||
2. iOS (15.x + current)
|
||||
3. macOS (traffic lights, trackpad)
|
||||
4. Linux (WebKitGTK)
|
||||
5. E-ink devices (contrast, colors)
|
||||
6. Web (CloudFlare Workers deployment)
|
||||
@@ -0,0 +1,50 @@
|
||||
# TTS (Text-to-Speech) Fixes Reference
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Components
|
||||
- `TTSController` (`src/services/tts/TTSController.ts`) - Core state machine
|
||||
- `EdgeTTSClient` (`src/services/tts/EdgeTTSClient.ts`) - Edge TTS provider
|
||||
- `useTTSControl` hook (`src/app/reader/hooks/useTTSControl.ts`) - React integration
|
||||
- `useTTSMediaSession` hook (`src/app/reader/hooks/useTTSMediaSession.ts`) - Media controls
|
||||
|
||||
### Section-Aware TTS Model
|
||||
TTS tracks its own section independently from the view via `#ttsSectionIndex`:
|
||||
- `#initTTSForSection()` - Creates TTS document for a section without changing the view
|
||||
- `#initTTSForNextSection()` / `#initTTSForPrevSection()` - Navigate TTS across sections
|
||||
- `#getHighlighter()` - Only returns highlighter if view section matches TTS section
|
||||
- `onSectionChange` callback - Notifies UI when TTS crosses section boundary
|
||||
- Highlights use CFI strings (not raw Range objects) for cross-section compatibility
|
||||
|
||||
### State Management Pitfalls
|
||||
1. **`#ttsSectionIndex` must match view section for highlights to work**
|
||||
- If `-1`, all highlight calls are suppressed
|
||||
- `shutdown()` sets it to `-1` but must also null out `this.view.tts`
|
||||
|
||||
2. **Guards/Refs that block re-entry:**
|
||||
- The old `ttsOnRef` guard blocked TTS restart from annotations (removed in #3292)
|
||||
- `view.tts` reference surviving shutdown blocked re-initialization (#3400)
|
||||
|
||||
3. **Timeouts that fire after pause:**
|
||||
- Edge TTS had a safety timeout that advanced sentences even when paused (#3244)
|
||||
- Solution: removed the entire `ontimeupdate` safety timeout mechanism
|
||||
|
||||
## Fix History
|
||||
|
||||
| Issue | Problem | Root Cause | Fix |
|
||||
|-------|---------|------------|-----|
|
||||
| #3100 | TTS scrolls too far | TTS coupled to view section | Added `#ttsSectionIndex`, "Back to TTS Location" button |
|
||||
| #3198 | TTS doesn't follow to next section | No `onSectionChange` callback | Added section change notification, extracted hooks |
|
||||
| #3244 | Paused TTS advances | Safety timeout fires after pause | Removed `ontimeupdate` timeout mechanism |
|
||||
| #3291 | TTS fails without lang attribute | Invalid SSML from missing lang | Set lang/xml:lang on html element from `ttsLang` |
|
||||
| #3292 | Can't restart TTS from annotation | `ttsOnRef` blocks re-entry | Removed the guard ref entirely |
|
||||
| #3400 | TTS highlight stops after restart | `view.tts` not nulled on shutdown | Added `this.view.tts = null` in `shutdown()` |
|
||||
|
||||
## Debugging TTS Issues
|
||||
|
||||
1. **TTS doesn't start:** Check `#initTTSForSection()` - does `view.tts.doc === doc` shortcut early?
|
||||
2. **No highlights:** Check `#ttsSectionIndex` matches view's section index
|
||||
3. **Advances when paused:** Look for setTimeout/timer callbacks that bypass pause state
|
||||
4. **Can't restart:** Check for refs/guards that prevent re-entry into speak handlers
|
||||
5. **Fails on some chapters:** Check if chapter has lang attribute and XHTML namespace
|
||||
6. **SSML errors:** Check `src/utils/ssml.ts` for proper namespace/lang handling
|
||||
@@ -0,0 +1,5 @@
|
||||
## Test-First Development
|
||||
|
||||
- Always write a failing unit test **before** implementing a fix.
|
||||
- Run the test to confirm it reproduces the bug or fails as expected, then apply the fix and verify the test passes.
|
||||
- Run the full test suite (`pnpm test`) after changes to ensure no regressions.
|
||||
@@ -0,0 +1,5 @@
|
||||
## TypeScript
|
||||
|
||||
- Never use the `any` type. Use `unknown`, proper types, or generics instead.
|
||||
- Strict mode is enabled. Target is ES2022.
|
||||
- Unused vars prefixed with `_` are allowed (ESLint configured).
|
||||
@@ -0,0 +1,8 @@
|
||||
## Verification (done-conditions)
|
||||
|
||||
Before marking work complete, all applicable checks must pass:
|
||||
|
||||
1. `pnpm test` — unit tests
|
||||
2. `pnpm lint` — ESLint
|
||||
3. `pnpm fmt:check` — Rust format check (only when `src-tauri/` files changed)
|
||||
4. `pnpm clippy:check` — Rust lint (only when `src-tauri/` files changed)
|
||||
Submodule
+1
Submodule apps/readest-app/.claude/skills/gstack added at 6169273d16
@@ -3,7 +3,7 @@ PDFJS_FONTS_PATH=../../packages/foliate-js/node_modules/pdfjs-dist
|
||||
PDFJS_STYLE_PATH=../../packages/foliate-js/vendor/pdfjs
|
||||
|
||||
NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64="aHR0cHM6Ly91cy5pLnBvc3Rob2cuY29t"
|
||||
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThpTmRSNTNsR1A3VFM3VGh4S08="
|
||||
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX0x3ekhZRWtsZUVub3ZSc05ZQlRpTVRTV2MyS1NUOFdZMzBIWWFhN0ZPa1IK"
|
||||
|
||||
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv"
|
||||
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw=="
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
.test-sandbox-node/
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
@@ -48,6 +49,9 @@ tauri.*.conf.json
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
|
||||
#generated
|
||||
src-tauri/gen
|
||||
|
||||
@@ -60,3 +64,8 @@ src-tauri/gen
|
||||
/public/fallback-*.js
|
||||
/public/swe-worker-*.js
|
||||
|
||||
/dist/
|
||||
|
||||
.claude/settings.local.json
|
||||
.claude/skills
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
## Project Overview
|
||||
|
||||
Readest is a cross-platform ebook reader built as a **Next.js 16 + Tauri v2** hybrid app. It's part of a pnpm monorepo at `/apps/readest-app/`. The app runs on web (CloudFlare Workers), desktop (macOS/Windows/Linux via Tauri), and mobile (iOS/Android via Tauri).
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm dev-web # Web-only dev server (no Rust compilation needed)
|
||||
pnpm tauri dev # Desktop dev with Tauri (compiles Rust backend)
|
||||
|
||||
# Building
|
||||
pnpm build # Build Next.js for Tauri
|
||||
pnpm build-web # Build Next.js for web deployment
|
||||
|
||||
# Testing (see [docs/testing.md](docs/testing.md) for full details)
|
||||
pnpm test # Unit tests (vitest + jsdom)
|
||||
pnpm test -- src/__tests__/utils/misc.test.ts # Run a single test file
|
||||
pnpm test -- --watch # Watch mode
|
||||
pnpm test:browser # Browser tests (Chromium via Playwright)
|
||||
pnpm tauri:dev:test # Start Tauri app with webdriver
|
||||
pnpm test:tauri # Run Tauri integration tests
|
||||
|
||||
# Linting & Formatting
|
||||
pnpm lint # Biome (linter) + tsgo (type check)
|
||||
pnpm format # Prettier (runs from monorepo root)
|
||||
pnpm format:check # Check formatting without writing
|
||||
|
||||
# Rust
|
||||
pnpm fmt:check # Check formatting Rust code (src-tauri)
|
||||
pnpm clippy:check # Lint Rust code (src-tauri)
|
||||
```
|
||||
|
||||
### Source Layout
|
||||
|
||||
| Directory | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| `src/app/` | Next.js App Router pages and API routes |
|
||||
| `src/components/` | React components (reader, settings, library, assistant, etc.) |
|
||||
| `src/services/` | Business logic: TTS, translators, OPDS, sync, AI, metadata |
|
||||
| `src/store/` | Zustand state stores |
|
||||
| `src/hooks/` | Custom React hooks |
|
||||
| `src/libs/` | Document loaders, payment, storage, sync |
|
||||
| `src/utils/` | Pure utility functions |
|
||||
| `src/types/` | TypeScript type definitions |
|
||||
| `src/context/` | React Context providers (Auth, Env, Sync, etc.) |
|
||||
| `src/workers/` | Web Workers for background tasks |
|
||||
| `src-tauri/` | Rust backend: Tauri plugins, platform-specific code |
|
||||
|
||||
### Path Aliases (tsconfig)
|
||||
|
||||
- `@/*` → `./src/*`
|
||||
- `@/components/ui/*` → `./src/components/primitives/*`
|
||||
|
||||
### Rust Backend (`src-tauri/`)
|
||||
|
||||
Platform-specific code lives in `src-tauri/src/{macos,windows,android,ios}/`. Custom Tauri plugins are in `src-tauri/plugins/`.
|
||||
|
||||
## Project Rules
|
||||
|
||||
Rules are in `.claude/rules/`: test-first, typescript, verification.
|
||||
|
||||
### 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.
|
||||
|
||||
## Web Browsing & QA (gstack)
|
||||
|
||||
For all web browsing, QA testing, and site interaction, use the `/browse` skill from gstack. **Never use `mcp__claude-in-chrome__*` tools directly.**
|
||||
|
||||
Available gstack skills:
|
||||
|
||||
- `/plan-ceo-review` — CEO/founder-mode plan review
|
||||
- `/plan-eng-review` — Eng manager-mode plan review
|
||||
- `/plan-design-review` — Designer's eye review of a live site
|
||||
- `/design-consultation` — Design system consultation
|
||||
- `/review` — Pre-landing PR review
|
||||
- `/ship` — Ship workflow (merge, test, review, bump, PR)
|
||||
- `/browse` — Fast headless browser for QA and site interaction
|
||||
- `/qa` — QA test and fix bugs
|
||||
- `/qa-only` — QA report only (no fixes)
|
||||
- `/qa-design-review` — Designer's eye QA with fixes
|
||||
- `/setup-browser-cookies` — Import cookies for authenticated testing
|
||||
- `/retro` — Weekly engineering retrospective
|
||||
- `/document-release` — Post-ship documentation update
|
||||
|
||||
If gstack skills aren't working, run `cd .claude/skills/gstack && ./setup` to build the binary and register skills.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
|
||||
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
||||
"files": { "ignoreUnknown": true },
|
||||
"formatter": { "enabled": false },
|
||||
"assist": { "enabled": false },
|
||||
"css": { "linter": { "enabled": false } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"includes": [
|
||||
"**",
|
||||
"!.next/**",
|
||||
"!.open-next/**",
|
||||
"!.wrangler/**",
|
||||
"!.claude/**",
|
||||
"!dist/**",
|
||||
"!out/**",
|
||||
"!build/**",
|
||||
"!public/**",
|
||||
"!src-tauri/**",
|
||||
"!next-env.d.ts",
|
||||
"!i18next-scanner.config.cjs"
|
||||
],
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
"recommended": true,
|
||||
"useKeyWithClickEvents": "off",
|
||||
"useKeyWithMouseEvents": "off",
|
||||
"noSvgWithoutTitle": "off",
|
||||
"noLabelWithoutControl": "off",
|
||||
"useSemanticElements": "off",
|
||||
"noAriaHiddenOnFocusable": "off",
|
||||
"noInteractiveElementToNoninteractiveRole": "off",
|
||||
"noNoninteractiveElementToInteractiveRole": "off",
|
||||
"noNoninteractiveElementInteractions": "off",
|
||||
"noStaticElementInteractions": "off",
|
||||
"noNoninteractiveTabindex": "off",
|
||||
"useButtonType": "off",
|
||||
"useAriaPropsSupportedByRole": "off",
|
||||
"useFocusableInteractive": "off",
|
||||
"noAutofocus": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noStaticOnlyClass": "off",
|
||||
"noUselessSwitchCase": "off",
|
||||
"noUselessFragments": "off",
|
||||
"noUselessCatch": "off",
|
||||
"useLiteralKeys": "off",
|
||||
"useOptionalChain": "off",
|
||||
"noThisInStatic": "off",
|
||||
"useArrowFunction": "off",
|
||||
"noUselessEscapeInRegex": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn",
|
||||
"noUnusedImports": "error",
|
||||
"useExhaustiveDependencies": "off",
|
||||
"useHookAtTopLevel": "error",
|
||||
"useJsxKeyInIterable": "error",
|
||||
"noChildrenProp": "error",
|
||||
"noNextAsyncClientComponent": "warn",
|
||||
"noSwitchDeclarations": "off",
|
||||
"noUndeclaredVariables": "off",
|
||||
"noEmptyCharacterClassInRegex": "off",
|
||||
"useParseIntRadix": "off",
|
||||
"noEmptyPattern": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"noBeforeInteractiveScriptOutsideDocument": "warn",
|
||||
"noDuplicateEnumValues": "error",
|
||||
"noSyncScripts": "warn",
|
||||
"useInlineScriptId": "error"
|
||||
},
|
||||
"performance": {
|
||||
"noImgElement": "off",
|
||||
"noUnwantedPolyfillio": "warn",
|
||||
"useGoogleFontPreconnect": "warn"
|
||||
},
|
||||
"security": {
|
||||
"noBlankTarget": "off",
|
||||
"noDangerouslySetInnerHtmlWithChildren": "off",
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off",
|
||||
"useImportType": "off",
|
||||
"noParameterAssign": "off",
|
||||
"useDefaultParameterLast": "off",
|
||||
"noUselessElse": "off",
|
||||
"noHeadElement": "warn",
|
||||
"noCommonJs": "off",
|
||||
"useFilenamingConvention": "off",
|
||||
"useNamingConvention": "off",
|
||||
"noUnusedTemplateLiteral": "off",
|
||||
"useTemplate": "off",
|
||||
"useExponentiationOperator": "off",
|
||||
"useNodejsImportProtocol": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noArrayIndexKey": "off",
|
||||
"noAssignInExpressions": "off",
|
||||
"noDoubleEquals": "off",
|
||||
"noDocumentImportInPage": "error",
|
||||
"noHeadImportInDocument": "error",
|
||||
"useGoogleFontDisplay": "warn",
|
||||
"noCommentText": "error",
|
||||
"noDuplicateJsxProps": "error",
|
||||
"noAsyncPromiseExecutor": "off",
|
||||
"noImplicitAnyLet": "off",
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noEmptyBlockStatements": "off",
|
||||
"useIterableCallbackReturn": "off",
|
||||
"noGlobalIsNan": "off",
|
||||
"noConfusingVoidType": "off",
|
||||
"noConstEnum": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"globals": ["React"]
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,52 @@
|
||||
## Safe Area Insets
|
||||
|
||||
The app runs on devices with notches, status bars, and rounded corners (iOS, Android). UI elements near screen edges must account for safe area insets to avoid being obscured.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **`gridInsets: Insets`** — Per-view insets derived from view settings (header/footer visibility, margins). Calculated by `getViewInsets()` in `src/utils/insets.ts`. Passed as a prop from `BooksGrid` → child components.
|
||||
- **`statusBarHeight: number`** — OS status bar height (default 24px). Stored in `themeStore`.
|
||||
- **`systemUIVisible: boolean`** — Whether the system UI (status bar, navigation bar) is currently shown. Stored in `themeStore`.
|
||||
- **`appService?.hasSafeAreaInset`** — Whether the platform requires safe area handling (mobile devices).
|
||||
|
||||
### Top Inset Rules
|
||||
|
||||
For UI elements anchored to the **top** of the screen (headers, close buttons, overlays):
|
||||
|
||||
```tsx
|
||||
// When system UI is visible, use the larger of gridInsets.top and statusBarHeight
|
||||
// When system UI is hidden, use gridInsets.top alone
|
||||
style={{
|
||||
marginTop: systemUIVisible
|
||||
? `${Math.max(gridInsets.top, statusBarHeight)}px`
|
||||
: `${gridInsets.top}px`,
|
||||
}}
|
||||
```
|
||||
|
||||
For containers that need safe area padding at the top:
|
||||
|
||||
```tsx
|
||||
style={{
|
||||
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
|
||||
}}
|
||||
```
|
||||
|
||||
### Bottom Inset Rules
|
||||
|
||||
For UI elements anchored to the **bottom** of the screen (footer bars, controls, progress indicators), use `gridInsets.bottom * 0.33` as padding — a fraction of the full inset since bottom bars don't need as much clearance as the home indicator area:
|
||||
|
||||
```tsx
|
||||
style={{
|
||||
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0,
|
||||
}}
|
||||
```
|
||||
|
||||
### Passing `gridInsets`
|
||||
|
||||
When creating overlay components (image viewers, table viewers, zoom controls, etc.), always pass `gridInsets` as a prop so they can position their controls correctly:
|
||||
|
||||
```tsx
|
||||
<ImageViewer gridInsets={gridInsets} ... />
|
||||
<TableViewer gridInsets={gridInsets} ... />
|
||||
<ZoomControls gridInsets={gridInsets} ... />
|
||||
```
|
||||
@@ -0,0 +1,110 @@
|
||||
# Testing
|
||||
|
||||
Readest uses three test tiers, all powered by [Vitest](https://vitest.dev/).
|
||||
|
||||
## Unit Tests (`pnpm test`)
|
||||
|
||||
Runs tests in a **jsdom** environment. No browser or Tauri runtime required.
|
||||
|
||||
```bash
|
||||
pnpm test # Run all unit tests
|
||||
pnpm test -- src/__tests__/utils/misc.test.ts # Run a single file
|
||||
pnpm test -- --watch # Watch mode
|
||||
```
|
||||
|
||||
- **Config:** `vitest.config.mts`
|
||||
- **Pattern:** `src/**/*.test.ts` (excludes `*.browser.test.ts` and `*.tauri.test.ts`)
|
||||
- **Environment:** jsdom
|
||||
- **Use for:** Pure logic, utilities, services that don't need real browser APIs or Tauri IPC.
|
||||
|
||||
## Browser Tests (`pnpm test:browser`)
|
||||
|
||||
Runs tests in a **real Chromium** browser via Playwright. Required for code that depends on Web Workers, SharedArrayBuffer, OPFS, or other browser-only APIs.
|
||||
|
||||
```bash
|
||||
pnpm test:browser
|
||||
```
|
||||
|
||||
- **Config:** `vitest.browser.config.mts`
|
||||
- **Pattern:** `src/**/*.browser.test.ts`
|
||||
- **Browser:** Chromium (headless, via `@vitest/browser-playwright`)
|
||||
- **Use for:** WASM modules (e.g. `@tursodatabase/database-wasm`), Web Worker integration, browser-specific storage APIs.
|
||||
|
||||
## Tauri Integration Tests (`pnpm test:tauri`)
|
||||
|
||||
Runs Vitest tests **inside the Tauri WebView**, with access to Tauri IPC and native plugin commands. Tests execute in the actual app environment.
|
||||
|
||||
### Step 1: Start the Tauri App
|
||||
|
||||
In one terminal, start the app with the `webdriver` feature enabled:
|
||||
|
||||
```bash
|
||||
pnpm tauri:dev:test # Dev mode (uses tauri dev server, faster iteration)
|
||||
pnpm tauri:build:test # Debug release build (closer to production)
|
||||
```
|
||||
|
||||
These commands compile the Rust backend with `--features webdriver`, which:
|
||||
|
||||
- Includes `tauri-plugin-webdriver` (embeds a W3C WebDriver server on port 4445)
|
||||
- Adds a runtime capability granting plugin permissions to remote URLs (`http://127.0.0.1:*`), so Vitest's browser-mode iframe can call Tauri IPC
|
||||
|
||||
Keep this running while you run tests.
|
||||
|
||||
### Step 2: Run Tests
|
||||
|
||||
In another terminal:
|
||||
|
||||
```bash
|
||||
pnpm test:tauri
|
||||
```
|
||||
|
||||
Vitest connects directly to the embedded WebDriver server (port 4445) in the running Tauri app and executes tests inside its WebView.
|
||||
|
||||
- **Config:** `vitest.tauri.config.mts`
|
||||
- **Pattern:** `src/**/*.tauri.test.ts`
|
||||
- **Browser provider:** `@vitest/browser-webdriverio` (connects to port 4445)
|
||||
- **Use for:** Tauri plugin commands (turso, native-tts, etc.), native filesystem, Tauri IPC.
|
||||
|
||||
### Writing Tauri Tests
|
||||
|
||||
Tests access Tauri IPC via a shared helper:
|
||||
|
||||
```typescript
|
||||
import { invoke } from '../tauri/tauri-invoke';
|
||||
|
||||
it('calls a plugin command', async () => {
|
||||
const result = await invoke('plugin:turso|load', { options: { path: 'sqlite::memory:' } });
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
The `invoke()` helper accesses `window.top.__TAURI_INTERNALS__` (Vitest runs in an iframe, Tauri injects IPC into the main frame).
|
||||
|
||||
**Limitations:** Only custom invoke commands and plugin commands listed in the webdriver capability work. Standard Tauri JS APIs (e.g. `@tauri-apps/api`) that rely on `URL: local` may not work from the Vitest iframe.
|
||||
|
||||
## E2E Tests (WDIO)
|
||||
|
||||
Full end-to-end tests using WebDriverIO, for UI-level testing against the running Tauri app. Same two-step workflow as Tauri integration tests.
|
||||
|
||||
```bash
|
||||
# Terminal 1: start the app (same as for Tauri integration tests)
|
||||
pnpm tauri:dev:test
|
||||
|
||||
# Terminal 2: run E2E tests
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
- **Config:** `wdio.conf.ts`
|
||||
- **Pattern:** `e2e/**/*.e2e.ts`
|
||||
- **Framework:** Mocha (via `@wdio/mocha-framework`)
|
||||
- **Connects to:** port 4445 (embedded WebDriver server)
|
||||
- **Use for:** UI interaction tests, window management, navigation flows.
|
||||
|
||||
## Test File Naming
|
||||
|
||||
| Suffix | Runner | Environment |
|
||||
| ------------------- | ------------------- | --------------------- |
|
||||
| `*.test.ts` | `pnpm test` | jsdom |
|
||||
| `*.browser.test.ts` | `pnpm test:browser` | Chromium (Playwright) |
|
||||
| `*.tauri.test.ts` | `pnpm test:tauri` | Tauri WebView |
|
||||
| `*.e2e.ts` | `pnpm test:e2e` | Tauri app (WDIO) |
|
||||
@@ -0,0 +1,123 @@
|
||||
describe('Readest App Launch', () => {
|
||||
it('should have a visible body element', async () => {
|
||||
const body = await $('body');
|
||||
await body.waitForDisplayed({ timeout: 10000 });
|
||||
expect(await body.isDisplayed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have the correct window handle', async () => {
|
||||
const handle = await browser.getWindowHandle();
|
||||
expect(handle).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return the page source', async () => {
|
||||
const source = await browser.getPageSource();
|
||||
expect(source).toContain('html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Library Page', () => {
|
||||
it('should navigate to the library page', async () => {
|
||||
const url = await browser.getUrl();
|
||||
expect(url).toMatch(/library|localhost/);
|
||||
});
|
||||
|
||||
it('should display the library container', async () => {
|
||||
const library = await $('[aria-label="Your Library"]');
|
||||
await library.waitForExist({ timeout: 15000 });
|
||||
expect(await library.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display the library header', async () => {
|
||||
const header = await $('[aria-label="Library Header"]');
|
||||
await header.waitForExist({ timeout: 10000 });
|
||||
expect(await header.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display the bookshelf area', async () => {
|
||||
const bookshelf = await $('[aria-label="Bookshelf"]');
|
||||
await bookshelf.waitForExist({ timeout: 10000 });
|
||||
expect(await bookshelf.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a search input', async () => {
|
||||
const searchInput = await $('.search-input');
|
||||
await searchInput.waitForExist({ timeout: 10000 });
|
||||
expect(await searchInput.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow typing in the search input', async () => {
|
||||
const searchInput = await $('.search-input');
|
||||
await searchInput.waitForDisplayed({ timeout: 10000 });
|
||||
await searchInput.setValue('test search');
|
||||
const value = await searchInput.getValue();
|
||||
expect(value).toBe('test search');
|
||||
});
|
||||
|
||||
it('should show the clear search button after typing', async () => {
|
||||
const clearBtn = await $('[aria-label="Clear Search"]');
|
||||
await clearBtn.waitForExist({ timeout: 5000 });
|
||||
expect(await clearBtn.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear the search input when clear button is clicked', async () => {
|
||||
const clearBtn = await $('[aria-label="Clear Search"]');
|
||||
await clearBtn.click();
|
||||
const searchInput = await $('.search-input');
|
||||
const value = await searchInput.getValue();
|
||||
expect(value).toBe('');
|
||||
});
|
||||
|
||||
it('should have a select books button', async () => {
|
||||
const selectBtn = await $('[aria-label="Select Books"]');
|
||||
await selectBtn.waitForExist({ timeout: 10000 });
|
||||
expect(await selectBtn.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have an import books button', async () => {
|
||||
const importBtn = await $('[aria-label="Import Books"]');
|
||||
await importBtn.waitForExist({ timeout: 10000 });
|
||||
expect(await importBtn.isExisting()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Window Management', () => {
|
||||
it('should return the window size', async () => {
|
||||
const size = await browser.getWindowSize();
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JavaScript Execution', () => {
|
||||
it('should execute JavaScript in the app context', async () => {
|
||||
const result = await browser.execute(() => {
|
||||
return document.readyState;
|
||||
});
|
||||
expect(result).toBe('complete');
|
||||
});
|
||||
|
||||
it('should access the document title via JS', async () => {
|
||||
const title = await browser.execute(() => {
|
||||
return document.title;
|
||||
});
|
||||
expect(title).toContain('Readest');
|
||||
});
|
||||
|
||||
it('should detect the app platform globals', async () => {
|
||||
const hasCLIAccess = await browser.execute(() => {
|
||||
return (window as unknown as Record<string, unknown>).__READEST_CLI_ACCESS === true;
|
||||
});
|
||||
expect(hasCLIAccess).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate back to library after visiting another route', async () => {
|
||||
const currentUrl = await browser.getUrl();
|
||||
await browser.url(currentUrl.replace(/\/[^/]*$/, '/library'));
|
||||
const library = await $('[aria-label="Your Library"]');
|
||||
await library.waitForExist({ timeout: 15000 });
|
||||
expect(await library.isExisting()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["mocha", "@wdio/globals/types"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import next from 'eslint-config-next';
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||
import tseslint from 'eslint-config-next/typescript';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...tseslint,
|
||||
...next,
|
||||
...nextVitals,
|
||||
{
|
||||
rules: {
|
||||
...jsxA11y.configs.recommended.rules,
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
globalIgnores([
|
||||
'node_modules/**',
|
||||
'.next/**',
|
||||
'.open-next/**',
|
||||
'out/**',
|
||||
'build/**',
|
||||
'public/**',
|
||||
'next-env.d.ts',
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,64 @@
|
||||
const options = {
|
||||
debug: false,
|
||||
sort: false,
|
||||
func: {
|
||||
list: ['_'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
lngs: [
|
||||
'de',
|
||||
'ja',
|
||||
'es',
|
||||
'fa',
|
||||
'fr',
|
||||
'it',
|
||||
'el',
|
||||
'ko',
|
||||
'uk',
|
||||
'nl',
|
||||
'sl',
|
||||
'sv',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'tr',
|
||||
'hi',
|
||||
'id',
|
||||
'vi',
|
||||
'ms',
|
||||
'he',
|
||||
'ar',
|
||||
'th',
|
||||
'bo',
|
||||
'bn',
|
||||
'ta',
|
||||
'si',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'ro',
|
||||
],
|
||||
ns: ['translation'],
|
||||
defaultNs: 'translation',
|
||||
defaultValue: '__STRING_NOT_TRANSLATED__',
|
||||
resource: {
|
||||
loadPath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
savePath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
jsonIndent: 2,
|
||||
lineEnding: '\n',
|
||||
},
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
interpolation: {
|
||||
prefix: '{{',
|
||||
suffix: '}}',
|
||||
},
|
||||
metadata: {},
|
||||
allowDynamicKeys: true,
|
||||
removeUnusedKeys: true,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
input: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.test.{js,jsx,ts,tsx}'],
|
||||
output: '.',
|
||||
options,
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
module.exports = {
|
||||
input: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.test.{js,jsx,ts,tsx}'],
|
||||
output: '.',
|
||||
options: {
|
||||
debug: false,
|
||||
sort: false,
|
||||
func: {
|
||||
list: ['_'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
lngs: [
|
||||
'de',
|
||||
'ja',
|
||||
'es',
|
||||
'fa',
|
||||
'fr',
|
||||
'it',
|
||||
'el',
|
||||
'ko',
|
||||
'uk',
|
||||
'nl',
|
||||
'sv',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'tr',
|
||||
'hi',
|
||||
'id',
|
||||
'vi',
|
||||
'ms',
|
||||
'he',
|
||||
'ar',
|
||||
'th',
|
||||
'bo',
|
||||
'bn',
|
||||
'ta',
|
||||
'si',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
],
|
||||
ns: ['translation'],
|
||||
defaultNs: 'translation',
|
||||
defaultValue: '__STRING_NOT_TRANSLATED__',
|
||||
resource: {
|
||||
loadPath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
savePath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
jsonIndent: 2,
|
||||
lineEnding: '\n',
|
||||
},
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
interpolation: {
|
||||
prefix: '{{',
|
||||
suffix: '}}',
|
||||
},
|
||||
metadata: {},
|
||||
allowDynamicKeys: true,
|
||||
removeUnusedKeys: true,
|
||||
},
|
||||
};
|
||||
@@ -31,12 +31,14 @@ const nextConfig = {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
nunjucks: 'nunjucks/browser/nunjucks.js',
|
||||
...(appPlatform !== 'web' ? { '@tursodatabase/database-wasm': false } : {}),
|
||||
};
|
||||
return config;
|
||||
},
|
||||
turbopack: {
|
||||
resolveAlias: {
|
||||
nunjucks: 'nunjucks/browser/nunjucks.js',
|
||||
...(appPlatform !== 'web' ? { '@tursodatabase/database-wasm': './src/utils/stub.ts' } : {}),
|
||||
},
|
||||
},
|
||||
transpilePackages: [
|
||||
@@ -59,6 +61,14 @@ const nextConfig = {
|
||||
'marked',
|
||||
]),
|
||||
],
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/reader/:ids',
|
||||
destination: '/reader?ids=:ids',
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.101",
|
||||
"version": "0.10.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
"build": "dotenv -e .env.tauri -- next build",
|
||||
@@ -9,12 +10,25 @@
|
||||
"dev-web": "dotenv -e .env.web -- next dev",
|
||||
"build-web": "dotenv -e .env.web -- next build",
|
||||
"start-web": "dotenv -e .env.web -- next start",
|
||||
"dev-web:vinext": "dotenv -e .env.web -- vinext dev",
|
||||
"build-web:vinext": "dotenv -e .env.web -- vinext build",
|
||||
"start-web:vinext": "dotenv -e .env.web -- vinext start",
|
||||
"build-tauri": "dotenv -e .env.tauri -- next build",
|
||||
"i18n:extract": "i18next-scanner",
|
||||
"lint": "eslint .",
|
||||
"i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs",
|
||||
"lint": "tsgo --noEmit && biome check .",
|
||||
"test": "dotenv -e .env -e .env.test.local vitest",
|
||||
"test:coverage": "dotenv -e .env -e .env.test.local -- vitest run --coverage",
|
||||
"test:browser": "vitest --config vitest.browser.config.mts --watch=false",
|
||||
"test:tauri": "bash scripts/test-tauri.sh",
|
||||
"test:pr:web": "pnpm test -- --watch=false && pnpm test:browser",
|
||||
"test:pr:tauri": "bash scripts/test-tauri.sh",
|
||||
"test:all": "pnpm test -- --watch=false && pnpm test:browser && bash scripts/test-tauri.sh",
|
||||
"test:e2e": "wdio run wdio.conf.ts",
|
||||
"tauri:dev:test": "dotenv -e .env.tauri -- tauri dev --features webdriver",
|
||||
"tauri:build:test": "dotenv -e .env.tauri -- tauri build --debug --features webdriver",
|
||||
"tauri": "tauri",
|
||||
"clippy": "cargo clippy -p Readest --no-deps -- -D warnings",
|
||||
"fmt:check": "cargo fmt -p Readest --check",
|
||||
"clippy:check": "cargo clippy -p Readest --no-deps -- -D warnings",
|
||||
"format": "pnpm -w format",
|
||||
"format:check": "pnpm -w format:check",
|
||||
"prepare-public-vendor": "mkdirp ./public/vendor/pdfjs ./public/vendor/simplecc",
|
||||
@@ -59,11 +73,14 @@
|
||||
"@assistant-ui/react": "0.11.56",
|
||||
"@assistant-ui/react-ai-sdk": "1.1.21",
|
||||
"@assistant-ui/react-markdown": "0.11.9",
|
||||
"@aws-sdk/client-s3": "^3.735.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.735.0",
|
||||
"@aws-sdk/client-s3": "^3.1000.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1000.0",
|
||||
"@choochmeque/tauri-plugin-sharekit-api": "^0.3.0",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@fabianlars/tauri-plugin-oauth": "2",
|
||||
"@opennextjs/cloudflare": "^1.15.1",
|
||||
"@napi-rs/wasm-runtime": "^1.1.1",
|
||||
"@opennextjs/cloudflare": "^1.17.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
@@ -74,26 +91,30 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@serwist/next": "^9.4.2",
|
||||
"@serwist/next": "^9.5.6",
|
||||
"@serwist/webpack-plugin": "^9.5.6",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.76.1",
|
||||
"@tauri-apps/api": "2.9.1",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-cli": "^2.4.1",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.6",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.7",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
"@tauri-apps/plugin-haptics": "^2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.6",
|
||||
"@tauri-apps/plugin-http": "^2.5.7",
|
||||
"@tauri-apps/plugin-log": "^2.8.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "~2.3.4",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-shell": "~2.3.5",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@tauri-apps/plugin-websocket": "~2.4.2",
|
||||
"@tursodatabase/database-common": "^0.5.0",
|
||||
"@tursodatabase/database-wasm": "^0.5.0",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"@zip.js/zip.js": "^2.8.16",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"ai": "^6.0.47",
|
||||
@@ -125,15 +146,15 @@
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^15.0.12",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "16.1.6",
|
||||
"next": "16.2.1",
|
||||
"next-view-transitions": "^0.3.5",
|
||||
"nunjucks": "^3.2.4",
|
||||
"overlayscrollbars": "^2.11.4",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"posthog-js": "^1.246.0",
|
||||
"react": "19.2.0",
|
||||
"react": "19.2.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "19.2.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-responsive": "^10.0.0",
|
||||
@@ -145,6 +166,7 @@
|
||||
"stripe": "^18.2.1",
|
||||
"styled-jsx": "^5.1.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tauri-plugin-device-info-api": "^1.0.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.3",
|
||||
@@ -154,12 +176,14 @@
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^15.4.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tauri-apps/cli": "2.9.6",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@tursodatabase/database": "^0.5.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/lunr": "^2.3.7",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/nunjucks": "^3.2.6",
|
||||
"@types/react": "^19.0.0",
|
||||
@@ -170,30 +194,40 @@
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260312.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitejs/plugin-rsc": "^0.5.21",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/browser-webdriverio": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@wdio/cli": "^9.25.0",
|
||||
"@wdio/globals": "^9.23.0",
|
||||
"@wdio/local-runner": "^9.24.0",
|
||||
"@wdio/mocha-framework": "^9.24.0",
|
||||
"@wdio/spec-reporter": "^9.24.0",
|
||||
"@wdio/types": "^9.24.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"cpx2": "^8.0.0",
|
||||
"daisyui": "^4.12.24",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "16.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"i18next-scanner": "^4.6.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"node-env-run": "^4.0.2",
|
||||
"playwright": "^1.58.2",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-server-dom-webpack": "^19.2.4",
|
||||
"serwist": "^9.3.0",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"typescript": "^5.7.2",
|
||||
"vinext": "^0.0.21",
|
||||
"vite": "^7.3.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.15",
|
||||
"wrangler": "^4.60.0"
|
||||
"vitest": "^4.0.18",
|
||||
"wrangler": "^4.77.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -105,7 +105,6 @@
|
||||
"Wikipedia": "ويكيبيديا",
|
||||
"Writing Mode": "وضع الكتابة",
|
||||
"Your Library": "المكتبة الخاصة بك",
|
||||
"TTS not supported for PDF": "القراءة الصوتية غير مدعومة لملفات PDF",
|
||||
"Override Book Font": "تجاوز خط الكتاب",
|
||||
"Apply to All Books": "تطبيق على جميع الكتب",
|
||||
"Apply to This Book": "تطبيق على هذا الكتاب",
|
||||
@@ -184,7 +183,6 @@
|
||||
"RTL Direction": "الاتجاه من اليمين إلى اليسار",
|
||||
"Maximum Column Height": "الارتفاع الأقصى للعمود",
|
||||
"Maximum Column Width": "العرض الأقصى للعمود",
|
||||
"Continuous Scroll": "التمرير المستمر",
|
||||
"Fullscreen": "ملء الشاشة",
|
||||
"No supported files found. Supported formats: {{formats}}": "لم يتم العثور على ملفات مدعومة. الصيغ المدعومة: {{formats}}",
|
||||
"Drop to Import Books": "قم بالسحب والإسقاط لاستيراد الكتب",
|
||||
@@ -872,6 +870,9 @@
|
||||
"Clear search": "مسح البحث",
|
||||
"Clear search history": "مسح سجل البحث",
|
||||
"Tap to Toggle Footer": "انقر لتبديل التذييل",
|
||||
"Show Current Time": "عرض الوقت الحالي",
|
||||
"Use 24 Hour Clock": "استخدام نظام 24 ساعة",
|
||||
"Show Current Battery Status": "عرض حالة البطارية الحالية",
|
||||
"Exported successfully": "تم التصدير بنجاح",
|
||||
"Book exported successfully.": "تم تصدير الكتاب بنجاح.",
|
||||
"Failed to export the book.": "فشل تصدير الكتاب.",
|
||||
@@ -1029,17 +1030,14 @@
|
||||
"words": "كلمات",
|
||||
"{{time}} left": "متبقي {{time}}",
|
||||
"Reading progress": "تقدم القراءة",
|
||||
"Click to seek": "انقر للتمرير",
|
||||
"Skip back 15 words": "تخطي للخلف 15 كلمة",
|
||||
"Back 15 words (Shift+Left)": "للخلف 15 كلمة (Shift+اليسار)",
|
||||
"Pause (Space)": "إيقاف مؤقت (المسافة)",
|
||||
"Play (Space)": "تشغيل (المسافة)",
|
||||
"Skip forward 15 words": "تخطي للأمام 15 كلمة",
|
||||
"Forward 15 words (Shift+Right)": "للأمام 15 كلمة (Shift+اليمين)",
|
||||
"Pause:": "إيقاف مؤقت:",
|
||||
"Decrease speed": "تقليل السرعة",
|
||||
"Slower (Left/Down)": "أبطأ (اليسار/الأسفل)",
|
||||
"Current speed": "السرعة الحالية",
|
||||
"Increase speed": "زيادة السرعة",
|
||||
"Faster (Right/Up)": "أسرع (اليمين/الأعلى)",
|
||||
"Start RSVP Reading": "بدء قراءة RSVP",
|
||||
@@ -1107,5 +1105,134 @@
|
||||
"System Screen Brightness": "سطوع شاشة النظام",
|
||||
"Page:": "صفحة:",
|
||||
"Page: {{number}}": "صفحة: {{number}}",
|
||||
"Annotation page number": "رقم صفحة التعליق"
|
||||
"Annotation page number": "رقم صفحة التعליق",
|
||||
"Translating...": "جارٍ الترجمة...",
|
||||
"Show Battery Percentage": "عرض نسبة البطارية",
|
||||
"Hide Scrollbar": "إخفاء شريط التمرير",
|
||||
"Skip to last reading position": "انتقل إلى آخر موضع قراءة",
|
||||
"Show context": "إظهار السياق",
|
||||
"Hide context": "إخفاء السياق",
|
||||
"Decrease font size": "تصغير حجم الخط",
|
||||
"Increase font size": "تكبير حجم الخط",
|
||||
"Focus": "تركيز",
|
||||
"Theme color": "لون السمة",
|
||||
"Import failed": "فشل الاستيراد",
|
||||
"Available Formatters:": "المُنسّقات المتاحة:",
|
||||
"Format date": "تنسيق التاريخ",
|
||||
"Markdown block quote (> per line)": "اقتباس Markdown (> لكل سطر)",
|
||||
"Newlines to <br>": "أسطر جديدة إلى <br>",
|
||||
"Change case": "تغيير حالة الأحرف",
|
||||
"Trim whitespace": "إزالة المسافات",
|
||||
"Truncate to n characters": "اقتطاع إلى n حرف",
|
||||
"Replace text": "استبدال النص",
|
||||
"Fallback value": "القيمة الاحتياطية",
|
||||
"Get length": "الحصول على الطول",
|
||||
"First/last element": "العنصر الأول/الأخير",
|
||||
"Join array": "دمج المصفوفة",
|
||||
"Backup failed: {{error}}": "فشل النسخ الاحتياطي: {{error}}",
|
||||
"Select Backup": "اختر نسخة احتياطية",
|
||||
"Restore failed: {{error}}": "فشل الاستعادة: {{error}}",
|
||||
"Backup & Restore": "النسخ الاحتياطي والاستعادة",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "أنشئ نسخة احتياطية من مكتبتك أو استعد من نسخة احتياطية سابقة. ستدمج الاستعادة مع مكتبتك الحالية.",
|
||||
"Backup Library": "نسخ المكتبة احتياطيًا",
|
||||
"Restore Library": "استعادة المكتبة",
|
||||
"Creating backup...": "جارٍ إنشاء النسخة الاحتياطية...",
|
||||
"Restoring library...": "جارٍ استعادة المكتبة...",
|
||||
"{{current}} of {{total}} items": "{{current}} من {{total}} عنصر",
|
||||
"Backup completed successfully!": "تم النسخ الاحتياطي بنجاح!",
|
||||
"Restore completed successfully!": "تمت الاستعادة بنجاح!",
|
||||
"Your library has been saved to the selected location.": "تم حفظ مكتبتك في الموقع المحدد.",
|
||||
"{{added}} books added, {{updated}} books updated.": "تمت إضافة {{added}} كتب، وتحديث {{updated}} كتب.",
|
||||
"Operation failed": "فشلت العملية",
|
||||
"Loading library...": "جارٍ تحميل المكتبة...",
|
||||
"{{count}} books refreshed_zero": "لم يتم تحديث أي كتاب",
|
||||
"{{count}} books refreshed_one": "تم تحديث كتاب واحد",
|
||||
"{{count}} books refreshed_two": "تم تحديث كتابين",
|
||||
"{{count}} books refreshed_few": "تم تحديث {{count}} كتب",
|
||||
"{{count}} books refreshed_many": "تم تحديث {{count}} كتابًا",
|
||||
"{{count}} books refreshed_other": "تم تحديث {{count}} كتاب",
|
||||
"Failed to refresh metadata": "فشل تحديث البيانات الوصفية",
|
||||
"Refresh Metadata": "تحديث البيانات الوصفية",
|
||||
"Keyboard Shortcuts": "اختصارات لوحة المفاتيح",
|
||||
"View all keyboard shortcuts": "عرض جميع اختصارات لوحة المفاتيح",
|
||||
"Switch Sidebar Tab": "تبديل علامة تبويب الشريط الجانبي",
|
||||
"Toggle Notebook": "إظهار/إخفاء دفتر الملاحظات",
|
||||
"Search in Book": "البحث في الكتاب",
|
||||
"Toggle Scroll Mode": "تبديل وضع التمرير",
|
||||
"Toggle Select Mode": "تبديل وضع التحديد",
|
||||
"Toggle Bookmark": "تبديل الإشارة المرجعية",
|
||||
"Toggle Text to Speech": "تبديل تحويل النص إلى كلام",
|
||||
"Play / Pause TTS": "تشغيل / إيقاف القراءة الصوتية",
|
||||
"Toggle Paragraph Mode": "تبديل وضع الفقرة",
|
||||
"Highlight Selection": "تمييز التحديد",
|
||||
"Underline Selection": "وضع خط تحت التحديد",
|
||||
"Annotate Selection": "إضافة تعليق على التحديد",
|
||||
"Search Selection": "البحث عن التحديد",
|
||||
"Copy Selection": "نسخ التحديد",
|
||||
"Translate Selection": "ترجمة التحديد",
|
||||
"Dictionary Lookup": "البحث في القاموس",
|
||||
"Wikipedia Lookup": "البحث في ويكيبيديا",
|
||||
"Read Aloud Selection": "قراءة التحديد بصوت عالٍ",
|
||||
"Proofread Selection": "تدقيق التحديد",
|
||||
"Open Settings": "فتح الإعدادات",
|
||||
"Open Command Palette": "فتح لوحة الأوامر",
|
||||
"Show Keyboard Shortcuts": "عرض اختصارات لوحة المفاتيح",
|
||||
"Open Books": "فتح الكتب",
|
||||
"Toggle Fullscreen": "تبديل ملء الشاشة",
|
||||
"Close Window": "إغلاق النافذة",
|
||||
"Quit App": "إنهاء التطبيق",
|
||||
"Go Left / Previous Page": "الانتقال لليسار / الصفحة السابقة",
|
||||
"Go Right / Next Page": "الانتقال لليمين / الصفحة التالية",
|
||||
"Go Up": "الانتقال لأعلى",
|
||||
"Go Down": "الانتقال لأسفل",
|
||||
"Previous Chapter": "الفصل السابق",
|
||||
"Next Chapter": "الفصل التالي",
|
||||
"Scroll Half Page Down": "تمرير نصف صفحة لأسفل",
|
||||
"Scroll Half Page Up": "تمرير نصف صفحة لأعلى",
|
||||
"Save Note": "حفظ الملاحظة",
|
||||
"Single Section Scroll": "التمرير بقسم واحد",
|
||||
"General": "عام",
|
||||
"Text to Speech": "تحويل النص إلى كلام",
|
||||
"Zoom": "تكبير",
|
||||
"Window": "نافذة",
|
||||
"Your Bookshelf": "رف الكتب الخاص بك",
|
||||
"TTS": "تحويل النص إلى كلام",
|
||||
"Media Info": "معلومات الوسائط",
|
||||
"Update Frequency": "تردد التحديث",
|
||||
"Every Sentence": "كل جملة",
|
||||
"Every Paragraph": "كل فقرة",
|
||||
"Every Chapter": "كل فصل",
|
||||
"TTS Media Info Update Frequency": "تردد تحديث معلومات وسائط TTS",
|
||||
"Select reading speed": "اختر سرعة القراءة",
|
||||
"Drag to seek": "اسحب للتقديم",
|
||||
"Punctuation Delay": "تأخير علامات الترقيم",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "يرجى التأكيد على أن اتصال OPDS هذا سيتم توجيهه عبر خوادم Readest على تطبيق الويب قبل المتابعة.",
|
||||
"Custom Headers": "رؤوس مخصصة",
|
||||
"Custom Headers (optional)": "رؤوس مخصصة (اختياري)",
|
||||
"Add one header per line using \"Header-Name: value\".": "أضف رأسًا واحدًا لكل سطر باستخدام \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "أفهم أن اتصال OPDS هذا سيتم توجيهه عبر خوادم Readest على تطبيق الويب. إذا لم أثق بـ Readest فيما يتعلق ببيانات الاعتماد أو الرؤوس هذه، يجب أن أستخدم التطبيق الأصلي بدلاً من ذلك.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "تعذر الاتصال بـ Hardcover. يرجى التحقق من اتصال الشبكة.",
|
||||
"Invalid Hardcover API token": "رمز API الخاص بـ Hardcover غير صالح",
|
||||
"Disconnected from Hardcover": "تم قطع الاتصال بـ Hardcover",
|
||||
"Hardcover Settings": "إعدادات Hardcover",
|
||||
"Connected to Hardcover": "متصل بـ Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "اربط حسابك على Hardcover لمزامنة تقدم القراءة والملاحظات.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "احصل على رمز API من hardcover.app ← الإعدادات ← API.",
|
||||
"API Token": "رمز API",
|
||||
"Paste your Hardcover API token": "الصق رمز API الخاص بك في Hardcover",
|
||||
"Hardcover sync enabled for this book": "تم تفعيل مزامنة Hardcover لهذا الكتاب",
|
||||
"Hardcover sync disabled for this book": "تم تعطيل مزامنة Hardcover لهذا الكتاب",
|
||||
"Hardcover Sync": "مزامنة Hardcover",
|
||||
"Enable for This Book": "تفعيل لهذا الكتاب",
|
||||
"Push Notes": "دفع الملاحظات",
|
||||
"Enable Hardcover sync for this book first.": "قم بتفعيل مزامنة Hardcover لهذا الكتاب أولاً.",
|
||||
"No annotations or excerpts to sync for this book.": "لا توجد تعليقات توضيحية أو مقتطفات لمزامنتها لهذا الكتاب.",
|
||||
"Configure Hardcover in Settings first.": "قم بإعداد Hardcover في الإعدادات أولاً.",
|
||||
"No new Hardcover note changes to sync.": "لا توجد تغييرات جديدة في ملاحظات Hardcover للمزامنة.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "تمت مزامنة Hardcover: {{inserted}} جديد، {{updated}} محدث، {{skipped}} بدون تغيير",
|
||||
"Hardcover notes sync failed: {{error}}": "فشلت مزامنة ملاحظات Hardcover: {{error}}",
|
||||
"Reading progress synced to Hardcover": "تمت مزامنة تقدم القراءة إلى Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "فشلت مزامنة تقدم Hardcover: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "الإبقاء على معرف المصدر الحالي"
|
||||
}
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
"Auto": "অটো",
|
||||
"Scroll": "স্ক্রল",
|
||||
"Scrolled Mode": "স্ক্রল মোড",
|
||||
"Continuous Scroll": "ক্রমাগত স্ক্রল",
|
||||
"Overlap Pixels": "ওভারল্যাপ পিক্সেল",
|
||||
"Disable Double Click": "ডাবল ক্লিক নিষ্ক্রিয়",
|
||||
"Volume Keys for Page Flip": "পৃষ্ঠা উল্টানোর জন্য ভলিউম কী",
|
||||
@@ -319,7 +318,6 @@
|
||||
"Table of Contents": "সূচিপত্র",
|
||||
"Sidebar": "সাইডবার",
|
||||
"Disable Translation": "অনুবাদ নিষ্ক্রিয়",
|
||||
"TTS not supported for PDF": "PDF এর জন্য TTS সমর্থিত নয়",
|
||||
"TTS not supported for this document": "এই ডকুমেন্টের জন্য TTS সমর্থিত নয়",
|
||||
"No Timeout": "কোন টাইমআউট নেই",
|
||||
"{{value}} minute": "{{value}} মিনিট",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "অনুসন্ধান সাফ করুন",
|
||||
"Clear search history": "অনুসন্ধান ইতিহাস সাফ করুন",
|
||||
"Tap to Toggle Footer": "ফুটার টগল করতে আলতো চাপুন",
|
||||
"Show Current Time": "বর্তমান সময় দেখান",
|
||||
"Use 24 Hour Clock": "২৪ ঘণ্টার ঘড়ি ব্যবহার করুন",
|
||||
"Show Current Battery Status": "বর্তমান ব্যাটারি অবস্থা দেখান",
|
||||
"Exported successfully": "সফলভাবে রপ্তানি হয়েছে",
|
||||
"Book exported successfully.": "বইটি সফলভাবে রপ্তানি হয়েছে।",
|
||||
"Failed to export the book.": "বই রপ্তানি করতে ব্যর্থ।",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "শব্দ",
|
||||
"{{time}} left": "{{time}} বাকি",
|
||||
"Reading progress": "পড়ার অগ্রগতি",
|
||||
"Click to seek": "খুঁজতে ক্লিক করুন",
|
||||
"Skip back 15 words": "১৫ শব্দ পিছিয়ে যান",
|
||||
"Back 15 words (Shift+Left)": "১৫ শব্দ পিছিয়ে যান (Shift+Left)",
|
||||
"Pause (Space)": "বিরতি (Space)",
|
||||
"Play (Space)": "চালান (Space)",
|
||||
"Skip forward 15 words": "১৫ শব্দ এগিয়ে যান",
|
||||
"Forward 15 words (Shift+Right)": "১৫ শব্দ এগিয়ে যান (Shift+Right)",
|
||||
"Pause:": "বিরতি:",
|
||||
"Decrease speed": "গতি কমান",
|
||||
"Slower (Left/Down)": "ধীর (Left/Down)",
|
||||
"Current speed": "বর্তমান গতি",
|
||||
"Increase speed": "গতি বাড়ান",
|
||||
"Faster (Right/Up)": "দ্রুত (Right/Up)",
|
||||
"Start RSVP Reading": "RSVP পড়া শুরু করুন",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "সিস্টেম স্ক্রিন উজ্জ্বলতা",
|
||||
"Page:": "পৃষ্ঠা:",
|
||||
"Page: {{number}}": "পৃষ্ঠা: {{number}}",
|
||||
"Annotation page number": "টীকা পৃষ্ঠা নম্বর"
|
||||
"Annotation page number": "টীকা পৃষ্ঠা নম্বর",
|
||||
"Translating...": "অনুবাদ করা হচ্ছে...",
|
||||
"Show Battery Percentage": "ব্যাটারি শতাংশ দেখান",
|
||||
"Hide Scrollbar": "স্ক্রলবার লুকান",
|
||||
"Skip to last reading position": "শেষ পড়ার অবস্থানে যান",
|
||||
"Show context": "প্রসঙ্গ দেখান",
|
||||
"Hide context": "প্রসঙ্গ লুকান",
|
||||
"Decrease font size": "ফন্টের আকার কমান",
|
||||
"Increase font size": "ফন্টের আকার বাড়ান",
|
||||
"Focus": "ফোকাস",
|
||||
"Theme color": "থিম রঙ",
|
||||
"Import failed": "আমদানি ব্যর্থ",
|
||||
"Available Formatters:": "উপলব্ধ ফরম্যাটার:",
|
||||
"Format date": "তারিখ ফরম্যাট",
|
||||
"Markdown block quote (> per line)": "Markdown ব্লক কোট (প্রতি লাইনে >)",
|
||||
"Newlines to <br>": "নতুন লাইন থেকে <br>",
|
||||
"Change case": "অক্ষরের কেস পরিবর্তন",
|
||||
"Trim whitespace": "শূন্যস্থান ছাঁটা",
|
||||
"Truncate to n characters": "n অক্ষরে সংক্ষিপ্ত করুন",
|
||||
"Replace text": "টেক্সট প্রতিস্থাপন",
|
||||
"Fallback value": "ফলব্যাক মান",
|
||||
"Get length": "দৈর্ঘ্য নিন",
|
||||
"First/last element": "প্রথম/শেষ উপাদান",
|
||||
"Join array": "অ্যারে যুক্ত করুন",
|
||||
"Backup failed: {{error}}": "ব্যাকআপ ব্যর্থ: {{error}}",
|
||||
"Select Backup": "ব্যাকআপ নির্বাচন করুন",
|
||||
"Restore failed: {{error}}": "পুনরুদ্ধার ব্যর্থ: {{error}}",
|
||||
"Backup & Restore": "ব্যাকআপ ও পুনরুদ্ধার",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "আপনার লাইব্রেরির ব্যাকআপ তৈরি করুন বা পূর্ববর্তী ব্যাকআপ থেকে পুনরুদ্ধার করুন। পুনরুদ্ধার আপনার বর্তমান লাইব্রেরির সাথে মার্জ হবে।",
|
||||
"Backup Library": "লাইব্রেরি ব্যাকআপ",
|
||||
"Restore Library": "লাইব্রেরি পুনরুদ্ধার",
|
||||
"Creating backup...": "ব্যাকআপ তৈরি হচ্ছে...",
|
||||
"Restoring library...": "লাইব্রেরি পুনরুদ্ধার হচ্ছে...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} আইটেম",
|
||||
"Backup completed successfully!": "ব্যাকআপ সফলভাবে সম্পন্ন!",
|
||||
"Restore completed successfully!": "পুনরুদ্ধার সফলভাবে সম্পন্ন!",
|
||||
"Your library has been saved to the selected location.": "আপনার লাইব্রেরি নির্বাচিত স্থানে সংরক্ষিত হয়েছে।",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}টি বই যোগ হয়েছে, {{updated}}টি বই আপডেট হয়েছে।",
|
||||
"Operation failed": "অপারেশন ব্যর্থ",
|
||||
"Loading library...": "লাইব্রেরি লোড হচ্ছে...",
|
||||
"{{count}} books refreshed_one": "{{count}}টি বইয়ের মেটাডেটা রিফ্রেশ হয়েছে",
|
||||
"{{count}} books refreshed_other": "{{count}}টি বইয়ের মেটাডেটা রিফ্রেশ হয়েছে",
|
||||
"Failed to refresh metadata": "মেটাডেটা রিফ্রেশ ব্যর্থ",
|
||||
"Refresh Metadata": "মেটাডেটা রিফ্রেশ",
|
||||
"Keyboard Shortcuts": "কীবোর্ড শর্টকাট",
|
||||
"View all keyboard shortcuts": "সমস্ত কীবোর্ড শর্টকাট দেখুন",
|
||||
"Switch Sidebar Tab": "সাইডবার ট্যাব পরিবর্তন করুন",
|
||||
"Toggle Notebook": "নোটবুক দেখান/লুকান",
|
||||
"Search in Book": "বইয়ে অনুসন্ধান করুন",
|
||||
"Toggle Scroll Mode": "স্ক্রল মোড টগল করুন",
|
||||
"Toggle Select Mode": "নির্বাচন মোড টগল করুন",
|
||||
"Toggle Bookmark": "বুকমার্ক টগল করুন",
|
||||
"Toggle Text to Speech": "টেক্সট টু স্পিচ টগল করুন",
|
||||
"Play / Pause TTS": "TTS চালান / বিরতি দিন",
|
||||
"Toggle Paragraph Mode": "অনুচ্ছেদ মোড টগল করুন",
|
||||
"Highlight Selection": "নির্বাচিত অংশ হাইলাইট করুন",
|
||||
"Underline Selection": "নির্বাচিত অংশে আন্ডারলাইন করুন",
|
||||
"Annotate Selection": "নির্বাচিত অংশে টীকা যোগ করুন",
|
||||
"Search Selection": "নির্বাচিত অংশ অনুসন্ধান করুন",
|
||||
"Copy Selection": "নির্বাচিত অংশ কপি করুন",
|
||||
"Translate Selection": "নির্বাচিত অংশ অনুবাদ করুন",
|
||||
"Dictionary Lookup": "অভিধানে খুঁজুন",
|
||||
"Wikipedia Lookup": "উইকিপিডিয়ায় খুঁজুন",
|
||||
"Read Aloud Selection": "নির্বাচিত অংশ জোরে পড়ুন",
|
||||
"Proofread Selection": "নির্বাচিত অংশ প্রুফরিড করুন",
|
||||
"Open Settings": "সেটিংস খুলুন",
|
||||
"Open Command Palette": "কমান্ড প্যালেট খুলুন",
|
||||
"Show Keyboard Shortcuts": "কীবোর্ড শর্টকাট দেখান",
|
||||
"Open Books": "বই খুলুন",
|
||||
"Toggle Fullscreen": "পূর্ণ পর্দা টগল করুন",
|
||||
"Close Window": "উইন্ডো বন্ধ করুন",
|
||||
"Quit App": "অ্যাপ থেকে প্রস্থান করুন",
|
||||
"Go Left / Previous Page": "বামে যান / আগের পৃষ্ঠা",
|
||||
"Go Right / Next Page": "ডানে যান / পরের পৃষ্ঠা",
|
||||
"Go Up": "উপরে যান",
|
||||
"Go Down": "নিচে যান",
|
||||
"Previous Chapter": "আগের অধ্যায়",
|
||||
"Next Chapter": "পরের অধ্যায়",
|
||||
"Scroll Half Page Down": "অর্ধেক পৃষ্ঠা নিচে স্ক্রল করুন",
|
||||
"Scroll Half Page Up": "অর্ধেক পৃষ্ঠা উপরে স্ক্রল করুন",
|
||||
"Save Note": "নোট সংরক্ষণ করুন",
|
||||
"Single Section Scroll": "একক অধ্যায় স্ক্রল",
|
||||
"General": "সাধারণ",
|
||||
"Text to Speech": "পাঠ থেকে বাক্য",
|
||||
"Zoom": "জুম",
|
||||
"Window": "উইন্ডো",
|
||||
"Your Bookshelf": "আপনার বইয়ের তাক",
|
||||
"TTS": "টেক্সট টু স্পিচ",
|
||||
"Media Info": "মিডিয়া তথ্য",
|
||||
"Update Frequency": "আপডেটের ফ্রিকোয়েন্সি",
|
||||
"Every Sentence": "প্রতিটি বাক্য",
|
||||
"Every Paragraph": "প্রতিটি অনুচ্ছেদ",
|
||||
"Every Chapter": "প্রতিটি অধ্যায়",
|
||||
"TTS Media Info Update Frequency": "TTS মিডিয়া তথ্য আপডেট ফ্রিকোয়েন্সি",
|
||||
"Select reading speed": "পড়ার গতি নির্বাচন করুন",
|
||||
"Drag to seek": "খুঁজতে টানুন",
|
||||
"Punctuation Delay": "বিরাম চিহ্ন বিলম্ব",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "অনুগ্রহ করে নিশ্চিত করুন যে এই OPDS সংযোগটি ওয়েব অ্যাপে Readest সার্ভারের মাধ্যমে প্রক্সি করা হবে।",
|
||||
"Custom Headers": "কাস্টম হেডার",
|
||||
"Custom Headers (optional)": "কাস্টম হেডার (ঐচ্ছিক)",
|
||||
"Add one header per line using \"Header-Name: value\".": "প্রতি লাইনে একটি হেডার যোগ করুন \"Header-Name: value\" ব্যবহার করে।",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "আমি বুঝতে পারছি যে এই OPDS সংযোগটি ওয়েব অ্যাপে Readest সার্ভারের মাধ্যমে প্রক্সি করা হবে। আমি যদি এই শংসাপত্র বা হেডারগুলির সাথে Readest-কে বিশ্বাস না করি, তবে আমার পরিবর্তে নেটিভ অ্যাপটি ব্যবহার করা উচিত।",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcover-এ সংযোগ করা যাচ্ছে না। অনুগ্রহ করে আপনার নেটওয়ার্ক সংযোগ পরীক্ষা করুন।",
|
||||
"Invalid Hardcover API token": "অবৈধ Hardcover API টোকেন",
|
||||
"Disconnected from Hardcover": "Hardcover থেকে সংযোগ বিচ্ছিন্ন",
|
||||
"Hardcover Settings": "Hardcover সেটিংস",
|
||||
"Connected to Hardcover": "Hardcover-এ সংযুক্ত",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "পড়ার অগ্রগতি এবং নোট সিঙ্ক করতে আপনার Hardcover অ্যাকাউন্ট সংযুক্ত করুন।",
|
||||
"Get your API token from hardcover.app → Settings → API.": "hardcover.app → সেটিংস → API থেকে আপনার API টোকেন পান।",
|
||||
"API Token": "API টোকেন",
|
||||
"Paste your Hardcover API token": "আপনার Hardcover API টোকেন পেস্ট করুন",
|
||||
"Hardcover sync enabled for this book": "এই বইয়ের জন্য Hardcover সিঙ্ক সক্রিয়",
|
||||
"Hardcover sync disabled for this book": "এই বইয়ের জন্য Hardcover সিঙ্ক নিষ্ক্রিয়",
|
||||
"Hardcover Sync": "Hardcover সিঙ্ক",
|
||||
"Enable for This Book": "এই বইয়ের জন্য সক্রিয় করুন",
|
||||
"Push Notes": "নোট পুশ করুন",
|
||||
"Enable Hardcover sync for this book first.": "প্রথমে এই বইয়ের জন্য Hardcover সিঙ্ক সক্রিয় করুন।",
|
||||
"No annotations or excerpts to sync for this book.": "এই বইয়ের জন্য সিঙ্ক করার মতো কোনো টীকা বা উদ্ধৃতি নেই।",
|
||||
"Configure Hardcover in Settings first.": "প্রথমে সেটিংসে Hardcover কনফিগার করুন।",
|
||||
"No new Hardcover note changes to sync.": "সিঙ্ক করার জন্য নতুন কোনো Hardcover নোট পরিবর্তন নেই।",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover সিঙ্ক সম্পন্ন: {{inserted}} নতুন, {{updated}} আপডেট, {{skipped}} অপরিবর্তিত",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover নোট সিঙ্ক ব্যর্থ: {{error}}",
|
||||
"Reading progress synced to Hardcover": "পড়ার অগ্রগতি Hardcover-এ সিঙ্ক হয়েছে",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover অগ্রগতি সিঙ্ক ব্যর্থ: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "বিদ্যমান উৎস শনাক্তকারী রাখুন"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "ཝེ་ཁི་པི་ཌི་ཡ།",
|
||||
"Writing Mode": "པར་སྒྲིག་གི་རྣམ་པ།",
|
||||
"Your Library": "དཔེ་མཛོད།",
|
||||
"TTS not supported for PDF": "PDF ཡིག་ཆར་ TTS རྒྱབ་སྐྱོར་མི་བྱེད།",
|
||||
"Override Book Font": "དཔེ་དེབ་ཀྱི་ཡིག་གཟུགས་བརྗེ་བ།",
|
||||
"Apply to All Books": "དཔེ་དེབ་ཡོད་རྒུར་སྤྱོད་པ།",
|
||||
"Apply to This Book": "དཔེ་དེབ་འདིར་སྤྱོད་པ།",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "གཡས་ནས་གཡོན་དུ།",
|
||||
"Maximum Column Height": "གྲལ་ཐིག་མཐོ་ཤོས།",
|
||||
"Maximum Column Width": "གྲལ་ཐིག་རྒྱ་ཆེ་ཤོས།",
|
||||
"Continuous Scroll": "རྒྱུན་མཐུད་འཁོར་རྒྱུག",
|
||||
"Fullscreen": "བརྡ་ཁྱབ་ངོས་ཆ་ཚང་།",
|
||||
"No supported files found. Supported formats: {{formats}}": "རྒྱབ་སྐྱོར་བྱེད་པའི་ཡིག་ཆ་མ་རྙེད། རྒྱབ་སྐྱོར་བྱེད་པའི་རྣམ་པ། {{formats}}",
|
||||
"Drop to Import Books": "འདྲུད་འཇོག་བྱས་ནས་དཔེ་དེབ་ནང་འདྲེན།",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "འཚོལ་བཤེར་གཙང་སེལ།",
|
||||
"Clear search history": "འཚོལ་བཤེར་ལོ་རྒྱུས་གཙང་སེལ།",
|
||||
"Tap to Toggle Footer": "ཞབས་མཇུག་སྒོ་འབྱེད་བྱེད་པར་གནོན།",
|
||||
"Show Current Time": "ད་ལྟའི་དུས་ཚོད་སྟོན་པ།",
|
||||
"Use 24 Hour Clock": "ཆུ་ཚོད་ ༢༤ ཅན་གྱི་ཟློས་འཁོར་བཀོལ་བ།",
|
||||
"Show Current Battery Status": "ད་ལྟའི་གློག་རྫས་གནས་བབ་སྟོན་པ།",
|
||||
"Exported successfully": "ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
|
||||
"Book exported successfully.": "དཔེ་དེབ་ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
|
||||
"Failed to export the book.": "དཔེ་དེབ་ཕྱིར་འདོན་མི་ཐུབ།",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "ཚིག",
|
||||
"{{time}} left": "དུས་ཚོད་ {{time}} ལྷག་ཡོད།",
|
||||
"Reading progress": "ཀློག་པའི་འཕེལ་རིམ།",
|
||||
"Click to seek": "གནས་ས་འཚོལ་བར་གནོན་པ།",
|
||||
"Skip back 15 words": "ཚིག་ ༡༥ རྒྱབ་ལ་བཤུད་པ།",
|
||||
"Back 15 words (Shift+Left)": "ཚིག་ ༡༥ རྒྱབ་ལ་བཤུད་པ། (Shift+Left)",
|
||||
"Pause (Space)": "མཚམས་འཇོག་པ། (Space)",
|
||||
"Play (Space)": "གཏོང་བ། (Space)",
|
||||
"Skip forward 15 words": "ཚིག་ ༡༥ མདུན་ལ་བཤུད་པ།",
|
||||
"Forward 15 words (Shift+Right)": "ཚིག་ ༡༥ མདུན་ལ་བཤུད་པ། (Shift+Right)",
|
||||
"Pause:": "མཚམས་འཇོག་པ།",
|
||||
"Decrease speed": "མགྱོགས་ཚད་འགོར་དུ་གཏོང་བ།",
|
||||
"Slower (Left/Down)": "འགོར་བ། (Left/Down)",
|
||||
"Current speed": "ད་ལྟའི་མགྱོགས་ཚད།",
|
||||
"Increase speed": "མགྱོགས་ཚད་མྱུར་དུ་གཏོང་བ།",
|
||||
"Faster (Right/Up)": "མྱུར་བ། (Right/Up)",
|
||||
"Start RSVP Reading": "RSVP ཀློག་འགོ་འཛུགས་པ།",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "མ་ལག་ཤེལ་སྒོའི་གསལ་ཚད།",
|
||||
"Page:": "ཤོག་ལྷེ།:",
|
||||
"Page: {{number}}": "ཤོག་ལྷེ།: {{number}}",
|
||||
"Annotation page number": "མཆན་འགྲེལ་ཤོག་ཨང་།"
|
||||
"Annotation page number": "མཆན་འགྲེལ་ཤོག་ཨང་།",
|
||||
"Translating...": "ཡིག་བསྒྱུར་བྱེད་བཞིན་པ།...",
|
||||
"Show Battery Percentage": "གློག་རྫས་བརྒྱ་ཆ་སྟོན།",
|
||||
"Hide Scrollbar": "འགྲིལ་ཤིང་སྦེད་པ།",
|
||||
"Skip to last reading position": "མཐའ་མའི་ཀློག་གནས་སུ་མཆོང་",
|
||||
"Show context": "སྐབས་དོན་མངོན་པ",
|
||||
"Hide context": "སྐབས་དོན་སྦས་པ",
|
||||
"Decrease font size": "ཡིག་གཟུགས་ཆུང་དུ་གཏོང་བ",
|
||||
"Increase font size": "ཡིག་གཟུགས་ཆེ་རུ་གཏོང་བ",
|
||||
"Focus": "དམིགས་གཏད",
|
||||
"Theme color": "བརྗོད་དོན་ཚོན་མདོག",
|
||||
"Import failed": "ནང་འདྲེན་མི་ཐུབ།",
|
||||
"Available Formatters:": "སྒྲིག་བཟོ་ཆས་ཡོད་པ།:",
|
||||
"Format date": "ཚེས་གྲངས་སྒྲིག་བཟོ",
|
||||
"Markdown block quote (> per line)": "Markdown དྲིལ་བསྡུ་འདྲེན་ (མི་ལྟར་ > )",
|
||||
"Newlines to <br>": "གསར་བརྗེ་ <br> ལ་བསྒྱུར",
|
||||
"Change case": "ཡིག་གཟུགས་བསྒྱུར",
|
||||
"Trim whitespace": "སྟོང་ཆ་གཙང་བཟོ",
|
||||
"Truncate to n characters": "n ཡི་གེར་ཐུང་དུ་བཅད",
|
||||
"Replace text": "ཡི་གེ་བརྗེ་བ",
|
||||
"Fallback value": "ཚབ་ཀྱི་གྲངས་ཀ",
|
||||
"Get length": "རིང་ཚད་ལེན",
|
||||
"First/last element": "དང་པོ/མཇུག་གི་རྒྱུ་ཆ",
|
||||
"Join array": "ཚོ་སྒྲིག་སྦྲེལ",
|
||||
"Backup failed: {{error}}": "གྲབས་ཉར་བྱེད་མ་ཐུབ: {{error}}",
|
||||
"Select Backup": "གྲབས་ཉར་འདེམས་པ",
|
||||
"Restore failed: {{error}}": "སླར་གསོ་བྱེད་མ་ཐུབ: {{error}}",
|
||||
"Backup & Restore": "གྲབས་ཉར་དང་སླར་གསོ",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "ཁྱོད་ཀྱི་དཔེ་མཛོད་ཀྱི་གྲབས་ཉར་བཟོ་བའམ་སྔོན་གྱི་གྲབས་ཉར་ནས་སླར་གསོ་བྱོས། སླར་གསོ་བྱས་ན་ཁྱོད་ཀྱི་ད་ལྟའི་དཔེ་མཛོད་དང་སྤྲོད་རེས་བྱེད།",
|
||||
"Backup Library": "དཔེ་མཛོད་གྲབས་ཉར",
|
||||
"Restore Library": "དཔེ་མཛོད་སླར་གསོ",
|
||||
"Creating backup...": "གྲབས་ཉར་བཟོ་བཞིན་པ...",
|
||||
"Restoring library...": "དཔེ་མཛོད་སླར་གསོ་བྱེད་བཞིན་པ...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} རྣམ་གྲངས",
|
||||
"Backup completed successfully!": "གྲབས་ཉར་ལེགས་གྲུབ་བྱུང་!",
|
||||
"Restore completed successfully!": "སླར་གསོ་ལེགས་གྲུབ་བྱུང་!",
|
||||
"Your library has been saved to the selected location.": "ཁྱོད་ཀྱི་དཔེ་མཛོད་འདེམས་ས་དེར་ཉར་ཟིན།",
|
||||
"{{added}} books added, {{updated}} books updated.": "དཔེ་དེབ {{added}} བསྣན་ཟིན, {{updated}} གསར་བསྒྱུར་བྱས་ཟིན།",
|
||||
"Operation failed": "བཀོལ་སྤྱོད་བྱེད་མ་ཐུབ",
|
||||
"Loading library...": "དཔེ་མཛོད་འཇུག་བཞིན་པ...",
|
||||
"{{count}} books refreshed_other": "تم تحديث {{count}} كتب",
|
||||
"Failed to refresh metadata": "མེ་ཊ་ཌེ་ཊ་གསར་བསྒྱུར་བྱེད་མ་ཐུབ",
|
||||
"Refresh Metadata": "མེ་ཊ་ཌེ་ཊ་གསར་བསྒྱུར",
|
||||
"Keyboard Shortcuts": "མཐེབ་གཞོང་མྱུར་ལམ།",
|
||||
"View all keyboard shortcuts": "མཐེབ་གཞོང་མྱུར་ལམ་ཚང་མ་ལྟ་བ།",
|
||||
"Switch Sidebar Tab": "ཟུར་འབར་གྱི་ཤོག་བྱང་བརྗེ་བ།",
|
||||
"Toggle Notebook": "ཟིན་དེབ་སྟོན་པ།/སྦས་པ།",
|
||||
"Search in Book": "དཔེ་དེབ་ནང་འཚོལ་བ།",
|
||||
"Toggle Scroll Mode": "འཁྱོག་སྒུལ་རྣམ་པ་བརྗེ་བ།",
|
||||
"Toggle Select Mode": "འདེམས་རྣམ་པ་བརྗེ་བ།",
|
||||
"Toggle Bookmark": "ཤོག་རྟགས་བརྗེ་བ།",
|
||||
"Toggle Text to Speech": "ཡི་གེ་སྒྲ་ལ་བརྗེ་བ།",
|
||||
"Play / Pause TTS": "སྒྲ་ཀློག་འགོ་འཛུགས / མཚམས་འཇོག",
|
||||
"Toggle Paragraph Mode": "དུམ་བུ་རྣམ་པ་བརྗེ་བ།",
|
||||
"Highlight Selection": "བདམས་པ་གསལ་བཀོད།",
|
||||
"Underline Selection": "བདམས་པའི་འོག་ཐིག",
|
||||
"Annotate Selection": "བདམས་པར་མཆན་འགོད།",
|
||||
"Search Selection": "བདམས་པ་འཚོལ་བ།",
|
||||
"Copy Selection": "བདམས་པ་བཤུ་བ།",
|
||||
"Translate Selection": "བདམས་པ་ཡིག་སྒྱུར།",
|
||||
"Dictionary Lookup": "ཚིག་མཛོད་ནང་འཚོལ་བ།",
|
||||
"Wikipedia Lookup": "ཝེ་ཁེ་པི་ཌི་ཡར་འཚོལ་བ།",
|
||||
"Read Aloud Selection": "བདམས་པ་སྒྲ་ཆེན་པོས་ཀློག",
|
||||
"Proofread Selection": "བདམས་པ་ཞུ་དག",
|
||||
"Open Settings": "སྒྲིག་འགོད་ཁ་ཕྱེ་བ།",
|
||||
"Open Command Palette": "བཀའ་བརྡའི་པང་ཁ་ཕྱེ་བ།",
|
||||
"Show Keyboard Shortcuts": "མཐེབ་གཞོང་མྱུར་ལམ་སྟོན་པ།",
|
||||
"Open Books": "དཔེ་དེབ་ཁ་ཕྱེ་བ།",
|
||||
"Toggle Fullscreen": "ཡོངས་ཁྱབ་བརྗེ་བ།",
|
||||
"Close Window": "སྒེའུ་ཁུང་ཁ་རྒྱག",
|
||||
"Quit App": "ཉེར་སྤྱོད་ཁ་རྒྱག",
|
||||
"Go Left / Previous Page": "གཡོན་དུ / སྔོན་མའི་ཤོག་ངོས།",
|
||||
"Go Right / Next Page": "གཡས་སུ / རྗེས་མའི་ཤོག་ངོས།",
|
||||
"Go Up": "གོང་དུ།",
|
||||
"Go Down": "འོག་ཏུ།",
|
||||
"Previous Chapter": "སྔོན་མའི་ལེའུ།",
|
||||
"Next Chapter": "རྗེས་མའི་ལེའུ།",
|
||||
"Scroll Half Page Down": "ཤོག་ངོས་ཕྱེད་ཀ་འོག་ཏུ།",
|
||||
"Scroll Half Page Up": "ཤོག་ངོས་ཕྱེད་ཀ་གོང་དུ།",
|
||||
"Save Note": "ཟིན་བྲིས་ཉར་བ།",
|
||||
"Single Section Scroll": "དོན་ཚན་གཅིག་འགུལ་བ།",
|
||||
"General": "སྤྱིར་བཏང་།",
|
||||
"Text to Speech": "ཡི་གེ་ནས་སྒྲ།",
|
||||
"Zoom": "ཆེ་རུ་གཏོང་།",
|
||||
"Window": "སྒེའུ་ཁུང་།",
|
||||
"Your Bookshelf": "ཁྱོད་ཀྱི་དཔེ་མཛོད།",
|
||||
"TTS": "ཡི་གེ་ནས་སྒྲ",
|
||||
"Media Info": "སྨྱན་བྱད་ཆ་འཕྲིན",
|
||||
"Update Frequency": "གསར་སྒྱུར་ཐེངས་གྲངས",
|
||||
"Every Sentence": "ཚིག་གྲུབ་རེ་རེ",
|
||||
"Every Paragraph": "དུམ་མཚམས་རེ་རེ",
|
||||
"Every Chapter": "ལེའུ་རེ་རེ",
|
||||
"TTS Media Info Update Frequency": "TTS སྨྱན་བྱད་ཆ་འཕྲིན་གསར་སྒྱུར་ཐེངས་གྲངས",
|
||||
"Select reading speed": "ཀློག་པའི་མྱུར་ཚད་འདེམས།",
|
||||
"Drag to seek": "འཚོལ་བར་འདྲུད།",
|
||||
"Punctuation Delay": "ཚེག་ཤད་ཕྱིར་འགྱངས།",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "མུ་མཐུད་མ་བྱས་གོང་ OPDS འབྲེལ་མཐུད་འདི་དྲ་རྒྱའི་ཉེར་སྤྱོད་ཐོག་ Readest ཞབས་ཞུ་བ་བརྒྱུད་ནས་བསྐུར་རྒྱུ་ཡིན་པ་གཏན་འཁེལ་བྱོས།",
|
||||
"Custom Headers": "སྲོལ་སྒྲིག་མགོ་བརྗེ",
|
||||
"Custom Headers (optional)": "སྲོལ་སྒྲིག་མགོ་བརྗེ (གདམ་གའི)",
|
||||
"Add one header per line using \"Header-Name: value\".": "\"Header-Name: value\" བེད་སྤྱོད་བྱས་ནས་ཕྲེང་རེ་རེའི་ནང་མགོ་བརྗེ་གཅིག་སྣོན།",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "OPDS འབྲེལ་མཐུད་འདི་དྲ་རྒྱའི་ཉེར་སྤྱོད་ཐོག་ Readest ཞབས་ཞུ་བ་བརྒྱུད་ནས་བསྐུར་རྒྱུ་ཡིན་པ་ངས་ཧ་གོ། ང་ Readest ལ་ངེས་འཛིན་འམ་མགོ་བརྗེ་འདི་དག་ཡིད་ཆེས་མེད་ན། ང་ས་གནས་ཉེར་སྤྱོད་བེད་སྤྱོད་བྱ་དགོས།",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcover ལ་འབྲེལ་མཐུད་བྱེད་ཐུབ་མ་སོང་། ཁྱེད་ཀྱི་དྲ་རྒྱའི་འབྲེལ་མཐུད་ལ་ཞིབ་བཤེར་གནང་།",
|
||||
"Invalid Hardcover API token": "Hardcover API ཐོབ་ཐང་མ་ཆིག་སྒྲིལ་མིན།",
|
||||
"Disconnected from Hardcover": "Hardcover ནས་འབྲེལ་མཐུད་ཆད་སོང་།",
|
||||
"Hardcover Settings": "Hardcover སྒྲིག་འགོད",
|
||||
"Connected to Hardcover": "Hardcover ལ་འབྲེལ་མཐུད་བྱས་ཟིན།",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "ཀློག་འགྲོས་དང་མཆན་ཟླ་སྒྲིག་བྱེད་པར་ཁྱེད་ཀྱི Hardcover ཐོ་ཁུངས་འབྲེལ་མཐུད་བྱོས།",
|
||||
"Get your API token from hardcover.app → Settings → API.": "hardcover.app → སྒྲིག་འགོད → API ནས་ཁྱེད་ཀྱི API ཐོབ་ཐང་ལེན།",
|
||||
"API Token": "API ཐོབ་ཐང་།",
|
||||
"Paste your Hardcover API token": "ཁྱེད་ཀྱི Hardcover API ཐོབ་ཐང་སྦྱར།",
|
||||
"Hardcover sync enabled for this book": "དེབ་འདིའི་ཆེད་ Hardcover ཟླ་སྒྲིག་སྤྱོད་འགོ་ཚུགས།",
|
||||
"Hardcover sync disabled for this book": "དེབ་འདིའི་ཆེད་ Hardcover ཟླ་སྒྲིག་མཚམས་བཞག",
|
||||
"Hardcover Sync": "Hardcover ཟླ་སྒྲིག",
|
||||
"Enable for This Book": "དེབ་འདིའི་ཆེད་སྤྱོད་འགོ་ཚུགས།",
|
||||
"Push Notes": "མཆན་གཏོང་བ",
|
||||
"Enable Hardcover sync for this book first.": "སྔོན་ལ་དེབ་འདིའི་ཆེད་ Hardcover ཟླ་སྒྲིག་སྤྱོད་འགོ་ཚུགས།",
|
||||
"No annotations or excerpts to sync for this book.": "དེབ་འདིའི་ཆེད་ཟླ་སྒྲིག་བྱེད་པའི་མཆན་ཡང་ན་དྲངས་ཡིག་མེད།",
|
||||
"Configure Hardcover in Settings first.": "སྔོན་ལ་སྒྲིག་འགོད་ནང་ Hardcover རིགས་སྒྲིག་བྱོས།",
|
||||
"No new Hardcover note changes to sync.": "ཟླ་སྒྲིག་བྱེད་པའི་ Hardcover མཆན་གསར་པ་མེད།",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover ཟླ་སྒྲིག་བྱས་ཟིན: {{inserted}} གསར་པ, {{updated}} གསར་བཅོས, {{skipped}} མི་འགྱུར",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover མཆན་ཟླ་སྒྲིག་མི་ཚོགས: {{error}}",
|
||||
"Reading progress synced to Hardcover": "ཀློག་འགྲོས་ Hardcover ལ་ཟླ་སྒྲིག་བྱས་ཟིན།",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover ཀློག་འགྲོས་ཟླ་སྒྲིག་མི་ཚོགས: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "ཡོད་བཞིན་པའི་འབྱུང་ཁུངས་ཐོབ་ཐང་གཞག"
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Schreibmodus",
|
||||
"Your Library": "Ihre Bibliothek",
|
||||
"TTS not supported for PDF": "TTS wird für PDF nicht unterstützt",
|
||||
"Override Book Font": "Buch-Schriftart überschreiben",
|
||||
"Apply to All Books": "Auf alle Bücher anwenden",
|
||||
"Apply to This Book": "Auf dieses Buch anwenden",
|
||||
@@ -184,7 +183,6 @@
|
||||
"RTL Direction": "RTL-Richtung",
|
||||
"Maximum Column Height": "Maximale Spaltenhöhe",
|
||||
"Maximum Column Width": "Maximale Spaltenbreite",
|
||||
"Continuous Scroll": "Kontinuierliches Scrollen",
|
||||
"Fullscreen": "Vollbild",
|
||||
"No supported files found. Supported formats: {{formats}}": "Keine unterstützten Dateien gefunden. Unterstützte Formate: {{formats}}",
|
||||
"Drop to Import Books": "Zum Importieren von Büchern ablegen",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "Suche löschen",
|
||||
"Clear search history": "Suchverlauf löschen",
|
||||
"Tap to Toggle Footer": "Tippen, um Fußzeile umzuschalten",
|
||||
"Show Current Time": "Aktuelle Uhrzeit anzeigen",
|
||||
"Use 24 Hour Clock": "24-Stunden-Format verwenden",
|
||||
"Show Current Battery Status": "Aktuellen Akkustand anzeigen",
|
||||
"Exported successfully": "Erfolgreich exportiert",
|
||||
"Book exported successfully.": "Buch erfolgreich exportiert.",
|
||||
"Failed to export the book.": "Buch konnte nicht exportiert werden.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "Wörter",
|
||||
"{{time}} left": "{{time}} übrig",
|
||||
"Reading progress": "Lesefortschritt",
|
||||
"Click to seek": "Klicken zum Suchen",
|
||||
"Skip back 15 words": "15 Wörter zurück",
|
||||
"Back 15 words (Shift+Left)": "15 Wörter zurück (Shift+Links)",
|
||||
"Pause (Space)": "Pause (Leertaste)",
|
||||
"Play (Space)": "Abspielen (Leertaste)",
|
||||
"Skip forward 15 words": "15 Wörter vor",
|
||||
"Forward 15 words (Shift+Right)": "15 Wörter vor (Shift+Rechts)",
|
||||
"Pause:": "Pause:",
|
||||
"Decrease speed": "Geschwindigkeit verringern",
|
||||
"Slower (Left/Down)": "Langsamer (Links/Unten)",
|
||||
"Current speed": "Aktuelle Geschwindigkeit",
|
||||
"Increase speed": "Geschwindigkeit erhöhen",
|
||||
"Faster (Right/Up)": "Schneller (Rechts/Oben)",
|
||||
"Start RSVP Reading": "RSVP-Lesen starten",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "System-Bildschirmhelligkeit",
|
||||
"Page:": "Seite:",
|
||||
"Page: {{number}}": "Seite: {{number}}",
|
||||
"Annotation page number": "Seitenzahl der Anmerkung"
|
||||
"Annotation page number": "Seitenzahl der Anmerkung",
|
||||
"Translating...": "Übersetzen...",
|
||||
"Show Battery Percentage": "Akkustand in Prozent anzeigen",
|
||||
"Hide Scrollbar": "Scrollleiste ausblenden",
|
||||
"Skip to last reading position": "Zur letzten Leseposition springen",
|
||||
"Show context": "Kontext anzeigen",
|
||||
"Hide context": "Kontext ausblenden",
|
||||
"Decrease font size": "Schriftgröße verkleinern",
|
||||
"Increase font size": "Schriftgröße vergrößern",
|
||||
"Focus": "Fokus",
|
||||
"Theme color": "Themenfarbe",
|
||||
"Import failed": "Import fehlgeschlagen",
|
||||
"Available Formatters:": "Verfügbare Formatierer:",
|
||||
"Format date": "Datum formatieren",
|
||||
"Markdown block quote (> per line)": "Markdown-Blockzitat (> pro Zeile)",
|
||||
"Newlines to <br>": "Zeilenumbrüche zu <br>",
|
||||
"Change case": "Groß-/Kleinschreibung ändern",
|
||||
"Trim whitespace": "Leerzeichen entfernen",
|
||||
"Truncate to n characters": "Auf n Zeichen kürzen",
|
||||
"Replace text": "Text ersetzen",
|
||||
"Fallback value": "Ersatzwert",
|
||||
"Get length": "Länge ermitteln",
|
||||
"First/last element": "Erstes/letztes Element",
|
||||
"Join array": "Array verbinden",
|
||||
"Backup failed: {{error}}": "Sicherung fehlgeschlagen: {{error}}",
|
||||
"Select Backup": "Sicherung auswählen",
|
||||
"Restore failed: {{error}}": "Wiederherstellung fehlgeschlagen: {{error}}",
|
||||
"Backup & Restore": "Sichern & Wiederherstellen",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Erstellen Sie eine Sicherung Ihrer Bibliothek oder stellen Sie eine frühere Sicherung wieder her. Die Wiederherstellung wird mit Ihrer aktuellen Bibliothek zusammengeführt.",
|
||||
"Backup Library": "Bibliothek sichern",
|
||||
"Restore Library": "Bibliothek wiederherstellen",
|
||||
"Creating backup...": "Sicherung wird erstellt...",
|
||||
"Restoring library...": "Bibliothek wird wiederhergestellt...",
|
||||
"{{current}} of {{total}} items": "{{current}} von {{total}} Elementen",
|
||||
"Backup completed successfully!": "Sicherung erfolgreich abgeschlossen!",
|
||||
"Restore completed successfully!": "Wiederherstellung erfolgreich abgeschlossen!",
|
||||
"Your library has been saved to the selected location.": "Ihre Bibliothek wurde am ausgewählten Ort gespeichert.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} Bücher hinzugefügt, {{updated}} Bücher aktualisiert.",
|
||||
"Operation failed": "Vorgang fehlgeschlagen",
|
||||
"Loading library...": "Bibliothek wird geladen...",
|
||||
"{{count}} books refreshed_one": "{{count}} Buch aktualisiert",
|
||||
"{{count}} books refreshed_other": "{{count}} Bücher aktualisiert",
|
||||
"Failed to refresh metadata": "Metadaten-Aktualisierung fehlgeschlagen",
|
||||
"Refresh Metadata": "Metadaten aktualisieren",
|
||||
"Keyboard Shortcuts": "Tastaturkürzel",
|
||||
"View all keyboard shortcuts": "Alle Tastaturkürzel anzeigen",
|
||||
"Switch Sidebar Tab": "Seitenleisten-Tab wechseln",
|
||||
"Toggle Notebook": "Notizbuch ein-/ausblenden",
|
||||
"Search in Book": "Im Buch suchen",
|
||||
"Toggle Scroll Mode": "Scrollmodus umschalten",
|
||||
"Toggle Select Mode": "Auswahlmodus umschalten",
|
||||
"Toggle Bookmark": "Lesezeichen umschalten",
|
||||
"Toggle Text to Speech": "Sprachausgabe umschalten",
|
||||
"Play / Pause TTS": "Sprachausgabe abspielen / pausieren",
|
||||
"Toggle Paragraph Mode": "Absatzmodus umschalten",
|
||||
"Highlight Selection": "Auswahl hervorheben",
|
||||
"Underline Selection": "Auswahl unterstreichen",
|
||||
"Annotate Selection": "Auswahl kommentieren",
|
||||
"Search Selection": "Auswahl suchen",
|
||||
"Copy Selection": "Auswahl kopieren",
|
||||
"Translate Selection": "Auswahl übersetzen",
|
||||
"Dictionary Lookup": "Im Wörterbuch nachschlagen",
|
||||
"Wikipedia Lookup": "In Wikipedia nachschlagen",
|
||||
"Read Aloud Selection": "Auswahl vorlesen",
|
||||
"Proofread Selection": "Auswahl korrekturlesen",
|
||||
"Open Settings": "Einstellungen öffnen",
|
||||
"Open Command Palette": "Befehlspalette öffnen",
|
||||
"Show Keyboard Shortcuts": "Tastaturkürzel anzeigen",
|
||||
"Open Books": "Bücher öffnen",
|
||||
"Toggle Fullscreen": "Vollbild umschalten",
|
||||
"Close Window": "Fenster schließen",
|
||||
"Quit App": "App beenden",
|
||||
"Go Left / Previous Page": "Nach links / Vorherige Seite",
|
||||
"Go Right / Next Page": "Nach rechts / Nächste Seite",
|
||||
"Go Up": "Nach oben",
|
||||
"Go Down": "Nach unten",
|
||||
"Previous Chapter": "Vorheriges Kapitel",
|
||||
"Next Chapter": "Nächstes Kapitel",
|
||||
"Scroll Half Page Down": "Halbe Seite nach unten scrollen",
|
||||
"Scroll Half Page Up": "Halbe Seite nach oben scrollen",
|
||||
"Save Note": "Notiz speichern",
|
||||
"Single Section Scroll": "Einzelabschnitt-Scrollen",
|
||||
"General": "Allgemein",
|
||||
"Text to Speech": "Sprachausgabe",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Fenster",
|
||||
"Your Bookshelf": "Dein Bücherregal",
|
||||
"TTS": "Sprachausgabe",
|
||||
"Media Info": "Medieninfo",
|
||||
"Update Frequency": "Aktualisierungshäufigkeit",
|
||||
"Every Sentence": "Jeden Satz",
|
||||
"Every Paragraph": "Jeden Absatz",
|
||||
"Every Chapter": "Jedes Kapitel",
|
||||
"TTS Media Info Update Frequency": "TTS-Medieninfo-Aktualisierungshäufigkeit",
|
||||
"Select reading speed": "Lesegeschwindigkeit wählen",
|
||||
"Drag to seek": "Ziehen zum Suchen",
|
||||
"Punctuation Delay": "Satzzeichen-Verzögerung",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Bitte bestätigen Sie, dass diese OPDS-Verbindung über Readest-Server in der Web-App weitergeleitet wird, bevor Sie fortfahren.",
|
||||
"Custom Headers": "Benutzerdefinierte Header",
|
||||
"Custom Headers (optional)": "Benutzerdefinierte Header (optional)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Fügen Sie pro Zeile einen Header im Format \"Header-Name: value\" hinzu.",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Ich verstehe, dass diese OPDS-Verbindung über Readest-Server in der Web-App weitergeleitet wird. Wenn ich Readest meine Anmeldedaten oder Header nicht anvertraue, sollte ich stattdessen die native App verwenden.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Verbindung zu Hardcover nicht möglich. Bitte überprüfen Sie Ihre Netzwerkverbindung.",
|
||||
"Invalid Hardcover API token": "Ungültiges Hardcover-API-Token",
|
||||
"Disconnected from Hardcover": "Von Hardcover getrennt",
|
||||
"Hardcover Settings": "Hardcover-Einstellungen",
|
||||
"Connected to Hardcover": "Mit Hardcover verbunden",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Verbinden Sie Ihr Hardcover-Konto, um Lesefortschritt und Notizen zu synchronisieren.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Holen Sie Ihr API-Token von hardcover.app → Einstellungen → API.",
|
||||
"API Token": "API-Token",
|
||||
"Paste your Hardcover API token": "Fügen Sie Ihr Hardcover-API-Token ein",
|
||||
"Hardcover sync enabled for this book": "Hardcover-Synchronisierung für dieses Buch aktiviert",
|
||||
"Hardcover sync disabled for this book": "Hardcover-Synchronisierung für dieses Buch deaktiviert",
|
||||
"Hardcover Sync": "Hardcover-Synchronisierung",
|
||||
"Enable for This Book": "Für dieses Buch aktivieren",
|
||||
"Push Notes": "Notizen übertragen",
|
||||
"Enable Hardcover sync for this book first.": "Aktivieren Sie zuerst die Hardcover-Synchronisierung für dieses Buch.",
|
||||
"No annotations or excerpts to sync for this book.": "Keine Anmerkungen oder Auszüge zum Synchronisieren für dieses Buch.",
|
||||
"Configure Hardcover in Settings first.": "Konfigurieren Sie zuerst Hardcover in den Einstellungen.",
|
||||
"No new Hardcover note changes to sync.": "Keine neuen Hardcover-Notizänderungen zum Synchronisieren.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover synchronisiert: {{inserted}} neu, {{updated}} aktualisiert, {{skipped}} unverändert",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover-Notizsynchronisierung fehlgeschlagen: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Lesefortschritt mit Hardcover synchronisiert",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover-Fortschrittssynchronisierung fehlgeschlagen: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Vorhandene Quellkennung beibehalten"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Βικιπαίδεια",
|
||||
"Writing Mode": "Λειτουργία γραφής",
|
||||
"Your Library": "Η βιβλιοθήκη σας",
|
||||
"TTS not supported for PDF": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται για αρχεία PDF",
|
||||
"Override Book Font": "Παράκαμψη γραμματοσειράς βιβλίου",
|
||||
"Apply to All Books": "Εφαρμογή σε όλα τα βιβλία",
|
||||
"Apply to This Book": "Εφαρμογή σε αυτό το βιβλίο",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Κατεύθυνση RTL",
|
||||
"Maximum Column Height": "Μέγιστο ύψος στήλης",
|
||||
"Maximum Column Width": "Μέγιστο πλάτος στήλης",
|
||||
"Continuous Scroll": "Συνεχής κύλιση",
|
||||
"Fullscreen": "Πλήρης οθόνη",
|
||||
"No supported files found. Supported formats: {{formats}}": "Δεν βρέθηκαν υποστηριζόμενα αρχεία. Υποστηριζόμενες μορφές: {{formats}}",
|
||||
"Drop to Import Books": "Ρίξτε για εισαγωγή βιβλίων",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "Εκκαθάριση αναζήτησης",
|
||||
"Clear search history": "Εκκαθάριση ιστορικού αναζήτησης",
|
||||
"Tap to Toggle Footer": "Πατήστε για εναλλαγή υποσέλιδου",
|
||||
"Show Current Time": "Εμφάνιση τρέχουσας ώρας",
|
||||
"Use 24 Hour Clock": "Χρήση 24ωρης μορφής ώρας",
|
||||
"Show Current Battery Status": "Εμφάνιση τρέχουσας κατάστασης μπαταρίας",
|
||||
"Exported successfully": "Εξαγωγή επιτυχής",
|
||||
"Book exported successfully.": "Το βιβλίο εξήχθη επιτυχώς.",
|
||||
"Failed to export the book.": "Αποτυχία εξαγωγής του βιβλίου.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "λέξεις",
|
||||
"{{time}} left": "{{time}} απομένουν",
|
||||
"Reading progress": "Πρόοδος ανάγνωσης",
|
||||
"Click to seek": "Κάντε κλικ για αναζήτηση",
|
||||
"Skip back 15 words": "Επιστροφή 15 λέξεων",
|
||||
"Back 15 words (Shift+Left)": "Πίσω 15 λέξεις (Shift+Αριστερά)",
|
||||
"Pause (Space)": "Παύση (Διάστημα)",
|
||||
"Play (Space)": "Αναπαραγωγή (Διάστημα)",
|
||||
"Skip forward 15 words": "Προώθηση 15 λέξεων",
|
||||
"Forward 15 words (Shift+Right)": "Εμπρός 15 λέξεις (Shift+Δεξιά)",
|
||||
"Pause:": "Παύση:",
|
||||
"Decrease speed": "Μείωση ταχύτητας",
|
||||
"Slower (Left/Down)": "Πιο αργά (Αριστερά/Κάτω)",
|
||||
"Current speed": "Τρέχουσα ταχύτητα",
|
||||
"Increase speed": "Αύξηση ταχύτητας",
|
||||
"Faster (Right/Up)": "Πιο γρήγορα (Δεξιά/Πάνω)",
|
||||
"Start RSVP Reading": "Έναρξη ανάγνωσης RSVP",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "Φωτεινότητα οθόνης συστήματος",
|
||||
"Page:": "Σελίδα:",
|
||||
"Page: {{number}}": "Σελίδα: {{number}}",
|
||||
"Annotation page number": "Αριθμός σελίδας σχολίου"
|
||||
"Annotation page number": "Αριθμός σελίδας σχολίου",
|
||||
"Translating...": "Μετάφραση...",
|
||||
"Show Battery Percentage": "Εμφάνιση ποσοστού μπαταρίας",
|
||||
"Hide Scrollbar": "Απόκρυψη γραμμής κύλισης",
|
||||
"Skip to last reading position": "Μετάβαση στην τελευταία θέση ανάγνωσης",
|
||||
"Show context": "Εμφάνιση πλαισίου",
|
||||
"Hide context": "Απόκρυψη πλαισίου",
|
||||
"Decrease font size": "Μείωση μεγέθους γραμματοσειράς",
|
||||
"Increase font size": "Αύξηση μεγέθους γραμματοσειράς",
|
||||
"Focus": "Εστίαση",
|
||||
"Theme color": "Χρώμα θέματος",
|
||||
"Import failed": "Η εισαγωγή απέτυχε",
|
||||
"Available Formatters:": "Διαθέσιμοι μορφοποιητές:",
|
||||
"Format date": "Μορφοποίηση ημερομηνίας",
|
||||
"Markdown block quote (> per line)": "Παράθεση Markdown (> ανά γραμμή)",
|
||||
"Newlines to <br>": "Αλλαγές γραμμής σε <br>",
|
||||
"Change case": "Αλλαγή πεζών/κεφαλαίων",
|
||||
"Trim whitespace": "Αφαίρεση κενών",
|
||||
"Truncate to n characters": "Περικοπή σε n χαρακτήρες",
|
||||
"Replace text": "Αντικατάσταση κειμένου",
|
||||
"Fallback value": "Εναλλακτική τιμή",
|
||||
"Get length": "Λήψη μήκους",
|
||||
"First/last element": "Πρώτο/τελευταίο στοιχείο",
|
||||
"Join array": "Ένωση πίνακα",
|
||||
"Backup failed: {{error}}": "Η δημιουργία αντιγράφου ασφαλείας απέτυχε: {{error}}",
|
||||
"Select Backup": "Επιλογή αντιγράφου ασφαλείας",
|
||||
"Restore failed: {{error}}": "Η επαναφορά απέτυχε: {{error}}",
|
||||
"Backup & Restore": "Αντίγραφα ασφαλείας & Επαναφορά",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Δημιουργήστε αντίγραφο ασφαλείας της βιβλιοθήκης σας ή επαναφέρετε από προηγούμενο αντίγραφο. Η επαναφορά θα συγχωνευθεί με την τρέχουσα βιβλιοθήκη σας.",
|
||||
"Backup Library": "Αντίγραφο βιβλιοθήκης",
|
||||
"Restore Library": "Επαναφορά βιβλιοθήκης",
|
||||
"Creating backup...": "Δημιουργία αντιγράφου ασφαλείας...",
|
||||
"Restoring library...": "Επαναφορά βιβλιοθήκης...",
|
||||
"{{current}} of {{total}} items": "{{current}} από {{total}} στοιχεία",
|
||||
"Backup completed successfully!": "Το αντίγραφο ασφαλείας ολοκληρώθηκε!",
|
||||
"Restore completed successfully!": "Η επαναφορά ολοκληρώθηκε!",
|
||||
"Your library has been saved to the selected location.": "Η βιβλιοθήκη σας αποθηκεύτηκε στην επιλεγμένη τοποθεσία.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} βιβλία προστέθηκαν, {{updated}} βιβλία ενημερώθηκαν.",
|
||||
"Operation failed": "Η λειτουργία απέτυχε",
|
||||
"Loading library...": "Φόρτωση βιβλιοθήκης...",
|
||||
"{{count}} books refreshed_one": "{{count}} βιβλίο ανανεώθηκε",
|
||||
"{{count}} books refreshed_other": "{{count}} βιβλία ανανεώθηκαν",
|
||||
"Failed to refresh metadata": "Αποτυχία ανανέωσης μεταδεδομένων",
|
||||
"Refresh Metadata": "Ανανέωση μεταδεδομένων",
|
||||
"Keyboard Shortcuts": "Συντομεύσεις πληκτρολογίου",
|
||||
"View all keyboard shortcuts": "Προβολή όλων των συντομεύσεων",
|
||||
"Switch Sidebar Tab": "Εναλλαγή καρτέλας πλαϊνής γραμμής",
|
||||
"Toggle Notebook": "Εμφάνιση/Απόκρυψη σημειωματάριου",
|
||||
"Search in Book": "Αναζήτηση στο βιβλίο",
|
||||
"Toggle Scroll Mode": "Εναλλαγή λειτουργίας κύλισης",
|
||||
"Toggle Select Mode": "Εναλλαγή λειτουργίας επιλογής",
|
||||
"Toggle Bookmark": "Εναλλαγή σελιδοδείκτη",
|
||||
"Toggle Text to Speech": "Εναλλαγή κειμένου σε ομιλία",
|
||||
"Play / Pause TTS": "Αναπαραγωγή / Παύση TTS",
|
||||
"Toggle Paragraph Mode": "Εναλλαγή λειτουργίας παραγράφου",
|
||||
"Highlight Selection": "Επισήμανση επιλογής",
|
||||
"Underline Selection": "Υπογράμμιση επιλογής",
|
||||
"Annotate Selection": "Σχολιασμός επιλογής",
|
||||
"Search Selection": "Αναζήτηση επιλογής",
|
||||
"Copy Selection": "Αντιγραφή επιλογής",
|
||||
"Translate Selection": "Μετάφραση επιλογής",
|
||||
"Dictionary Lookup": "Αναζήτηση στο λεξικό",
|
||||
"Wikipedia Lookup": "Αναζήτηση στη Wikipedia",
|
||||
"Read Aloud Selection": "Ανάγνωση επιλογής",
|
||||
"Proofread Selection": "Διόρθωση επιλογής",
|
||||
"Open Settings": "Άνοιγμα ρυθμίσεων",
|
||||
"Open Command Palette": "Άνοιγμα παλέτας εντολών",
|
||||
"Show Keyboard Shortcuts": "Εμφάνιση συντομεύσεων",
|
||||
"Open Books": "Άνοιγμα βιβλίων",
|
||||
"Toggle Fullscreen": "Εναλλαγή πλήρους οθόνης",
|
||||
"Close Window": "Κλείσιμο παραθύρου",
|
||||
"Quit App": "Έξοδος εφαρμογής",
|
||||
"Go Left / Previous Page": "Αριστερά / Προηγούμενη σελίδα",
|
||||
"Go Right / Next Page": "Δεξιά / Επόμενη σελίδα",
|
||||
"Go Up": "Πάνω",
|
||||
"Go Down": "Κάτω",
|
||||
"Previous Chapter": "Προηγούμενο κεφάλαιο",
|
||||
"Next Chapter": "Επόμενο κεφάλαιο",
|
||||
"Scroll Half Page Down": "Κύλιση μισής σελίδας κάτω",
|
||||
"Scroll Half Page Up": "Κύλιση μισής σελίδας πάνω",
|
||||
"Save Note": "Αποθήκευση σημείωσης",
|
||||
"Single Section Scroll": "Κύλιση μεμονωμένης ενότητας",
|
||||
"General": "Γενικά",
|
||||
"Text to Speech": "Κείμενο σε ομιλία",
|
||||
"Zoom": "Ζουμ",
|
||||
"Window": "Παράθυρο",
|
||||
"Your Bookshelf": "Η βιβλιοθήκη σου",
|
||||
"TTS": "Εκφώνηση",
|
||||
"Media Info": "Πληροφορίες μέσων",
|
||||
"Update Frequency": "Συχνότητα ενημέρωσης",
|
||||
"Every Sentence": "Κάθε πρόταση",
|
||||
"Every Paragraph": "Κάθε παράγραφο",
|
||||
"Every Chapter": "Κάθε κεφάλαιο",
|
||||
"TTS Media Info Update Frequency": "Συχνότητα ενημέρωσης πληροφοριών μέσων TTS",
|
||||
"Select reading speed": "Επιλέξτε ταχύτητα ανάγνωσης",
|
||||
"Drag to seek": "Σύρετε για αναζήτηση",
|
||||
"Punctuation Delay": "Καθυστέρηση στίξης",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Επιβεβαιώστε ότι αυτή η σύνδεση OPDS θα δρομολογηθεί μέσω των διακομιστών Readest στην εφαρμογή ιστού πριν συνεχίσετε.",
|
||||
"Custom Headers": "Προσαρμοσμένες κεφαλίδες",
|
||||
"Custom Headers (optional)": "Προσαρμοσμένες κεφαλίδες (προαιρετικό)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Προσθέστε μία κεφαλίδα ανά γραμμή χρησιμοποιώντας \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Κατανοώ ότι αυτή η σύνδεση OPDS θα δρομολογηθεί μέσω των διακομιστών Readest στην εφαρμογή ιστού. Αν δεν εμπιστεύομαι τo Readest με αυτά τα διαπιστευτήρια ή τις κεφαλίδες, θα πρέπει να χρησιμοποιήσω την εγγενή εφαρμογή.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Αδυναμία σύνδεσης στο Hardcover. Ελέγξτε τη σύνδεση δικτύου σας.",
|
||||
"Invalid Hardcover API token": "Μη έγκυρο Hardcover API token",
|
||||
"Disconnected from Hardcover": "Αποσυνδέθηκε από το Hardcover",
|
||||
"Hardcover Settings": "Ρυθμίσεις Hardcover",
|
||||
"Connected to Hardcover": "Συνδεδεμένο στο Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Συνδέστε τον λογαριασμό σας Hardcover για συγχρονισμό προόδου ανάγνωσης και σημειώσεων.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Λάβετε το API token σας από hardcover.app → Ρυθμίσεις → API.",
|
||||
"API Token": "API Token",
|
||||
"Paste your Hardcover API token": "Επικολλήστε το Hardcover API token σας",
|
||||
"Hardcover sync enabled for this book": "Ο συγχρονισμός Hardcover ενεργοποιήθηκε για αυτό το βιβλίο",
|
||||
"Hardcover sync disabled for this book": "Ο συγχρονισμός Hardcover απενεργοποιήθηκε για αυτό το βιβλίο",
|
||||
"Hardcover Sync": "Συγχρονισμός Hardcover",
|
||||
"Enable for This Book": "Ενεργοποίηση για αυτό το βιβλίο",
|
||||
"Push Notes": "Αποστολή σημειώσεων",
|
||||
"Enable Hardcover sync for this book first.": "Ενεργοποιήστε πρώτα τον συγχρονισμό Hardcover για αυτό το βιβλίο.",
|
||||
"No annotations or excerpts to sync for this book.": "Δεν υπάρχουν σχολιασμοί ή αποσπάσματα για συγχρονισμό σε αυτό το βιβλίο.",
|
||||
"Configure Hardcover in Settings first.": "Ρυθμίστε πρώτα το Hardcover στις Ρυθμίσεις.",
|
||||
"No new Hardcover note changes to sync.": "Δεν υπάρχουν νέες αλλαγές σημειώσεων Hardcover για συγχρονισμό.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover συγχρονίστηκε: {{inserted}} νέα, {{updated}} ενημερωμένα, {{skipped}} αμετάβλητα",
|
||||
"Hardcover notes sync failed: {{error}}": "Αποτυχία συγχρονισμού σημειώσεων Hardcover: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Η πρόοδος ανάγνωσης συγχρονίστηκε στο Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Αποτυχία συγχρονισμού προόδου Hardcover: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Διατήρηση υπάρχοντος αναγνωριστικού πηγής"
|
||||
}
|
||||
|
||||
@@ -133,7 +133,6 @@
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Modo de escritura",
|
||||
"Your Library": "Tu biblioteca",
|
||||
"TTS not supported for PDF": "TTS no es compatible con PDF",
|
||||
"Override Book Font": "Sobrescribir fuente del libro",
|
||||
"Apply to All Books": "Aplicar a todos los libros",
|
||||
"Apply to This Book": "Aplicar a este libro",
|
||||
@@ -212,7 +211,6 @@
|
||||
"RTL Direction": "Dirección RTL",
|
||||
"Maximum Column Height": "Altura máxima de columna",
|
||||
"Maximum Column Width": "Ancho máximo de columna",
|
||||
"Continuous Scroll": "Desplazamiento continuo",
|
||||
"Fullscreen": "Pantalla completa",
|
||||
"No supported files found. Supported formats: {{formats}}": "No se encontraron archivos compatibles. Formatos compatibles: {{formats}}",
|
||||
"Drop to Import Books": "Soltar para importar libros",
|
||||
@@ -842,6 +840,9 @@
|
||||
"Clear search": "Borrar búsqueda",
|
||||
"Clear search history": "Borrar historial de búsqueda",
|
||||
"Tap to Toggle Footer": "Toca para alternar pie de página",
|
||||
"Show Current Time": "Mostrar hora actual",
|
||||
"Use 24 Hour Clock": "Usar formato de 24 horas",
|
||||
"Show Current Battery Status": "Mostrar el estado actual de la batería",
|
||||
"Exported successfully": "Exportado con éxito",
|
||||
"Book exported successfully.": "Libro exportado con éxito.",
|
||||
"Failed to export the book.": "Error al exportar el libro.",
|
||||
@@ -993,17 +994,14 @@
|
||||
"words": "palabras",
|
||||
"{{time}} left": "faltan {{time}}",
|
||||
"Reading progress": "Progreso de lectura",
|
||||
"Click to seek": "Hacer clic para buscar",
|
||||
"Skip back 15 words": "Retroceder 15 palabras",
|
||||
"Back 15 words (Shift+Left)": "Retroceder 15 palabras (Shift+Izquierda)",
|
||||
"Pause (Space)": "Pausa (Espacio)",
|
||||
"Play (Space)": "Reproducir (Espacio)",
|
||||
"Skip forward 15 words": "Adelantar 15 palabras",
|
||||
"Forward 15 words (Shift+Right)": "Adelantar 15 palabras (Shift+Derecha)",
|
||||
"Pause:": "Pausa:",
|
||||
"Decrease speed": "Disminuir velocidad",
|
||||
"Slower (Left/Down)": "Más lento (Izquierda/Abajo)",
|
||||
"Current speed": "Velocidad actual",
|
||||
"Increase speed": "Aumentar velocidad",
|
||||
"Faster (Right/Up)": "Más rápido (Derecha/Arriba)",
|
||||
"Start RSVP Reading": "Iniciar lectura RSVP",
|
||||
@@ -1071,5 +1069,131 @@
|
||||
"System Screen Brightness": "Brillo de pantalla del sistema",
|
||||
"Page:": "Página:",
|
||||
"Page: {{number}}": "Página: {{number}}",
|
||||
"Annotation page number": "Número de página de la anotación"
|
||||
"Annotation page number": "Número de página de la anotación",
|
||||
"Translating...": "Traduciendo...",
|
||||
"Show Battery Percentage": "Mostrar porcentaje de batería",
|
||||
"Hide Scrollbar": "Ocultar barra de desplazamiento",
|
||||
"Skip to last reading position": "Ir a la última posición de lectura",
|
||||
"Show context": "Mostrar contexto",
|
||||
"Hide context": "Ocultar contexto",
|
||||
"Decrease font size": "Reducir tamaño de fuente",
|
||||
"Increase font size": "Aumentar tamaño de fuente",
|
||||
"Focus": "Enfoque",
|
||||
"Theme color": "Color del tema",
|
||||
"Import failed": "Error de importación",
|
||||
"Available Formatters:": "Formateadores disponibles:",
|
||||
"Format date": "Formatear fecha",
|
||||
"Markdown block quote (> per line)": "Cita en bloque Markdown (> por línea)",
|
||||
"Newlines to <br>": "Saltos de línea a <br>",
|
||||
"Change case": "Cambiar mayúsculas/minúsculas",
|
||||
"Trim whitespace": "Recortar espacios",
|
||||
"Truncate to n characters": "Truncar a n caracteres",
|
||||
"Replace text": "Reemplazar texto",
|
||||
"Fallback value": "Valor alternativo",
|
||||
"Get length": "Obtener longitud",
|
||||
"First/last element": "Primer/último elemento",
|
||||
"Join array": "Unir array",
|
||||
"Backup failed: {{error}}": "Error en la copia de seguridad: {{error}}",
|
||||
"Select Backup": "Seleccionar copia de seguridad",
|
||||
"Restore failed: {{error}}": "Error en la restauración: {{error}}",
|
||||
"Backup & Restore": "Copia de seguridad y restauración",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Crea una copia de seguridad de tu biblioteca o restaura desde una copia anterior. La restauración se fusionará con tu biblioteca actual.",
|
||||
"Backup Library": "Hacer copia de seguridad",
|
||||
"Restore Library": "Restaurar biblioteca",
|
||||
"Creating backup...": "Creando copia de seguridad...",
|
||||
"Restoring library...": "Restaurando biblioteca...",
|
||||
"{{current}} of {{total}} items": "{{current}} de {{total}} elementos",
|
||||
"Backup completed successfully!": "¡Copia de seguridad completada!",
|
||||
"Restore completed successfully!": "¡Restauración completada!",
|
||||
"Your library has been saved to the selected location.": "Tu biblioteca se ha guardado en la ubicación seleccionada.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} libros añadidos, {{updated}} libros actualizados.",
|
||||
"Operation failed": "La operación falló",
|
||||
"Loading library...": "Cargando biblioteca...",
|
||||
"{{count}} books refreshed_one": "{{count}} libro actualizado",
|
||||
"{{count}} books refreshed_many": "{{count}} libros actualizados",
|
||||
"{{count}} books refreshed_other": "{{count}} libros actualizados",
|
||||
"Failed to refresh metadata": "Error al actualizar metadatos",
|
||||
"Refresh Metadata": "Actualizar metadatos",
|
||||
"Keyboard Shortcuts": "Atajos de teclado",
|
||||
"View all keyboard shortcuts": "Ver todos los atajos de teclado",
|
||||
"Switch Sidebar Tab": "Cambiar pestaña de la barra lateral",
|
||||
"Toggle Notebook": "Mostrar/ocultar cuaderno",
|
||||
"Search in Book": "Buscar en el libro",
|
||||
"Toggle Scroll Mode": "Alternar modo desplazamiento",
|
||||
"Toggle Select Mode": "Alternar modo selección",
|
||||
"Toggle Bookmark": "Alternar marcador",
|
||||
"Toggle Text to Speech": "Alternar texto a voz",
|
||||
"Play / Pause TTS": "Reproducir / Pausar TTS",
|
||||
"Toggle Paragraph Mode": "Alternar modo párrafo",
|
||||
"Highlight Selection": "Resaltar selección",
|
||||
"Underline Selection": "Subrayar selección",
|
||||
"Annotate Selection": "Anotar selección",
|
||||
"Search Selection": "Buscar selección",
|
||||
"Copy Selection": "Copiar selección",
|
||||
"Translate Selection": "Traducir selección",
|
||||
"Dictionary Lookup": "Buscar en diccionario",
|
||||
"Wikipedia Lookup": "Buscar en Wikipedia",
|
||||
"Read Aloud Selection": "Leer selección en voz alta",
|
||||
"Proofread Selection": "Corregir selección",
|
||||
"Open Settings": "Abrir ajustes",
|
||||
"Open Command Palette": "Abrir paleta de comandos",
|
||||
"Show Keyboard Shortcuts": "Mostrar atajos de teclado",
|
||||
"Open Books": "Abrir libros",
|
||||
"Toggle Fullscreen": "Alternar pantalla completa",
|
||||
"Close Window": "Cerrar ventana",
|
||||
"Quit App": "Salir de la aplicación",
|
||||
"Go Left / Previous Page": "Ir a la izquierda / Página anterior",
|
||||
"Go Right / Next Page": "Ir a la derecha / Página siguiente",
|
||||
"Go Up": "Ir arriba",
|
||||
"Go Down": "Ir abajo",
|
||||
"Previous Chapter": "Capítulo anterior",
|
||||
"Next Chapter": "Capítulo siguiente",
|
||||
"Scroll Half Page Down": "Desplazar media página abajo",
|
||||
"Scroll Half Page Up": "Desplazar media página arriba",
|
||||
"Save Note": "Guardar nota",
|
||||
"Single Section Scroll": "Desplazamiento por sección",
|
||||
"General": "General",
|
||||
"Text to Speech": "Texto a voz",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Ventana",
|
||||
"Your Bookshelf": "Tu estantería",
|
||||
"TTS": "Lectura en voz alta",
|
||||
"Media Info": "Info multimedia",
|
||||
"Update Frequency": "Frecuencia de actualización",
|
||||
"Every Sentence": "Cada oración",
|
||||
"Every Paragraph": "Cada párrafo",
|
||||
"Every Chapter": "Cada capítulo",
|
||||
"TTS Media Info Update Frequency": "Frecuencia de actualización de info multimedia TTS",
|
||||
"Select reading speed": "Seleccionar velocidad de lectura",
|
||||
"Drag to seek": "Arrastra para buscar",
|
||||
"Punctuation Delay": "Retraso de puntuación",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Confirme que esta conexión OPDS se enrutará a través de los servidores de Readest en la aplicación web antes de continuar.",
|
||||
"Custom Headers": "Encabezados personalizados",
|
||||
"Custom Headers (optional)": "Encabezados personalizados (opcional)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Añada un encabezado por línea usando \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Entiendo que esta conexión OPDS se enrutará a través de los servidores de Readest en la aplicación web. Si no confío en Readest con estas credenciales o encabezados, debería usar la aplicación nativa.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "No se puede conectar a Hardcover. Compruebe su conexión de red.",
|
||||
"Invalid Hardcover API token": "Token de API de Hardcover no válido",
|
||||
"Disconnected from Hardcover": "Desconectado de Hardcover",
|
||||
"Hardcover Settings": "Ajustes de Hardcover",
|
||||
"Connected to Hardcover": "Conectado a Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Conecte su cuenta de Hardcover para sincronizar el progreso de lectura y las notas.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Obtenga su token de API en hardcover.app → Ajustes → API.",
|
||||
"API Token": "Token de API",
|
||||
"Paste your Hardcover API token": "Pegue su token de API de Hardcover",
|
||||
"Hardcover sync enabled for this book": "Sincronización de Hardcover activada para este libro",
|
||||
"Hardcover sync disabled for this book": "Sincronización de Hardcover desactivada para este libro",
|
||||
"Hardcover Sync": "Sincronización de Hardcover",
|
||||
"Enable for This Book": "Activar para este libro",
|
||||
"Push Notes": "Enviar notas",
|
||||
"Enable Hardcover sync for this book first.": "Active primero la sincronización de Hardcover para este libro.",
|
||||
"No annotations or excerpts to sync for this book.": "No hay anotaciones ni extractos para sincronizar en este libro.",
|
||||
"Configure Hardcover in Settings first.": "Configure Hardcover en los ajustes primero.",
|
||||
"No new Hardcover note changes to sync.": "No hay nuevos cambios en notas de Hardcover para sincronizar.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover sincronizado: {{inserted}} nuevos, {{updated}} actualizados, {{skipped}} sin cambios",
|
||||
"Hardcover notes sync failed: {{error}}": "Error al sincronizar notas de Hardcover: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Progreso de lectura sincronizado con Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Error al sincronizar progreso de Hardcover: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Mantener el identificador de origen existente"
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@
|
||||
"Wikipedia": "ویکیپدیا",
|
||||
"Writing Mode": "حالت نوشتن",
|
||||
"Your Library": "کتابخانهی شما",
|
||||
"TTS not supported for PDF": "قابلیت تبدیل متن به گفتار برای PDF پشتیبانی نمیشود",
|
||||
"Override Book Font": "جایگزینی فونت کتاب",
|
||||
"Apply to All Books": "اِعمال برای همهی کتابها",
|
||||
"Apply to This Book": "اِعمال برای این کتاب",
|
||||
@@ -184,7 +183,6 @@
|
||||
"RTL Direction": "جهت راستبهچپ",
|
||||
"Maximum Column Height": "حداکثر ارتفاع ستون",
|
||||
"Maximum Column Width": "حداکثر عرض ستون",
|
||||
"Continuous Scroll": "پیمایش مداوم",
|
||||
"Fullscreen": "تمامصفحه",
|
||||
"No supported files found. Supported formats: {{formats}}": "فایل پشتیبانیشدهای یافت نشد. فرمتهای قابل پشتیبانی: {{formats}}",
|
||||
"Drop to Import Books": "برای وارد کردن کتابها، فایل را رها کنید",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "پاک کردن جستجو",
|
||||
"Clear search history": "پاک کردن تاریخچه جستجو",
|
||||
"Tap to Toggle Footer": "برای تغییر پاورقی ضربه بزنید",
|
||||
"Show Current Time": "نمایش زمان فعلی",
|
||||
"Use 24 Hour Clock": "استفاده از ساعت ۲۴ ساعته",
|
||||
"Show Current Battery Status": "نمایش وضعیت فعلی باتری",
|
||||
"Exported successfully": "صادرات موفق",
|
||||
"Book exported successfully.": "کتاب با موفقیت صادر شد.",
|
||||
"Failed to export the book.": "صادر کردن کتاب ناموفق بود.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "کلمات",
|
||||
"{{time}} left": "{{time}} باقیمانده",
|
||||
"Reading progress": "پیشرفت مطالعه",
|
||||
"Click to seek": "برای جستجو کلیک کنید",
|
||||
"Skip back 15 words": "۱۵ کلمه به عقب",
|
||||
"Back 15 words (Shift+Left)": "۱۵ کلمه به عقب (Shift+Left)",
|
||||
"Pause (Space)": "توقف (Space)",
|
||||
"Play (Space)": "پخش (Space)",
|
||||
"Skip forward 15 words": "۱۵ کلمه به جلو",
|
||||
"Forward 15 words (Shift+Right)": "۱۵ کلمه به جلو (Shift+Right)",
|
||||
"Pause:": "توقف:",
|
||||
"Decrease speed": "کاهش سرعت",
|
||||
"Slower (Left/Down)": "کندتر (Left/Down)",
|
||||
"Current speed": "سرعت فعلی",
|
||||
"Increase speed": "افزایش سرعت",
|
||||
"Faster (Right/Up)": "سریعتر (Right/Up)",
|
||||
"Start RSVP Reading": "شروع مطالعه RSVP",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "روشنایی صفحه نمایش سیستم",
|
||||
"Page:": "صفحه:",
|
||||
"Page: {{number}}": "صفحه: {{number}}",
|
||||
"Annotation page number": "شماره صفحه یادداشت"
|
||||
"Annotation page number": "شماره صفحه یادداشت",
|
||||
"Translating...": "در حال ترجمه...",
|
||||
"Show Battery Percentage": "نمایش درصد باتری",
|
||||
"Hide Scrollbar": "مخفی کردن نوار پیمایش",
|
||||
"Skip to last reading position": "رفتن به آخرین موقعیت مطالعه",
|
||||
"Show context": "نمایش زمینه",
|
||||
"Hide context": "پنهان کردن زمینه",
|
||||
"Decrease font size": "کاهش اندازه قلم",
|
||||
"Increase font size": "افزایش اندازه قلم",
|
||||
"Focus": "تمرکز",
|
||||
"Theme color": "رنگ پوسته",
|
||||
"Import failed": "وارد کردن ناموفق بود",
|
||||
"Available Formatters:": "قالببندهای موجود:",
|
||||
"Format date": "قالببندی تاریخ",
|
||||
"Markdown block quote (> per line)": "نقلقول Markdown (> در هر خط)",
|
||||
"Newlines to <br>": "خطوط جدید به <br>",
|
||||
"Change case": "تغییر بزرگی/کوچکی حروف",
|
||||
"Trim whitespace": "حذف فاصلهها",
|
||||
"Truncate to n characters": "کوتاه کردن به n نویسه",
|
||||
"Replace text": "جایگزینی متن",
|
||||
"Fallback value": "مقدار پیشفرض",
|
||||
"Get length": "دریافت طول",
|
||||
"First/last element": "عنصر اول/آخر",
|
||||
"Join array": "پیوستن آرایه",
|
||||
"Backup failed: {{error}}": "پشتیبانگیری ناموفق: {{error}}",
|
||||
"Select Backup": "انتخاب پشتیبان",
|
||||
"Restore failed: {{error}}": "بازیابی ناموفق: {{error}}",
|
||||
"Backup & Restore": "پشتیبانگیری و بازیابی",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "از کتابخانه خود پشتیبان بگیرید یا از پشتیبان قبلی بازیابی کنید. بازیابی با کتابخانه فعلی شما ادغام خواهد شد.",
|
||||
"Backup Library": "پشتیبانگیری کتابخانه",
|
||||
"Restore Library": "بازیابی کتابخانه",
|
||||
"Creating backup...": "در حال ایجاد پشتیبان...",
|
||||
"Restoring library...": "در حال بازیابی کتابخانه...",
|
||||
"{{current}} of {{total}} items": "{{current}} از {{total}} مورد",
|
||||
"Backup completed successfully!": "پشتیبانگیری با موفقیت انجام شد!",
|
||||
"Restore completed successfully!": "بازیابی با موفقیت انجام شد!",
|
||||
"Your library has been saved to the selected location.": "کتابخانه شما در مکان انتخابشده ذخیره شد.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} کتاب اضافه شد، {{updated}} کتاب بهروزرسانی شد.",
|
||||
"Operation failed": "عملیات ناموفق",
|
||||
"Loading library...": "در حال بارگذاری کتابخانه...",
|
||||
"{{count}} books refreshed_one": "{{count}} کتاب بهروزرسانی شد",
|
||||
"{{count}} books refreshed_other": "{{count}} کتاب بهروزرسانی شد",
|
||||
"Failed to refresh metadata": "بهروزرسانی فراداده ناموفق",
|
||||
"Refresh Metadata": "بهروزرسانی فراداده",
|
||||
"Keyboard Shortcuts": "میانبرهای صفحهکلید",
|
||||
"View all keyboard shortcuts": "مشاهده همه میانبرهای صفحهکلید",
|
||||
"Switch Sidebar Tab": "تغییر زبانه نوار کناری",
|
||||
"Toggle Notebook": "نمایش/پنهان کردن دفترچه یادداشت",
|
||||
"Search in Book": "جستجو در کتاب",
|
||||
"Toggle Scroll Mode": "تغییر حالت پیمایش",
|
||||
"Toggle Select Mode": "تغییر حالت انتخاب",
|
||||
"Toggle Bookmark": "تغییر نشانک",
|
||||
"Toggle Text to Speech": "تغییر متن به گفتار",
|
||||
"Play / Pause TTS": "پخش / توقف TTS",
|
||||
"Toggle Paragraph Mode": "تغییر حالت پاراگراف",
|
||||
"Highlight Selection": "برجستهسازی انتخاب",
|
||||
"Underline Selection": "زیرخط انتخاب",
|
||||
"Annotate Selection": "افزودن یادداشت به انتخاب",
|
||||
"Search Selection": "جستجوی انتخاب",
|
||||
"Copy Selection": "کپی انتخاب",
|
||||
"Translate Selection": "ترجمه انتخاب",
|
||||
"Dictionary Lookup": "جستجو در فرهنگ لغت",
|
||||
"Wikipedia Lookup": "جستجو در ویکیپدیا",
|
||||
"Read Aloud Selection": "خواندن بلند انتخاب",
|
||||
"Proofread Selection": "بازبینی انتخاب",
|
||||
"Open Settings": "باز کردن تنظیمات",
|
||||
"Open Command Palette": "باز کردن پالت فرمان",
|
||||
"Show Keyboard Shortcuts": "نمایش میانبرهای صفحهکلید",
|
||||
"Open Books": "باز کردن کتابها",
|
||||
"Toggle Fullscreen": "تغییر تمامصفحه",
|
||||
"Close Window": "بستن پنجره",
|
||||
"Quit App": "خروج از برنامه",
|
||||
"Go Left / Previous Page": "رفتن به چپ / صفحه قبلی",
|
||||
"Go Right / Next Page": "رفتن به راست / صفحه بعدی",
|
||||
"Go Up": "رفتن به بالا",
|
||||
"Go Down": "رفتن به پایین",
|
||||
"Previous Chapter": "فصل قبلی",
|
||||
"Next Chapter": "فصل بعدی",
|
||||
"Scroll Half Page Down": "پیمایش نیم صفحه پایین",
|
||||
"Scroll Half Page Up": "پیمایش نیم صفحه بالا",
|
||||
"Save Note": "ذخیره یادداشت",
|
||||
"Single Section Scroll": "پیمایش تکبخشی",
|
||||
"General": "عمومی",
|
||||
"Text to Speech": "تبدیل متن به گفتار",
|
||||
"Zoom": "بزرگنمایی",
|
||||
"Window": "پنجره",
|
||||
"Your Bookshelf": "قفسه کتاب شما",
|
||||
"TTS": "تبدیل متن به گفتار",
|
||||
"Media Info": "اطلاعات رسانه",
|
||||
"Update Frequency": "تناوب بهروزرسانی",
|
||||
"Every Sentence": "هر جمله",
|
||||
"Every Paragraph": "هر پاراگراف",
|
||||
"Every Chapter": "هر فصل",
|
||||
"TTS Media Info Update Frequency": "تناوب بهروزرسانی اطلاعات رسانه TTS",
|
||||
"Select reading speed": "انتخاب سرعت خواندن",
|
||||
"Drag to seek": "بکشید برای جستجو",
|
||||
"Punctuation Delay": "تأخیر نشانهگذاری",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "لطفاً تأیید کنید که این اتصال OPDS از طریق سرورهای Readest در برنامه وب پروکسی خواهد شد.",
|
||||
"Custom Headers": "سرآیندهای سفارشی",
|
||||
"Custom Headers (optional)": "سرآیندهای سفارشی (اختیاری)",
|
||||
"Add one header per line using \"Header-Name: value\".": "در هر خط یک سرآیند با استفاده از \"Header-Name: value\" اضافه کنید.",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "متوجه هستم که این اتصال OPDS از طریق سرورهای Readest در برنامه وب پروکسی خواهد شد. اگر به Readest برای این اعتبارنامهها یا سرآیندها اعتماد ندارم، باید از برنامه بومی استفاده کنم.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "اتصال به Hardcover ممکن نیست. لطفاً اتصال شبکه خود را بررسی کنید.",
|
||||
"Invalid Hardcover API token": "توکن API نامعتبر Hardcover",
|
||||
"Disconnected from Hardcover": "قطع اتصال از Hardcover",
|
||||
"Hardcover Settings": "تنظیمات Hardcover",
|
||||
"Connected to Hardcover": "متصل به Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "حساب Hardcover خود را برای همگامسازی پیشرفت مطالعه و یادداشتها متصل کنید.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "توکن API خود را از hardcover.app ← تنظیمات ← API دریافت کنید.",
|
||||
"API Token": "توکن API",
|
||||
"Paste your Hardcover API token": "توکن API Hardcover خود را جایگذاری کنید",
|
||||
"Hardcover sync enabled for this book": "همگامسازی Hardcover برای این کتاب فعال شد",
|
||||
"Hardcover sync disabled for this book": "همگامسازی Hardcover برای این کتاب غیرفعال شد",
|
||||
"Hardcover Sync": "همگامسازی Hardcover",
|
||||
"Enable for This Book": "فعالسازی برای این کتاب",
|
||||
"Push Notes": "ارسال یادداشتها",
|
||||
"Enable Hardcover sync for this book first.": "ابتدا همگامسازی Hardcover را برای این کتاب فعال کنید.",
|
||||
"No annotations or excerpts to sync for this book.": "هیچ حاشیهنویسی یا گزیدهای برای همگامسازی این کتاب وجود ندارد.",
|
||||
"Configure Hardcover in Settings first.": "ابتدا Hardcover را در تنظیمات پیکربندی کنید.",
|
||||
"No new Hardcover note changes to sync.": "تغییرات جدیدی در یادداشتهای Hardcover برای همگامسازی وجود ندارد.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover همگامسازی شد: {{inserted}} جدید، {{updated}} بهروزرسانی، {{skipped}} بدون تغییر",
|
||||
"Hardcover notes sync failed: {{error}}": "همگامسازی یادداشتهای Hardcover ناموفق: {{error}}",
|
||||
"Reading progress synced to Hardcover": "پیشرفت مطالعه با Hardcover همگامسازی شد",
|
||||
"Hardcover progress sync failed: {{error}}": "همگامسازی پیشرفت Hardcover ناموفق: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "حفظ شناسه منبع موجود"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Wikipédia",
|
||||
"Writing Mode": "Mode écriture",
|
||||
"Your Library": "Votre bibliothèque",
|
||||
"TTS not supported for PDF": "La synthèse vocale n'est pas prise en charge pour les fichiers PDF",
|
||||
"Override Book Font": "Remplacer la police du livre",
|
||||
"Apply to All Books": "Appliquer à tous les livres",
|
||||
"Apply to This Book": "Appliquer à ce livre",
|
||||
@@ -184,7 +183,6 @@
|
||||
"RTL Direction": "Direction RTL",
|
||||
"Maximum Column Height": "Hauteur maximale de colonne",
|
||||
"Maximum Column Width": "Largeur maximale de colonne",
|
||||
"Continuous Scroll": "Défilement continu",
|
||||
"Fullscreen": "Plein écran",
|
||||
"No supported files found. Supported formats: {{formats}}": "Aucun fichier pris en charge trouvé. Formats pris en charge : {{formats}}",
|
||||
"Drop to Import Books": "Déposez pour importer des livres",
|
||||
@@ -842,6 +840,9 @@
|
||||
"Clear search": "Effacer la recherche",
|
||||
"Clear search history": "Effacer l'historique de recherche",
|
||||
"Tap to Toggle Footer": "Appuyez pour afficher/masquer le pied de page",
|
||||
"Show Current Time": "Afficher l'heure actuelle",
|
||||
"Use 24 Hour Clock": "Utiliser le format 24 heures",
|
||||
"Show Current Battery Status": "Afficher l'état actuel de la batterie",
|
||||
"Exported successfully": "Exporté avec succès",
|
||||
"Book exported successfully.": "Livre exporté avec succès.",
|
||||
"Failed to export the book.": "Échec de l'exportation du livre.",
|
||||
@@ -993,17 +994,14 @@
|
||||
"words": "mots",
|
||||
"{{time}} left": "{{time}} restant",
|
||||
"Reading progress": "Progression de la lecture",
|
||||
"Click to seek": "Cliquer pour chercher",
|
||||
"Skip back 15 words": "Reculer de 15 mots",
|
||||
"Back 15 words (Shift+Left)": "Reculer de 15 mots (Shift+Left)",
|
||||
"Pause (Space)": "Pause (Espace)",
|
||||
"Play (Space)": "Lecture (Espace)",
|
||||
"Skip forward 15 words": "Avancer de 15 mots",
|
||||
"Forward 15 words (Shift+Right)": "Avancer de 15 mots (Shift+Right)",
|
||||
"Pause:": "Pause :",
|
||||
"Decrease speed": "Diminuer la vitesse",
|
||||
"Slower (Left/Down)": "Plus lent (Gauche/Bas)",
|
||||
"Current speed": "Vitesse actuelle",
|
||||
"Increase speed": "Augmenter la vitesse",
|
||||
"Faster (Right/Up)": "Plus rapide (Droite/Haut)",
|
||||
"Start RSVP Reading": "Démarrer la lecture RSVP",
|
||||
@@ -1071,5 +1069,131 @@
|
||||
"System Screen Brightness": "Luminosité de l'écran système",
|
||||
"Page:": "Page :",
|
||||
"Page: {{number}}": "Page : {{number}}",
|
||||
"Annotation page number": "Numéro de page de l'annotation"
|
||||
"Annotation page number": "Numéro de page de l'annotation",
|
||||
"Translating...": "Traduction en cours...",
|
||||
"Show Battery Percentage": "Afficher le pourcentage de batterie",
|
||||
"Hide Scrollbar": "Masquer la barre de défilement",
|
||||
"Skip to last reading position": "Aller à la dernière position de lecture",
|
||||
"Show context": "Afficher le contexte",
|
||||
"Hide context": "Masquer le contexte",
|
||||
"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",
|
||||
"Import failed": "Échec de l'importation",
|
||||
"Available Formatters:": "Formateurs disponibles :",
|
||||
"Format date": "Formater la date",
|
||||
"Markdown block quote (> per line)": "Citation Markdown (> par ligne)",
|
||||
"Newlines to <br>": "Retours à la ligne en <br>",
|
||||
"Change case": "Changer la casse",
|
||||
"Trim whitespace": "Supprimer les espaces",
|
||||
"Truncate to n characters": "Tronquer à n caractères",
|
||||
"Replace text": "Remplacer le texte",
|
||||
"Fallback value": "Valeur par défaut",
|
||||
"Get length": "Obtenir la longueur",
|
||||
"First/last element": "Premier/dernier élément",
|
||||
"Join array": "Joindre le tableau",
|
||||
"Backup failed: {{error}}": "Échec de la sauvegarde : {{error}}",
|
||||
"Select Backup": "Sélectionner une sauvegarde",
|
||||
"Restore failed: {{error}}": "Échec de la restauration : {{error}}",
|
||||
"Backup & Restore": "Sauvegarde et restauration",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Créez une sauvegarde de votre bibliothèque ou restaurez à partir d'une sauvegarde précédente. La restauration fusionnera avec votre bibliothèque actuelle.",
|
||||
"Backup Library": "Sauvegarder la bibliothèque",
|
||||
"Restore Library": "Restaurer la bibliothèque",
|
||||
"Creating backup...": "Création de la sauvegarde...",
|
||||
"Restoring library...": "Restauration de la bibliothèque...",
|
||||
"{{current}} of {{total}} items": "{{current}} sur {{total}} éléments",
|
||||
"Backup completed successfully!": "Sauvegarde terminée avec succès !",
|
||||
"Restore completed successfully!": "Restauration terminée avec succès !",
|
||||
"Your library has been saved to the selected location.": "Votre bibliothèque a été enregistrée à l'emplacement sélectionné.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} livres ajoutés, {{updated}} livres mis à jour.",
|
||||
"Operation failed": "L'opération a échoué",
|
||||
"Loading library...": "Chargement de la bibliothèque...",
|
||||
"{{count}} books refreshed_one": "{{count}} livre rafraîchi",
|
||||
"{{count}} books refreshed_many": "{{count}} livres rafraîchis",
|
||||
"{{count}} books refreshed_other": "{{count}} livres rafraîchis",
|
||||
"Failed to refresh metadata": "Échec du rafraîchissement des métadonnées",
|
||||
"Refresh Metadata": "Rafraîchir les métadonnées",
|
||||
"Keyboard Shortcuts": "Raccourcis clavier",
|
||||
"View all keyboard shortcuts": "Voir tous les raccourcis clavier",
|
||||
"Switch Sidebar Tab": "Changer l'onglet de la barre latérale",
|
||||
"Toggle Notebook": "Afficher/masquer le carnet",
|
||||
"Search in Book": "Rechercher dans le livre",
|
||||
"Toggle Scroll Mode": "Basculer le mode défilement",
|
||||
"Toggle Select Mode": "Basculer le mode sélection",
|
||||
"Toggle Bookmark": "Basculer le signet",
|
||||
"Toggle Text to Speech": "Basculer la synthèse vocale",
|
||||
"Play / Pause TTS": "Lecture / Pause TTS",
|
||||
"Toggle Paragraph Mode": "Basculer le mode paragraphe",
|
||||
"Highlight Selection": "Surligner la sélection",
|
||||
"Underline Selection": "Souligner la sélection",
|
||||
"Annotate Selection": "Annoter la sélection",
|
||||
"Search Selection": "Rechercher la sélection",
|
||||
"Copy Selection": "Copier la sélection",
|
||||
"Translate Selection": "Traduire la sélection",
|
||||
"Dictionary Lookup": "Recherche dans le dictionnaire",
|
||||
"Wikipedia Lookup": "Recherche sur Wikipédia",
|
||||
"Read Aloud Selection": "Lire la sélection à voix haute",
|
||||
"Proofread Selection": "Relire la sélection",
|
||||
"Open Settings": "Ouvrir les paramètres",
|
||||
"Open Command Palette": "Ouvrir la palette de commandes",
|
||||
"Show Keyboard Shortcuts": "Afficher les raccourcis clavier",
|
||||
"Open Books": "Ouvrir les livres",
|
||||
"Toggle Fullscreen": "Basculer en plein écran",
|
||||
"Close Window": "Fermer la fenêtre",
|
||||
"Quit App": "Quitter l'application",
|
||||
"Go Left / Previous Page": "Aller à gauche / Page précédente",
|
||||
"Go Right / Next Page": "Aller à droite / Page suivante",
|
||||
"Go Up": "Aller en haut",
|
||||
"Go Down": "Aller en bas",
|
||||
"Previous Chapter": "Chapitre précédent",
|
||||
"Next Chapter": "Chapitre suivant",
|
||||
"Scroll Half Page Down": "Défiler d'une demi-page vers le bas",
|
||||
"Scroll Half Page Up": "Défiler d'une demi-page vers le haut",
|
||||
"Save Note": "Enregistrer la note",
|
||||
"Single Section Scroll": "Défilement par section",
|
||||
"General": "Général",
|
||||
"Text to Speech": "Synthèse vocale",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Fenêtre",
|
||||
"Your Bookshelf": "Votre bibliothèque",
|
||||
"TTS": "Synthèse vocale",
|
||||
"Media Info": "Infos média",
|
||||
"Update Frequency": "Fréquence de mise à jour",
|
||||
"Every Sentence": "Chaque phrase",
|
||||
"Every Paragraph": "Chaque paragraphe",
|
||||
"Every Chapter": "Chaque chapitre",
|
||||
"TTS Media Info Update Frequency": "Fréquence de mise à jour des infos média TTS",
|
||||
"Select reading speed": "Sélectionner la vitesse de lecture",
|
||||
"Drag to seek": "Glisser pour chercher",
|
||||
"Punctuation Delay": "Délai de ponctuation",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Veuillez confirmer que cette connexion OPDS sera acheminée via les serveurs Readest sur l'application web avant de continuer.",
|
||||
"Custom Headers": "En-têtes personnalisés",
|
||||
"Custom Headers (optional)": "En-têtes personnalisés (facultatif)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Ajoutez un en-tête par ligne en utilisant \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Je comprends que cette connexion OPDS sera acheminée via les serveurs Readest sur l'application web. Si je ne fais pas confiance à Readest pour ces identifiants ou en-têtes, je devrais utiliser l'application native.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Impossible de se connecter à Hardcover. Veuillez vérifier votre connexion réseau.",
|
||||
"Invalid Hardcover API token": "Jeton API Hardcover invalide",
|
||||
"Disconnected from Hardcover": "Déconnecté de Hardcover",
|
||||
"Hardcover Settings": "Paramètres Hardcover",
|
||||
"Connected to Hardcover": "Connecté à Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Connectez votre compte Hardcover pour synchroniser la progression de lecture et les notes.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Obtenez votre jeton API depuis hardcover.app → Paramètres → API.",
|
||||
"API Token": "Jeton API",
|
||||
"Paste your Hardcover API token": "Collez votre jeton API Hardcover",
|
||||
"Hardcover sync enabled for this book": "Synchronisation Hardcover activée pour ce livre",
|
||||
"Hardcover sync disabled for this book": "Synchronisation Hardcover désactivée pour ce livre",
|
||||
"Hardcover Sync": "Synchronisation Hardcover",
|
||||
"Enable for This Book": "Activer pour ce livre",
|
||||
"Push Notes": "Envoyer les notes",
|
||||
"Enable Hardcover sync for this book first.": "Activez d'abord la synchronisation Hardcover pour ce livre.",
|
||||
"No annotations or excerpts to sync for this book.": "Aucune annotation ni extrait à synchroniser pour ce livre.",
|
||||
"Configure Hardcover in Settings first.": "Configurez d'abord Hardcover dans les paramètres.",
|
||||
"No new Hardcover note changes to sync.": "Aucun nouveau changement de notes Hardcover à synchroniser.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover synchronisé : {{inserted}} nouveaux, {{updated}} mis à jour, {{skipped}} inchangés",
|
||||
"Hardcover notes sync failed: {{error}}": "Échec de la synchronisation des notes Hardcover : {{error}}",
|
||||
"Reading progress synced to Hardcover": "Progression de lecture synchronisée avec Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Échec de la synchronisation de la progression Hardcover : {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Conserver l'identifiant source existant"
|
||||
}
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"words": "מילים",
|
||||
"{{time}} left": "נותרו {{time}}",
|
||||
"Reading progress": "התקדמות קריאה",
|
||||
"Click to seek": "לחץ לחיפוש",
|
||||
"Skip back 15 words": "קפוץ 15 מילים אחורה",
|
||||
"Back 15 words (Shift+Left)": "15 מילים אחורה (Shift+Left)",
|
||||
"Pause": "השהה",
|
||||
@@ -502,10 +501,8 @@
|
||||
"Play (Space)": "נגן (רווח)",
|
||||
"Skip forward 15 words": "קפוץ 15 מילים קדימה",
|
||||
"Forward 15 words (Shift+Right)": "15 מילים קדימה (Shift+Right)",
|
||||
"Pause:": "השהה:",
|
||||
"Decrease speed": "הפחת מהירות",
|
||||
"Slower (Left/Down)": "איטי יותר (חץ שמאלה/למטה)",
|
||||
"Current speed": "מהירות נוכחית",
|
||||
"Increase speed": "הגבר מהירות",
|
||||
"Faster (Right/Up)": "מהיר יותר (חץ ימינה/למעלה)",
|
||||
"Start RSVP Reading": "התחל קריאת RSVP",
|
||||
@@ -570,7 +567,6 @@
|
||||
"Read Aloud": "הקרא בקול",
|
||||
"Ready to read aloud": "מוכן להקראה בקול",
|
||||
"Please log in to use advanced TTS features": "אנא התחבר כדי להשתמש בתכונות TTS מתקדמות",
|
||||
"TTS not supported for PDF": "TTS אינו נתמך עבור PDF",
|
||||
"TTS not supported for this document": "TTS אינו נתמך עבור מסמך זה",
|
||||
"Back to TTS Location": "חזור למיקום ה-TTS",
|
||||
"No Timeout": "ללא הגבלת זמן",
|
||||
@@ -856,7 +852,6 @@
|
||||
"Override Book Color": "דרוס צבע ספר",
|
||||
"None": "ללא",
|
||||
"Scroll": "גלילה",
|
||||
"Continuous Scroll": "גלילה רציפה",
|
||||
"Overlap Pixels": "פיקסלים לחפיפה",
|
||||
"Pagination": "עימוד (Pagination)",
|
||||
"Tap to Paginate": "הקש לעימוד",
|
||||
@@ -966,6 +961,9 @@
|
||||
"Page Number": "מספר דף",
|
||||
"Percentage": "אחוזים",
|
||||
"Tap to Toggle Footer": "הקש להצגת/הסתרת כותרת תחתונה",
|
||||
"Show Current Time": "הצגת השעה הנוכחית",
|
||||
"Use 24 Hour Clock": "השתמש בשעון 24 שעות",
|
||||
"Show Current Battery Status": "הצג מצב סוללה נוכחי",
|
||||
"Screen": "מסך",
|
||||
"Orientation": "כיוון מסך (Orientation)",
|
||||
"Portrait": "לאורך (Portrait)",
|
||||
@@ -1071,5 +1069,131 @@
|
||||
"System Screen Brightness": "בהירות מסך המערכת",
|
||||
"Page:": "עמוד:",
|
||||
"Page: {{number}}": "עמוד: {{number}}",
|
||||
"Annotation page number": "מספר עמוד הערה"
|
||||
"Annotation page number": "מספר עמוד הערה",
|
||||
"Translating...": "מתרגם...",
|
||||
"Show Battery Percentage": "הצג אחוזי סוללה",
|
||||
"Hide Scrollbar": "הסתר סרגל גלילה",
|
||||
"Skip to last reading position": "דלג למיקום הקריאה האחרון",
|
||||
"Show context": "הצג הקשר",
|
||||
"Hide context": "הסתר הקשר",
|
||||
"Decrease font size": "הקטן גודל גופן",
|
||||
"Increase font size": "הגדל גודל גופן",
|
||||
"Focus": "מיקוד",
|
||||
"Theme color": "צבע ערכת נושא",
|
||||
"Import failed": "הייבוא נכשל",
|
||||
"Available Formatters:": "מעצבים זמינים:",
|
||||
"Format date": "עיצוב תאריך",
|
||||
"Markdown block quote (> per line)": "ציטוט Markdown (> בכל שורה)",
|
||||
"Newlines to <br>": "שורות חדשות ל-<br>",
|
||||
"Change case": "שינוי רישיות",
|
||||
"Trim whitespace": "חיתוך רווחים",
|
||||
"Truncate to n characters": "קיצוץ ל-n תווים",
|
||||
"Replace text": "החלפת טקסט",
|
||||
"Fallback value": "ערך חלופי",
|
||||
"Get length": "קבלת אורך",
|
||||
"First/last element": "אלמנט ראשון/אחרון",
|
||||
"Join array": "חיבור מערך",
|
||||
"Backup failed: {{error}}": "הגיבוי נכשל: {{error}}",
|
||||
"Select Backup": "בחר גיבוי",
|
||||
"Restore failed: {{error}}": "השחזור נכשל: {{error}}",
|
||||
"Backup & Restore": "גיבוי ושחזור",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "צור גיבוי של הספרייה שלך או שחזר מגיבוי קודם. השחזור ימוזג עם הספרייה הנוכחית שלך.",
|
||||
"Backup Library": "גיבוי ספרייה",
|
||||
"Restore Library": "שחזור ספרייה",
|
||||
"Creating backup...": "יוצר גיבוי...",
|
||||
"Restoring library...": "משחזר ספרייה...",
|
||||
"{{current}} of {{total}} items": "{{current}} מתוך {{total}} פריטים",
|
||||
"Backup completed successfully!": "הגיבוי הושלם בהצלחה!",
|
||||
"Restore completed successfully!": "השחזור הושלם בהצלחה!",
|
||||
"Your library has been saved to the selected location.": "הספרייה שלך נשמרה במיקום שנבחר.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} ספרים נוספו, {{updated}} ספרים עודכנו.",
|
||||
"Operation failed": "הפעולה נכשלה",
|
||||
"Loading library...": "טוען ספרייה...",
|
||||
"{{count}} books refreshed_one": "ספר {{count}} רוענן",
|
||||
"{{count}} books refreshed_two": "{{count}} ספרים רועננו",
|
||||
"{{count}} books refreshed_other": "{{count}} ספרים רועננו",
|
||||
"Failed to refresh metadata": "רענון המטא-נתונים נכשל",
|
||||
"Refresh Metadata": "רענון מטא-נתונים",
|
||||
"Keyboard Shortcuts": "קיצורי מקלדת",
|
||||
"View all keyboard shortcuts": "הצג את כל קיצורי המקלדת",
|
||||
"Switch Sidebar Tab": "החלף לשונית סרגל צד",
|
||||
"Toggle Notebook": "הצג/הסתר מחברת",
|
||||
"Search in Book": "חפש בספר",
|
||||
"Toggle Scroll Mode": "החלף מצב גלילה",
|
||||
"Toggle Select Mode": "החלף מצב בחירה",
|
||||
"Toggle Bookmark": "החלף סימנייה",
|
||||
"Toggle Text to Speech": "החלף טקסט לדיבור",
|
||||
"Play / Pause TTS": "נגן / השהה TTS",
|
||||
"Toggle Paragraph Mode": "החלף מצב פסקה",
|
||||
"Highlight Selection": "הדגש בחירה",
|
||||
"Underline Selection": "קו תחתון לבחירה",
|
||||
"Annotate Selection": "הוסף הערה לבחירה",
|
||||
"Search Selection": "חפש בחירה",
|
||||
"Copy Selection": "העתק בחירה",
|
||||
"Translate Selection": "תרגם בחירה",
|
||||
"Dictionary Lookup": "חיפוש במילון",
|
||||
"Wikipedia Lookup": "חיפוש בוויקיפדיה",
|
||||
"Read Aloud Selection": "קרא בקול את הבחירה",
|
||||
"Proofread Selection": "הגה בחירה",
|
||||
"Open Settings": "פתח הגדרות",
|
||||
"Open Command Palette": "פתח לוח פקודות",
|
||||
"Show Keyboard Shortcuts": "הצג קיצורי מקלדת",
|
||||
"Open Books": "פתח ספרים",
|
||||
"Toggle Fullscreen": "החלף מסך מלא",
|
||||
"Close Window": "סגור חלון",
|
||||
"Quit App": "צא מהאפליקציה",
|
||||
"Go Left / Previous Page": "שמאלה / עמוד קודם",
|
||||
"Go Right / Next Page": "ימינה / עמוד הבא",
|
||||
"Go Up": "למעלה",
|
||||
"Go Down": "למטה",
|
||||
"Previous Chapter": "פרק קודם",
|
||||
"Next Chapter": "פרק הבא",
|
||||
"Scroll Half Page Down": "גלול חצי עמוד למטה",
|
||||
"Scroll Half Page Up": "גלול חצי עמוד למעלה",
|
||||
"Save Note": "שמור הערה",
|
||||
"Single Section Scroll": "גלילת קטע בודד",
|
||||
"General": "כללי",
|
||||
"Text to Speech": "טקסט לדיבור",
|
||||
"Zoom": "זום",
|
||||
"Window": "חלון",
|
||||
"Your Bookshelf": "מדף הספרים שלך",
|
||||
"TTS": "הקראה",
|
||||
"Media Info": "פרטי מדיה",
|
||||
"Update Frequency": "תדירות עדכון",
|
||||
"Every Sentence": "כל משפט",
|
||||
"Every Paragraph": "כל פסקה",
|
||||
"Every Chapter": "כל פרק",
|
||||
"TTS Media Info Update Frequency": "תדירות עדכון פרטי מדיה TTS",
|
||||
"Select reading speed": "בחר מהירות קריאה",
|
||||
"Drag to seek": "גרור לחיפוש",
|
||||
"Punctuation Delay": "השהיית פיסוק",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "אנא אשר שחיבור OPDS זה יועבר דרך שרתי Readest באפליקציית האינטרנט לפני שתמשיך.",
|
||||
"Custom Headers": "כותרות מותאמות אישית",
|
||||
"Custom Headers (optional)": "כותרות מותאמות אישית (אופציונלי)",
|
||||
"Add one header per line using \"Header-Name: value\".": "הוסף כותרת אחת לכל שורה בפורמט \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "אני מבין שחיבור OPDS זה יועבר דרך שרתי Readest באפליקציית האינטרנט. אם אינני סומך על Readest עם פרטי ההתחברות או הכותרות האלה, עליי להשתמש באפליקציה המקורית.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "לא ניתן להתחבר ל-Hardcover. אנא בדוק את חיבור הרשת שלך.",
|
||||
"Invalid Hardcover API token": "טוקן API של Hardcover לא חוקי",
|
||||
"Disconnected from Hardcover": "מנותק מ-Hardcover",
|
||||
"Hardcover Settings": "הגדרות Hardcover",
|
||||
"Connected to Hardcover": "מחובר ל-Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "חבר את חשבון ה-Hardcover שלך לסנכרון התקדמות קריאה והערות.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "קבל את טוקן ה-API שלך מ-hardcover.app ← הגדרות ← API.",
|
||||
"API Token": "טוקן API",
|
||||
"Paste your Hardcover API token": "הדבק את טוקן ה-API של Hardcover שלך",
|
||||
"Hardcover sync enabled for this book": "סנכרון Hardcover הופעל לספר זה",
|
||||
"Hardcover sync disabled for this book": "סנכרון Hardcover הושבת לספר זה",
|
||||
"Hardcover Sync": "סנכרון Hardcover",
|
||||
"Enable for This Book": "הפעל לספר זה",
|
||||
"Push Notes": "שלח הערות",
|
||||
"Enable Hardcover sync for this book first.": "הפעל תחילה סנכרון Hardcover לספר זה.",
|
||||
"No annotations or excerpts to sync for this book.": "אין הערות או ציטוטים לסנכרון עבור ספר זה.",
|
||||
"Configure Hardcover in Settings first.": "הגדר תחילה את Hardcover בהגדרות.",
|
||||
"No new Hardcover note changes to sync.": "אין שינויים חדשים בהערות Hardcover לסנכרון.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover סונכרן: {{inserted}} חדשים, {{updated}} עודכנו, {{skipped}} ללא שינוי",
|
||||
"Hardcover notes sync failed: {{error}}": "סנכרון הערות Hardcover נכשל: {{error}}",
|
||||
"Reading progress synced to Hardcover": "התקדמות הקריאה סונכרנה ל-Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "סנכרון התקדמות Hardcover נכשל: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "שמור על מזהה המקור הקיים"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "विकिपीडिया",
|
||||
"Writing Mode": "लेखन मोड",
|
||||
"Your Library": "आपकी लाइब्रेरी",
|
||||
"TTS not supported for PDF": "PDF के लिए TTS समर्थित नहीं है",
|
||||
"Override Book Font": "पुस्तक फ़ॉन्ट ओवरराइड करें",
|
||||
"Apply to All Books": "सभी पुस्तकों पर लागू करें",
|
||||
"Apply to This Book": "इस पुस्तक पर लागू करें",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "RTL दिशा",
|
||||
"Maximum Column Height": "कॉलम की अधिकतम ऊँचाई",
|
||||
"Maximum Column Width": "कॉलम की अधिकतम चौड़ाई",
|
||||
"Continuous Scroll": "निरंतर स्क्रॉल",
|
||||
"Fullscreen": "पूर्णस्क्रीन",
|
||||
"No supported files found. Supported formats: {{formats}}": "कोई समर्थित फ़ाइलें नहीं मिलीं। समर्थित प्रारूप: {{formats}}",
|
||||
"Drop to Import Books": "पुस्तकें आयात करने के लिए छोड़ें",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "खोज साफ़ करें",
|
||||
"Clear search history": "खोज इतिहास साफ़ करें",
|
||||
"Tap to Toggle Footer": "फुटर टॉगल करने के लिए टैप करें",
|
||||
"Show Current Time": "वर्तमान समय दिखाएं",
|
||||
"Use 24 Hour Clock": "24 घंटे की घड़ी का उपयोग करें",
|
||||
"Show Current Battery Status": "वर्तमान बैटरी स्थिति दिखाएं",
|
||||
"Exported successfully": "सफलतापूर्वक निर्यात किया गया",
|
||||
"Book exported successfully.": "पुस्तक सफलतापूर्वक निर्यात की गई।",
|
||||
"Failed to export the book.": "पुस्तक निर्यात करने में विफल।",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "शब्द",
|
||||
"{{time}} left": "{{time}} शेष",
|
||||
"Reading progress": "पठन प्रगति",
|
||||
"Click to seek": "सीक करने के लिए क्लिक करें",
|
||||
"Skip back 15 words": "15 शब्द पीछे",
|
||||
"Back 15 words (Shift+Left)": "15 शब्द पीछे (Shift+Left)",
|
||||
"Pause (Space)": "विराम (Space)",
|
||||
"Play (Space)": "चलाएं (Space)",
|
||||
"Skip forward 15 words": "15 शब्द आगे",
|
||||
"Forward 15 words (Shift+Right)": "15 शब्द आगे (Shift+Right)",
|
||||
"Pause:": "विराम:",
|
||||
"Decrease speed": "गति कम करें",
|
||||
"Slower (Left/Down)": "धीमी (Left/Down)",
|
||||
"Current speed": "वर्तमान गति",
|
||||
"Increase speed": "गति बढ़ाएं",
|
||||
"Faster (Right/Up)": "तेज़ (Right/Up)",
|
||||
"Start RSVP Reading": "RSVP पठन शुरू करें",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "सिस्टम स्क्रीन चमक",
|
||||
"Page:": "पृष्ठ:",
|
||||
"Page: {{number}}": "पृष्ठ: {{number}}",
|
||||
"Annotation page number": "व्याख्या पृष्ठ संख्या"
|
||||
"Annotation page number": "व्याख्या पृष्ठ संख्या",
|
||||
"Translating...": "अनुवाद हो रहा है...",
|
||||
"Show Battery Percentage": "बैटरी प्रतिशत दिखाएं",
|
||||
"Hide Scrollbar": "स्क्रॉलबार छुपाएँ",
|
||||
"Skip to last reading position": "अंतिम पढ़ने की स्थिति पर जाएँ",
|
||||
"Show context": "संदर्भ दिखाएँ",
|
||||
"Hide context": "संदर्भ छुपाएँ",
|
||||
"Decrease font size": "फ़ॉन्ट आकार घटाएँ",
|
||||
"Increase font size": "फ़ॉन्ट आकार बढ़ाएँ",
|
||||
"Focus": "फ़ोकस",
|
||||
"Theme color": "थीम रंग",
|
||||
"Import failed": "आयात विफल",
|
||||
"Available Formatters:": "उपलब्ध फ़ॉर्मेटर:",
|
||||
"Format date": "तारीख फ़ॉर्मेट करें",
|
||||
"Markdown block quote (> per line)": "Markdown ब्लॉक कोट (> प्रति पंक्ति)",
|
||||
"Newlines to <br>": "नई पंक्तियाँ <br> में",
|
||||
"Change case": "केस बदलें",
|
||||
"Trim whitespace": "रिक्त स्थान हटाएँ",
|
||||
"Truncate to n characters": "n अक्षरों तक छोटा करें",
|
||||
"Replace text": "टेक्स्ट बदलें",
|
||||
"Fallback value": "फ़ॉलबैक मान",
|
||||
"Get length": "लंबाई प्राप्त करें",
|
||||
"First/last element": "पहला/अंतिम तत्व",
|
||||
"Join array": "ऐरे जोड़ें",
|
||||
"Backup failed: {{error}}": "बैकअप विफल: {{error}}",
|
||||
"Select Backup": "बैकअप चुनें",
|
||||
"Restore failed: {{error}}": "पुनर्स्थापना विफल: {{error}}",
|
||||
"Backup & Restore": "बैकअप और पुनर्स्थापना",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "अपनी लाइब्रेरी का बैकअप बनाएं या पिछले बैकअप से पुनर्स्थापित करें। पुनर्स्थापना आपकी वर्तमान लाइब्रेरी के साथ मर्ज होगी।",
|
||||
"Backup Library": "लाइब्रेरी बैकअप",
|
||||
"Restore Library": "लाइब्रेरी पुनर्स्थापित करें",
|
||||
"Creating backup...": "बैकअप बनाया जा रहा है...",
|
||||
"Restoring library...": "लाइब्रेरी पुनर्स्थापित हो रही है...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} आइटम",
|
||||
"Backup completed successfully!": "बैकअप सफलतापूर्वक पूर्ण!",
|
||||
"Restore completed successfully!": "पुनर्स्थापना सफलतापूर्वक पूर्ण!",
|
||||
"Your library has been saved to the selected location.": "आपकी लाइब्रेरी चयनित स्थान पर सहेजी गई है।",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} पुस्तकें जोड़ी गईं, {{updated}} पुस्तकें अपडेट की गईं।",
|
||||
"Operation failed": "ऑपरेशन विफल",
|
||||
"Loading library...": "लाइब्रेरी लोड हो रही है...",
|
||||
"{{count}} books refreshed_one": "{{count}} पुस्तक रिफ्रेश हुई",
|
||||
"{{count}} books refreshed_other": "{{count}} पुस्तकें रिफ्रेश हुईं",
|
||||
"Failed to refresh metadata": "मेटाडेटा रिफ्रेश विफल",
|
||||
"Refresh Metadata": "मेटाडेटा रिफ्रेश करें",
|
||||
"Keyboard Shortcuts": "कीबोर्ड शॉर्टकट",
|
||||
"View all keyboard shortcuts": "सभी कीबोर्ड शॉर्टकट देखें",
|
||||
"Switch Sidebar Tab": "साइडबार टैब बदलें",
|
||||
"Toggle Notebook": "नोटबुक दिखाएँ/छुपाएँ",
|
||||
"Search in Book": "पुस्तक में खोजें",
|
||||
"Toggle Scroll Mode": "स्क्रॉल मोड टॉगल करें",
|
||||
"Toggle Select Mode": "चयन मोड टॉगल करें",
|
||||
"Toggle Bookmark": "बुकमार्क टॉगल करें",
|
||||
"Toggle Text to Speech": "टेक्स्ट टू स्पीच टॉगल करें",
|
||||
"Play / Pause TTS": "TTS चलाएँ / रोकें",
|
||||
"Toggle Paragraph Mode": "अनुच्छेद मोड टॉगल करें",
|
||||
"Highlight Selection": "चयन हाइलाइट करें",
|
||||
"Underline Selection": "चयन अंडरलाइन करें",
|
||||
"Annotate Selection": "चयन पर टिप्पणी करें",
|
||||
"Search Selection": "चयन खोजें",
|
||||
"Copy Selection": "चयन कॉपी करें",
|
||||
"Translate Selection": "चयन का अनुवाद करें",
|
||||
"Dictionary Lookup": "शब्दकोश में खोजें",
|
||||
"Wikipedia Lookup": "विकिपीडिया पर खोजें",
|
||||
"Read Aloud Selection": "चयन को ज़ोर से पढ़ें",
|
||||
"Proofread Selection": "चयन की प्रूफरीडिंग करें",
|
||||
"Open Settings": "सेटिंग्स खोलें",
|
||||
"Open Command Palette": "कमांड पैलेट खोलें",
|
||||
"Show Keyboard Shortcuts": "कीबोर्ड शॉर्टकट दिखाएँ",
|
||||
"Open Books": "पुस्तकें खोलें",
|
||||
"Toggle Fullscreen": "पूर्ण स्क्रीन टॉगल करें",
|
||||
"Close Window": "विंडो बंद करें",
|
||||
"Quit App": "ऐप से बाहर निकलें",
|
||||
"Go Left / Previous Page": "बाएँ जाएँ / पिछला पृष्ठ",
|
||||
"Go Right / Next Page": "दाएँ जाएँ / अगला पृष्ठ",
|
||||
"Go Up": "ऊपर जाएँ",
|
||||
"Go Down": "नीचे जाएँ",
|
||||
"Previous Chapter": "पिछला अध्याय",
|
||||
"Next Chapter": "अगला अध्याय",
|
||||
"Scroll Half Page Down": "आधा पृष्ठ नीचे स्क्रॉल करें",
|
||||
"Scroll Half Page Up": "आधा पृष्ठ ऊपर स्क्रॉल करें",
|
||||
"Save Note": "नोट सहेजें",
|
||||
"Single Section Scroll": "एकल अनुभाग स्क्रॉल",
|
||||
"General": "सामान्य",
|
||||
"Text to Speech": "पाठ से वाक्",
|
||||
"Zoom": "ज़ूम",
|
||||
"Window": "विंडो",
|
||||
"Your Bookshelf": "आपकी किताबों की अलमारी",
|
||||
"TTS": "पाठ से वाक्",
|
||||
"Media Info": "मीडिया जानकारी",
|
||||
"Update Frequency": "अपडेट आवृत्ति",
|
||||
"Every Sentence": "हर वाक्य",
|
||||
"Every Paragraph": "हर पैराग्राफ",
|
||||
"Every Chapter": "हर अध्याय",
|
||||
"TTS Media Info Update Frequency": "TTS मीडिया जानकारी अपडेट आवृत्ति",
|
||||
"Select reading speed": "पढ़ने की गति चुनें",
|
||||
"Drag to seek": "खोजने के लिए खींचें",
|
||||
"Punctuation Delay": "विराम चिह्न विलंब",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "कृपया जारी रखने से पहले पुष्टि करें कि यह OPDS कनेक्शन वेब ऐप पर Readest सर्वर के माध्यम से प्रॉक्सी किया जाएगा।",
|
||||
"Custom Headers": "कस्टम हेडर",
|
||||
"Custom Headers (optional)": "कस्टम हेडर (वैकल्पिक)",
|
||||
"Add one header per line using \"Header-Name: value\".": "\"Header-Name: value\" का उपयोग करके प्रति पंक्ति एक हेडर जोड़ें।",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "मैं समझता/समझती हूँ कि यह OPDS कनेक्शन वेब ऐप पर Readest सर्वर के माध्यम से प्रॉक्सी किया जाएगा। यदि मुझे इन क्रेडेंशियल या हेडर के साथ Readest पर भरोसा नहीं है, तो मुझे नेटिव ऐप का उपयोग करना चाहिए।",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcover से कनेक्ट करने में असमर्थ। कृपया अपना नेटवर्क कनेक्शन जाँचें।",
|
||||
"Invalid Hardcover API token": "अमान्य Hardcover API टोकन",
|
||||
"Disconnected from Hardcover": "Hardcover से डिस्कनेक्ट हो गया",
|
||||
"Hardcover Settings": "Hardcover सेटिंग्स",
|
||||
"Connected to Hardcover": "Hardcover से कनेक्टेड",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "पढ़ने की प्रगति और नोट्स सिंक करने के लिए अपना Hardcover खाता कनेक्ट करें।",
|
||||
"Get your API token from hardcover.app → Settings → API.": "hardcover.app → सेटिंग्स → API से अपना API टोकन प्राप्त करें।",
|
||||
"API Token": "API टोकन",
|
||||
"Paste your Hardcover API token": "अपना Hardcover API टोकन पेस्ट करें",
|
||||
"Hardcover sync enabled for this book": "इस पुस्तक के लिए Hardcover सिंक सक्षम",
|
||||
"Hardcover sync disabled for this book": "इस पुस्तक के लिए Hardcover सिंक अक्षम",
|
||||
"Hardcover Sync": "Hardcover सिंक",
|
||||
"Enable for This Book": "इस पुस्तक के लिए सक्षम करें",
|
||||
"Push Notes": "नोट्स भेजें",
|
||||
"Enable Hardcover sync for this book first.": "पहले इस पुस्तक के लिए Hardcover सिंक सक्षम करें।",
|
||||
"No annotations or excerpts to sync for this book.": "इस पुस्तक के लिए सिंक करने हेतु कोई एनोटेशन या अंश नहीं हैं।",
|
||||
"Configure Hardcover in Settings first.": "पहले सेटिंग्स में Hardcover कॉन्फ़िगर करें।",
|
||||
"No new Hardcover note changes to sync.": "सिंक करने के लिए कोई नया Hardcover नोट परिवर्तन नहीं है।",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover सिंक हुआ: {{inserted}} नए, {{updated}} अपडेटेड, {{skipped}} अपरिवर्तित",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover नोट्स सिंक विफल: {{error}}",
|
||||
"Reading progress synced to Hardcover": "पढ़ने की प्रगति Hardcover से सिंक हो गई",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover प्रगति सिंक विफल: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "मौजूदा स्रोत पहचानकर्ता रखें"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Mode Menulis",
|
||||
"Your Library": "Perpustakaan Anda",
|
||||
"TTS not supported for PDF": "TTS tidak didukung untuk PDF",
|
||||
"Override Book Font": "Ganti Font Buku",
|
||||
"Apply to All Books": "Terapkan ke semua buku",
|
||||
"Apply to This Book": "Terapkan ke buku ini",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Arah RTL",
|
||||
"Maximum Column Height": "Tinggi Kolom Maksimum",
|
||||
"Maximum Column Width": "Lebar Kolom Maksimum",
|
||||
"Continuous Scroll": "Gulir Terus",
|
||||
"Fullscreen": "Layar Penuh",
|
||||
"No supported files found. Supported formats: {{formats}}": "Tidak ada file yang didukung ditemukan. Format yang didukung: {{formats}}",
|
||||
"Drop to Import Books": "Jatuhkan untuk Mengimpor Buku",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "Hapus pencarian",
|
||||
"Clear search history": "Hapus riwayat pencarian",
|
||||
"Tap to Toggle Footer": "Ketuk untuk beralih footer",
|
||||
"Show Current Time": "Tampilkan Waktu Sekarang",
|
||||
"Use 24 Hour Clock": "Gunakan Format 24 Jam",
|
||||
"Show Current Battery Status": "Tampilkan Status Baterai Saat Ini",
|
||||
"Exported successfully": "Berhasil diekspor",
|
||||
"Book exported successfully.": "Buku berhasil diekspor.",
|
||||
"Failed to export the book.": "Gagal mengekspor buku.",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "kata",
|
||||
"{{time}} left": "{{time}} tersisa",
|
||||
"Reading progress": "Kemajuan membaca",
|
||||
"Click to seek": "Klik untuk mencari",
|
||||
"Skip back 15 words": "Lompat mundur 15 kata",
|
||||
"Back 15 words (Shift+Left)": "Mundur 15 kata (Shift+Kiri)",
|
||||
"Pause (Space)": "Jeda (Spasi)",
|
||||
"Play (Space)": "Putar (Spasi)",
|
||||
"Skip forward 15 words": "Lompat maju 15 kata",
|
||||
"Forward 15 words (Shift+Right)": "Maju 15 kata (Shift+Kanan)",
|
||||
"Pause:": "Jeda:",
|
||||
"Decrease speed": "Kurangi kecepatan",
|
||||
"Slower (Left/Down)": "Lebih lambat (Kiri/Bawah)",
|
||||
"Current speed": "Kecepatan saat ini",
|
||||
"Increase speed": "Tambah kecepatan",
|
||||
"Faster (Right/Up)": "Lebih cepat (Kanan/Atas)",
|
||||
"Start RSVP Reading": "Mulai Membaca RSVP",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "Kecerahan Layar Sistem",
|
||||
"Page:": "Halaman:",
|
||||
"Page: {{number}}": "Halaman: {{number}}",
|
||||
"Annotation page number": "Nomor halaman anotasi"
|
||||
"Annotation page number": "Nomor halaman anotasi",
|
||||
"Translating...": "Menerjemahkan...",
|
||||
"Show Battery Percentage": "Tampilkan Persentase Baterai",
|
||||
"Hide Scrollbar": "Sembunyikan Bilah Gulir",
|
||||
"Skip to last reading position": "Lompat ke posisi baca terakhir",
|
||||
"Show context": "Tampilkan konteks",
|
||||
"Hide context": "Sembunyikan konteks",
|
||||
"Decrease font size": "Perkecil ukuran font",
|
||||
"Increase font size": "Perbesar ukuran font",
|
||||
"Focus": "Fokus",
|
||||
"Theme color": "Warna tema",
|
||||
"Import failed": "Impor gagal",
|
||||
"Available Formatters:": "Formatter Tersedia:",
|
||||
"Format date": "Format tanggal",
|
||||
"Markdown block quote (> per line)": "Kutipan blok Markdown (> per baris)",
|
||||
"Newlines to <br>": "Baris baru ke <br>",
|
||||
"Change case": "Ubah huruf besar/kecil",
|
||||
"Trim whitespace": "Pangkas spasi",
|
||||
"Truncate to n characters": "Potong ke n karakter",
|
||||
"Replace text": "Ganti teks",
|
||||
"Fallback value": "Nilai cadangan",
|
||||
"Get length": "Dapatkan panjang",
|
||||
"First/last element": "Elemen pertama/terakhir",
|
||||
"Join array": "Gabungkan array",
|
||||
"Backup failed: {{error}}": "Pencadangan gagal: {{error}}",
|
||||
"Select Backup": "Pilih Cadangan",
|
||||
"Restore failed: {{error}}": "Pemulihan gagal: {{error}}",
|
||||
"Backup & Restore": "Cadangkan & Pulihkan",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Buat cadangan perpustakaan Anda atau pulihkan dari cadangan sebelumnya. Pemulihan akan digabungkan dengan perpustakaan Anda saat ini.",
|
||||
"Backup Library": "Cadangkan Perpustakaan",
|
||||
"Restore Library": "Pulihkan Perpustakaan",
|
||||
"Creating backup...": "Membuat cadangan...",
|
||||
"Restoring library...": "Memulihkan perpustakaan...",
|
||||
"{{current}} of {{total}} items": "{{current}} dari {{total}} item",
|
||||
"Backup completed successfully!": "Pencadangan berhasil!",
|
||||
"Restore completed successfully!": "Pemulihan berhasil!",
|
||||
"Your library has been saved to the selected location.": "Perpustakaan Anda telah disimpan di lokasi yang dipilih.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} buku ditambahkan, {{updated}} buku diperbarui.",
|
||||
"Operation failed": "Operasi gagal",
|
||||
"Loading library...": "Memuat perpustakaan...",
|
||||
"{{count}} books refreshed_other": "{{count}} buku diperbarui",
|
||||
"Failed to refresh metadata": "Gagal memperbarui metadata",
|
||||
"Refresh Metadata": "Perbarui Metadata",
|
||||
"Keyboard Shortcuts": "Pintasan Keyboard",
|
||||
"View all keyboard shortcuts": "Lihat semua pintasan keyboard",
|
||||
"Switch Sidebar Tab": "Ganti tab bilah sisi",
|
||||
"Toggle Notebook": "Tampilkan/sembunyikan buku catatan",
|
||||
"Search in Book": "Cari di buku",
|
||||
"Toggle Scroll Mode": "Alihkan mode gulir",
|
||||
"Toggle Select Mode": "Alihkan mode pilih",
|
||||
"Toggle Bookmark": "Alihkan penanda",
|
||||
"Toggle Text to Speech": "Alihkan teks ke suara",
|
||||
"Play / Pause TTS": "Putar / Jeda TTS",
|
||||
"Toggle Paragraph Mode": "Alihkan mode paragraf",
|
||||
"Highlight Selection": "Sorot pilihan",
|
||||
"Underline Selection": "Garis bawahi pilihan",
|
||||
"Annotate Selection": "Beri anotasi pilihan",
|
||||
"Search Selection": "Cari pilihan",
|
||||
"Copy Selection": "Salin pilihan",
|
||||
"Translate Selection": "Terjemahkan pilihan",
|
||||
"Dictionary Lookup": "Cari di kamus",
|
||||
"Wikipedia Lookup": "Cari di Wikipedia",
|
||||
"Read Aloud Selection": "Bacakan pilihan",
|
||||
"Proofread Selection": "Periksa ejaan pilihan",
|
||||
"Open Settings": "Buka pengaturan",
|
||||
"Open Command Palette": "Buka palet perintah",
|
||||
"Show Keyboard Shortcuts": "Tampilkan pintasan keyboard",
|
||||
"Open Books": "Buka buku",
|
||||
"Toggle Fullscreen": "Alihkan layar penuh",
|
||||
"Close Window": "Tutup jendela",
|
||||
"Quit App": "Keluar dari aplikasi",
|
||||
"Go Left / Previous Page": "Ke kiri / Halaman sebelumnya",
|
||||
"Go Right / Next Page": "Ke kanan / Halaman berikutnya",
|
||||
"Go Up": "Ke atas",
|
||||
"Go Down": "Ke bawah",
|
||||
"Previous Chapter": "Bab sebelumnya",
|
||||
"Next Chapter": "Bab berikutnya",
|
||||
"Scroll Half Page Down": "Gulir setengah halaman ke bawah",
|
||||
"Scroll Half Page Up": "Gulir setengah halaman ke atas",
|
||||
"Save Note": "Simpan catatan",
|
||||
"Single Section Scroll": "Gulir Satu Bagian",
|
||||
"General": "Umum",
|
||||
"Text to Speech": "Teks ke Suara",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Jendela",
|
||||
"Your Bookshelf": "Rak Buku Anda",
|
||||
"TTS": "Teks ke Suara",
|
||||
"Media Info": "Info Media",
|
||||
"Update Frequency": "Frekuensi Pembaruan",
|
||||
"Every Sentence": "Setiap Kalimat",
|
||||
"Every Paragraph": "Setiap Paragraf",
|
||||
"Every Chapter": "Setiap Bab",
|
||||
"TTS Media Info Update Frequency": "Frekuensi Pembaruan Info Media TTS",
|
||||
"Select reading speed": "Pilih kecepatan membaca",
|
||||
"Drag to seek": "Seret untuk mencari",
|
||||
"Punctuation Delay": "Jeda tanda baca",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Harap konfirmasi bahwa koneksi OPDS ini akan diteruskan melalui server Readest di aplikasi web sebelum melanjutkan.",
|
||||
"Custom Headers": "Header Kustom",
|
||||
"Custom Headers (optional)": "Header Kustom (opsional)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Tambahkan satu header per baris menggunakan \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Saya memahami bahwa koneksi OPDS ini akan diteruskan melalui server Readest di aplikasi web. Jika saya tidak mempercayai Readest dengan kredensial atau header ini, saya harus menggunakan aplikasi native.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Tidak dapat terhubung ke Hardcover. Periksa koneksi jaringan Anda.",
|
||||
"Invalid Hardcover API token": "Token API Hardcover tidak valid",
|
||||
"Disconnected from Hardcover": "Terputus dari Hardcover",
|
||||
"Hardcover Settings": "Pengaturan Hardcover",
|
||||
"Connected to Hardcover": "Terhubung ke Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Hubungkan akun Hardcover Anda untuk menyinkronkan progres membaca dan catatan.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Dapatkan token API Anda dari hardcover.app → Pengaturan → API.",
|
||||
"API Token": "Token API",
|
||||
"Paste your Hardcover API token": "Tempel token API Hardcover Anda",
|
||||
"Hardcover sync enabled for this book": "Sinkronisasi Hardcover diaktifkan untuk buku ini",
|
||||
"Hardcover sync disabled for this book": "Sinkronisasi Hardcover dinonaktifkan untuk buku ini",
|
||||
"Hardcover Sync": "Sinkronisasi Hardcover",
|
||||
"Enable for This Book": "Aktifkan untuk Buku Ini",
|
||||
"Push Notes": "Kirim Catatan",
|
||||
"Enable Hardcover sync for this book first.": "Aktifkan sinkronisasi Hardcover untuk buku ini terlebih dahulu.",
|
||||
"No annotations or excerpts to sync for this book.": "Tidak ada anotasi atau kutipan untuk disinkronkan pada buku ini.",
|
||||
"Configure Hardcover in Settings first.": "Konfigurasikan Hardcover di Pengaturan terlebih dahulu.",
|
||||
"No new Hardcover note changes to sync.": "Tidak ada perubahan catatan Hardcover baru untuk disinkronkan.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover disinkronkan: {{inserted}} baru, {{updated}} diperbarui, {{skipped}} tidak berubah",
|
||||
"Hardcover notes sync failed: {{error}}": "Sinkronisasi catatan Hardcover gagal: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Progres membaca disinkronkan ke Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Sinkronisasi progres Hardcover gagal: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Pertahankan pengidentifikasi sumber yang ada"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Modalità scrittura",
|
||||
"Your Library": "La tua biblioteca",
|
||||
"TTS not supported for PDF": "TTS non supportato per PDF",
|
||||
"Override Book Font": "Sovrascrivi font libro",
|
||||
"Apply to All Books": "Applica a tutti i libri",
|
||||
"Apply to This Book": "Applica a questo libro",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Direzione RTL",
|
||||
"Maximum Column Height": "Altezza massima colonna",
|
||||
"Maximum Column Width": "Larghezza massima colonna",
|
||||
"Continuous Scroll": "Scorrimento continuo",
|
||||
"Fullscreen": "Schermo intero",
|
||||
"No supported files found. Supported formats: {{formats}}": "Nessun file supportato trovato. Formati supportati: {{formats}}",
|
||||
"Drop to Import Books": "Rilascia per importare libri",
|
||||
@@ -842,6 +840,9 @@
|
||||
"Clear search": "Cancella ricerca",
|
||||
"Clear search history": "Cancella cronologia ricerche",
|
||||
"Tap to Toggle Footer": "Tocca per mostrare/nascondere il piè di pagina",
|
||||
"Show Current Time": "Mostra ora attuale",
|
||||
"Use 24 Hour Clock": "Usa formato 24 ore",
|
||||
"Show Current Battery Status": "Mostra lo stato attuale della batteria",
|
||||
"Exported successfully": "Esportato con successo",
|
||||
"Book exported successfully.": "Libro esportato con successo.",
|
||||
"Failed to export the book.": "Impossibile esportare il libro.",
|
||||
@@ -993,17 +994,14 @@
|
||||
"words": "parole",
|
||||
"{{time}} left": "{{time}} rimanenti",
|
||||
"Reading progress": "Progresso lettura",
|
||||
"Click to seek": "Clicca per cercare",
|
||||
"Skip back 15 words": "Indietro di 15 parole",
|
||||
"Back 15 words (Shift+Left)": "Indietro di 15 parole (Shift+Sinistra)",
|
||||
"Pause (Space)": "Pausa (Spazio)",
|
||||
"Play (Space)": "Riproduci (Spazio)",
|
||||
"Skip forward 15 words": "Avanti di 15 parole",
|
||||
"Forward 15 words (Shift+Right)": "Avanti di 15 parole (Shift+Destra)",
|
||||
"Pause:": "Pausa:",
|
||||
"Decrease speed": "Diminuisci velocità",
|
||||
"Slower (Left/Down)": "Più lento (Sinistra/Giù)",
|
||||
"Current speed": "Velocità attuale",
|
||||
"Increase speed": "Aumenta velocità",
|
||||
"Faster (Right/Up)": "Più veloce (Destra/Su)",
|
||||
"Start RSVP Reading": "Avvia lettura RSVP",
|
||||
@@ -1071,5 +1069,131 @@
|
||||
"System Screen Brightness": "Luminosità dello schermo del sistema",
|
||||
"Page:": "Pagina:",
|
||||
"Page: {{number}}": "Pagina: {{number}}",
|
||||
"Annotation page number": "Numero di pagina dell'annotazione"
|
||||
"Annotation page number": "Numero di pagina dell'annotazione",
|
||||
"Translating...": "Traduzione...",
|
||||
"Show Battery Percentage": "Mostra percentuale batteria",
|
||||
"Hide Scrollbar": "Nascondi barra di scorrimento",
|
||||
"Skip to last reading position": "Vai all'ultima posizione di lettura",
|
||||
"Show context": "Mostra contesto",
|
||||
"Hide context": "Nascondi contesto",
|
||||
"Decrease font size": "Riduci dimensione carattere",
|
||||
"Increase font size": "Aumenta dimensione carattere",
|
||||
"Focus": "Messa a fuoco",
|
||||
"Theme color": "Colore del tema",
|
||||
"Import failed": "Importazione fallita",
|
||||
"Available Formatters:": "Formattatori disponibili:",
|
||||
"Format date": "Formatta data",
|
||||
"Markdown block quote (> per line)": "Citazione Markdown (> per riga)",
|
||||
"Newlines to <br>": "A capo in <br>",
|
||||
"Change case": "Cambia maiuscole/minuscole",
|
||||
"Trim whitespace": "Rimuovi spazi",
|
||||
"Truncate to n characters": "Tronca a n caratteri",
|
||||
"Replace text": "Sostituisci testo",
|
||||
"Fallback value": "Valore predefinito",
|
||||
"Get length": "Ottieni lunghezza",
|
||||
"First/last element": "Primo/ultimo elemento",
|
||||
"Join array": "Unisci array",
|
||||
"Backup failed: {{error}}": "Backup fallito: {{error}}",
|
||||
"Select Backup": "Seleziona backup",
|
||||
"Restore failed: {{error}}": "Ripristino fallito: {{error}}",
|
||||
"Backup & Restore": "Backup e ripristino",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Crea un backup della tua libreria o ripristina da un backup precedente. Il ripristino verrà unito alla tua libreria attuale.",
|
||||
"Backup Library": "Backup libreria",
|
||||
"Restore Library": "Ripristina libreria",
|
||||
"Creating backup...": "Creazione backup...",
|
||||
"Restoring library...": "Ripristino libreria...",
|
||||
"{{current}} of {{total}} items": "{{current}} di {{total}} elementi",
|
||||
"Backup completed successfully!": "Backup completato con successo!",
|
||||
"Restore completed successfully!": "Ripristino completato con successo!",
|
||||
"Your library has been saved to the selected location.": "La tua libreria è stata salvata nella posizione selezionata.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} libri aggiunti, {{updated}} libri aggiornati.",
|
||||
"Operation failed": "Operazione fallita",
|
||||
"Loading library...": "Caricamento libreria...",
|
||||
"{{count}} books refreshed_one": "{{count}} libro aggiornato",
|
||||
"{{count}} books refreshed_many": "{{count}} libri aggiornati",
|
||||
"{{count}} books refreshed_other": "{{count}} libri aggiornati",
|
||||
"Failed to refresh metadata": "Aggiornamento metadati fallito",
|
||||
"Refresh Metadata": "Aggiorna metadati",
|
||||
"Keyboard Shortcuts": "Scorciatoie da tastiera",
|
||||
"View all keyboard shortcuts": "Visualizza tutte le scorciatoie",
|
||||
"Switch Sidebar Tab": "Cambia scheda barra laterale",
|
||||
"Toggle Notebook": "Mostra/nascondi blocco note",
|
||||
"Search in Book": "Cerca nel libro",
|
||||
"Toggle Scroll Mode": "Attiva/disattiva scorrimento",
|
||||
"Toggle Select Mode": "Attiva/disattiva selezione",
|
||||
"Toggle Bookmark": "Attiva/disattiva segnalibro",
|
||||
"Toggle Text to Speech": "Attiva/disattiva sintesi vocale",
|
||||
"Play / Pause TTS": "Riproduci / Pausa TTS",
|
||||
"Toggle Paragraph Mode": "Attiva/disattiva modo paragrafo",
|
||||
"Highlight Selection": "Evidenzia selezione",
|
||||
"Underline Selection": "Sottolinea selezione",
|
||||
"Annotate Selection": "Annota selezione",
|
||||
"Search Selection": "Cerca selezione",
|
||||
"Copy Selection": "Copia selezione",
|
||||
"Translate Selection": "Traduci selezione",
|
||||
"Dictionary Lookup": "Cerca nel dizionario",
|
||||
"Wikipedia Lookup": "Cerca su Wikipedia",
|
||||
"Read Aloud Selection": "Leggi la selezione ad alta voce",
|
||||
"Proofread Selection": "Correggi selezione",
|
||||
"Open Settings": "Apri impostazioni",
|
||||
"Open Command Palette": "Apri tavolozza comandi",
|
||||
"Show Keyboard Shortcuts": "Mostra scorciatoie da tastiera",
|
||||
"Open Books": "Apri libri",
|
||||
"Toggle Fullscreen": "Attiva/disattiva schermo intero",
|
||||
"Close Window": "Chiudi finestra",
|
||||
"Quit App": "Esci dall'app",
|
||||
"Go Left / Previous Page": "Vai a sinistra / Pagina precedente",
|
||||
"Go Right / Next Page": "Vai a destra / Pagina successiva",
|
||||
"Go Up": "Vai su",
|
||||
"Go Down": "Vai giù",
|
||||
"Previous Chapter": "Capitolo precedente",
|
||||
"Next Chapter": "Capitolo successivo",
|
||||
"Scroll Half Page Down": "Scorri mezza pagina in basso",
|
||||
"Scroll Half Page Up": "Scorri mezza pagina in alto",
|
||||
"Save Note": "Salva nota",
|
||||
"Single Section Scroll": "Scorrimento a sezione singola",
|
||||
"General": "Generale",
|
||||
"Text to Speech": "Sintesi vocale",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Finestra",
|
||||
"Your Bookshelf": "La tua libreria",
|
||||
"TTS": "Sintesi vocale",
|
||||
"Media Info": "Info multimediali",
|
||||
"Update Frequency": "Frequenza aggiornamento",
|
||||
"Every Sentence": "Ogni frase",
|
||||
"Every Paragraph": "Ogni paragrafo",
|
||||
"Every Chapter": "Ogni capitolo",
|
||||
"TTS Media Info Update Frequency": "Frequenza aggiornamento info multimediali TTS",
|
||||
"Select reading speed": "Seleziona velocità di lettura",
|
||||
"Drag to seek": "Trascina per cercare",
|
||||
"Punctuation Delay": "Ritardo punteggiatura",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Conferma che questa connessione OPDS verrà instradata attraverso i server Readest nell'app web prima di continuare.",
|
||||
"Custom Headers": "Intestazioni personalizzate",
|
||||
"Custom Headers (optional)": "Intestazioni personalizzate (facoltativo)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Aggiungi un'intestazione per riga usando \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Capisco che questa connessione OPDS verrà instradata attraverso i server Readest nell'app web. Se non mi fido di Readest con queste credenziali o intestazioni, dovrei usare l'app nativa.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Impossibile connettersi a Hardcover. Controlla la connessione di rete.",
|
||||
"Invalid Hardcover API token": "Token API Hardcover non valido",
|
||||
"Disconnected from Hardcover": "Disconnesso da Hardcover",
|
||||
"Hardcover Settings": "Impostazioni Hardcover",
|
||||
"Connected to Hardcover": "Connesso a Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Collega il tuo account Hardcover per sincronizzare i progressi di lettura e le note.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Ottieni il tuo token API da hardcover.app → Impostazioni → API.",
|
||||
"API Token": "Token API",
|
||||
"Paste your Hardcover API token": "Incolla il tuo token API Hardcover",
|
||||
"Hardcover sync enabled for this book": "Sincronizzazione Hardcover attivata per questo libro",
|
||||
"Hardcover sync disabled for this book": "Sincronizzazione Hardcover disattivata per questo libro",
|
||||
"Hardcover Sync": "Sincronizzazione Hardcover",
|
||||
"Enable for This Book": "Attiva per questo libro",
|
||||
"Push Notes": "Invia note",
|
||||
"Enable Hardcover sync for this book first.": "Attiva prima la sincronizzazione Hardcover per questo libro.",
|
||||
"No annotations or excerpts to sync for this book.": "Nessuna annotazione o estratto da sincronizzare per questo libro.",
|
||||
"Configure Hardcover in Settings first.": "Configura prima Hardcover nelle impostazioni.",
|
||||
"No new Hardcover note changes to sync.": "Nessuna nuova modifica alle note Hardcover da sincronizzare.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover sincronizzato: {{inserted}} nuovi, {{updated}} aggiornati, {{skipped}} invariati",
|
||||
"Hardcover notes sync failed: {{error}}": "Sincronizzazione note Hardcover non riuscita: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Progressi di lettura sincronizzati con Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Sincronizzazione progressi Hardcover non riuscita: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Mantieni l'identificatore sorgente esistente"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "ウィキペディア",
|
||||
"Writing Mode": "書き込みモード",
|
||||
"Your Library": "ライブラリー",
|
||||
"TTS not supported for PDF": "PDFではTTSがサポートされていません",
|
||||
"Override Book Font": "書籍フォントを上書き",
|
||||
"Apply to All Books": "すべての書籍に適用",
|
||||
"Apply to This Book": "この書籍に適用",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "RTL方向",
|
||||
"Maximum Column Height": "最大列高",
|
||||
"Maximum Column Width": "最大列幅",
|
||||
"Continuous Scroll": "連続スクロール",
|
||||
"Fullscreen": "全画面",
|
||||
"No supported files found. Supported formats: {{formats}}": "サポートされているファイルが見つかりません。サポートされている形式:{{formats}}",
|
||||
"Drop to Import Books": "書籍をインポートするにはドロップ",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "検索をクリア",
|
||||
"Clear search history": "検索履歴をクリア",
|
||||
"Tap to Toggle Footer": "タップでフッターを切り替え",
|
||||
"Show Current Time": "現在時刻を表示",
|
||||
"Use 24 Hour Clock": "24時間表示を使用",
|
||||
"Show Current Battery Status": "バッテリー残量を表示",
|
||||
"Exported successfully": "エクスポート成功",
|
||||
"Book exported successfully.": "書籍のエクスポートに成功しました。",
|
||||
"Failed to export the book.": "書籍のエクスポートに失敗しました。",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "単語",
|
||||
"{{time}} left": "残り {{time}}",
|
||||
"Reading progress": "読書の進捗",
|
||||
"Click to seek": "クリックしてシーク",
|
||||
"Skip back 15 words": "15単語戻る",
|
||||
"Back 15 words (Shift+Left)": "15単語戻る (Shift+左)",
|
||||
"Pause (Space)": "一時停止 (Space)",
|
||||
"Play (Space)": "再生 (Space)",
|
||||
"Skip forward 15 words": "15単語進む",
|
||||
"Forward 15 words (Shift+Right)": "15単語進む (Shift+右)",
|
||||
"Pause:": "一時停止:",
|
||||
"Decrease speed": "速度を下げる",
|
||||
"Slower (Left/Down)": "低速 (左/下)",
|
||||
"Current speed": "現在の速度",
|
||||
"Increase speed": "速度を上げる",
|
||||
"Faster (Right/Up)": "高速 (右/上)",
|
||||
"Start RSVP Reading": "RSVP読書を開始",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "システムの画面の明るさ",
|
||||
"Page:": "ページ:",
|
||||
"Page: {{number}}": "ページ: {{number}}",
|
||||
"Annotation page number": "注釈のページ番号"
|
||||
"Annotation page number": "注釈のページ番号",
|
||||
"Translating...": "翻訳中...",
|
||||
"Show Battery Percentage": "バッテリー残量を表示",
|
||||
"Hide Scrollbar": "スクロールバーを非表示",
|
||||
"Skip to last reading position": "最後の読書位置へ移動",
|
||||
"Show context": "コンテキストを表示",
|
||||
"Hide context": "コンテキストを非表示",
|
||||
"Decrease font size": "文字サイズを縮小",
|
||||
"Increase font size": "文字サイズを拡大",
|
||||
"Focus": "フォーカス",
|
||||
"Theme color": "テーマカラー",
|
||||
"Import failed": "インポートに失敗しました",
|
||||
"Available Formatters:": "利用可能なフォーマッター:",
|
||||
"Format date": "日付のフォーマット",
|
||||
"Markdown block quote (> per line)": "Markdown引用ブロック (行ごとに >)",
|
||||
"Newlines to <br>": "改行を <br> に変換",
|
||||
"Change case": "大文字/小文字の変更",
|
||||
"Trim whitespace": "空白を除去",
|
||||
"Truncate to n characters": "n文字に切り詰め",
|
||||
"Replace text": "テキストを置換",
|
||||
"Fallback value": "フォールバック値",
|
||||
"Get length": "長さを取得",
|
||||
"First/last element": "最初/最後の要素",
|
||||
"Join array": "配列を結合",
|
||||
"Backup failed: {{error}}": "バックアップに失敗しました: {{error}}",
|
||||
"Select Backup": "バックアップを選択",
|
||||
"Restore failed: {{error}}": "復元に失敗しました: {{error}}",
|
||||
"Backup & Restore": "バックアップと復元",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "ライブラリのバックアップを作成するか、以前のバックアップから復元します。復元は現在のライブラリとマージされます。",
|
||||
"Backup Library": "ライブラリをバックアップ",
|
||||
"Restore Library": "ライブラリを復元",
|
||||
"Creating backup...": "バックアップを作成中...",
|
||||
"Restoring library...": "ライブラリを復元中...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 件",
|
||||
"Backup completed successfully!": "バックアップが完了しました!",
|
||||
"Restore completed successfully!": "復元が完了しました!",
|
||||
"Your library has been saved to the selected location.": "ライブラリが選択した場所に保存されました。",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}冊追加、{{updated}}冊更新されました。",
|
||||
"Operation failed": "操作に失敗しました",
|
||||
"Loading library...": "ライブラリを読み込み中...",
|
||||
"{{count}} books refreshed_other": "{{count}}冊のメタデータを更新しました",
|
||||
"Failed to refresh metadata": "メタデータの更新に失敗しました",
|
||||
"Refresh Metadata": "メタデータを更新",
|
||||
"Keyboard Shortcuts": "キーボードショートカット",
|
||||
"View all keyboard shortcuts": "すべてのキーボードショートカットを表示",
|
||||
"Switch Sidebar Tab": "サイドバータブを切り替え",
|
||||
"Toggle Notebook": "ノートブックの表示/非表示",
|
||||
"Search in Book": "本の中を検索",
|
||||
"Toggle Scroll Mode": "スクロールモードの切り替え",
|
||||
"Toggle Select Mode": "選択モードの切り替え",
|
||||
"Toggle Bookmark": "ブックマークの切り替え",
|
||||
"Toggle Text to Speech": "音声読み上げの切り替え",
|
||||
"Play / Pause TTS": "読み上げ再生 / 一時停止",
|
||||
"Toggle Paragraph Mode": "段落モードの切り替え",
|
||||
"Highlight Selection": "選択範囲をハイライト",
|
||||
"Underline Selection": "選択範囲に下線",
|
||||
"Annotate Selection": "選択範囲に注釈",
|
||||
"Search Selection": "選択範囲を検索",
|
||||
"Copy Selection": "選択範囲をコピー",
|
||||
"Translate Selection": "選択範囲を翻訳",
|
||||
"Dictionary Lookup": "辞書で調べる",
|
||||
"Wikipedia Lookup": "Wikipediaで調べる",
|
||||
"Read Aloud Selection": "選択範囲を読み上げ",
|
||||
"Proofread Selection": "選択範囲を校正",
|
||||
"Open Settings": "設定を開く",
|
||||
"Open Command Palette": "コマンドパレットを開く",
|
||||
"Show Keyboard Shortcuts": "キーボードショートカットを表示",
|
||||
"Open Books": "本を開く",
|
||||
"Toggle Fullscreen": "フルスクリーンの切り替え",
|
||||
"Close Window": "ウィンドウを閉じる",
|
||||
"Quit App": "アプリを終了",
|
||||
"Go Left / Previous Page": "左へ / 前のページ",
|
||||
"Go Right / Next Page": "右へ / 次のページ",
|
||||
"Go Up": "上へ",
|
||||
"Go Down": "下へ",
|
||||
"Previous Chapter": "前の章",
|
||||
"Next Chapter": "次の章",
|
||||
"Scroll Half Page Down": "半ページ下にスクロール",
|
||||
"Scroll Half Page Up": "半ページ上にスクロール",
|
||||
"Save Note": "メモを保存",
|
||||
"Single Section Scroll": "単一セクションスクロール",
|
||||
"General": "一般",
|
||||
"Text to Speech": "テキスト読み上げ",
|
||||
"Zoom": "ズーム",
|
||||
"Window": "ウィンドウ",
|
||||
"Your Bookshelf": "あなたの本棚",
|
||||
"TTS": "読み上げ",
|
||||
"Media Info": "メディア情報",
|
||||
"Update Frequency": "更新頻度",
|
||||
"Every Sentence": "文ごと",
|
||||
"Every Paragraph": "段落ごと",
|
||||
"Every Chapter": "章ごと",
|
||||
"TTS Media Info Update Frequency": "TTSメディア情報の更新頻度",
|
||||
"Select reading speed": "読み上げ速度を選択",
|
||||
"Drag to seek": "ドラッグしてシーク",
|
||||
"Punctuation Delay": "句読点の遅延",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "続行する前に、このOPDS接続がWebアプリのReadestサーバーを経由してプロキシされることを確認してください。",
|
||||
"Custom Headers": "カスタムヘッダー",
|
||||
"Custom Headers (optional)": "カスタムヘッダー(任意)",
|
||||
"Add one header per line using \"Header-Name: value\".": "「Header-Name: value」の形式で1行に1つのヘッダーを追加してください。",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "このOPDS接続がWebアプリのReadestサーバーを経由してプロキシされることを理解しています。これらの認証情報やヘッダーについてReadestを信頼しない場合は、ネイティブアプリを使用してください。",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcoverに接続できません。ネットワーク接続を確認してください。",
|
||||
"Invalid Hardcover API token": "Hardcover APIトークンが無効です",
|
||||
"Disconnected from Hardcover": "Hardcoverから切断されました",
|
||||
"Hardcover Settings": "Hardcover設定",
|
||||
"Connected to Hardcover": "Hardcoverに接続済み",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Hardcoverアカウントを接続して、読書の進捗やメモを同期しましょう。",
|
||||
"Get your API token from hardcover.app → Settings → API.": "hardcover.app → 設定 → APIからAPIトークンを取得してください。",
|
||||
"API Token": "APIトークン",
|
||||
"Paste your Hardcover API token": "HardcoverのAPIトークンを貼り付け",
|
||||
"Hardcover sync enabled for this book": "この本のHardcover同期が有効になりました",
|
||||
"Hardcover sync disabled for this book": "この本のHardcover同期が無効になりました",
|
||||
"Hardcover Sync": "Hardcover同期",
|
||||
"Enable for This Book": "この本で有効にする",
|
||||
"Push Notes": "メモを送信",
|
||||
"Enable Hardcover sync for this book first.": "まずこの本のHardcover同期を有効にしてください。",
|
||||
"No annotations or excerpts to sync for this book.": "この本に同期する注釈や抜粋がありません。",
|
||||
"Configure Hardcover in Settings first.": "まず設定でHardcoverを構成してください。",
|
||||
"No new Hardcover note changes to sync.": "同期するHardcoverメモの新しい変更はありません。",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover同期完了: {{inserted}}件追加、{{updated}}件更新、{{skipped}}件変更なし",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcoverメモの同期に失敗: {{error}}",
|
||||
"Reading progress synced to Hardcover": "読書の進捗がHardcoverに同期されました",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover進捗の同期に失敗: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "既存のソース識別子を保持"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "위키피디아",
|
||||
"Writing Mode": "글쓰기 모드",
|
||||
"Your Library": "내 서재",
|
||||
"TTS not supported for PDF": "PDF에서 TTS가 지원되지 않음",
|
||||
"Override Book Font": "책 글꼴 덮어쓰기",
|
||||
"Apply to All Books": "모든 책에 적용",
|
||||
"Apply to This Book": "이 책에 적용",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "RTL 방향",
|
||||
"Maximum Column Height": "최대 열 높이",
|
||||
"Maximum Column Width": "최대 열 너비",
|
||||
"Continuous Scroll": "연속 스크롤",
|
||||
"Fullscreen": "전체 화면",
|
||||
"No supported files found. Supported formats: {{formats}}": "지원되는 파일이 없습니다. 지원되는 형식: {{formats}}",
|
||||
"Drop to Import Books": "책 가져오기 위해 드롭",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "검색 지우기",
|
||||
"Clear search history": "검색 기록 지우기",
|
||||
"Tap to Toggle Footer": "탭하여 바닥글 전환",
|
||||
"Show Current Time": "현재 시간 표시",
|
||||
"Use 24 Hour Clock": "24시간제 사용",
|
||||
"Show Current Battery Status": "현재 배터리 상태 표시",
|
||||
"Exported successfully": "내보내기 성공",
|
||||
"Book exported successfully.": "책을 성공적으로 내보냈습니다.",
|
||||
"Failed to export the book.": "책 내보내기에 실패했습니다.",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "단어",
|
||||
"{{time}} left": "{{time}} 남음",
|
||||
"Reading progress": "독서 진행 상황",
|
||||
"Click to seek": "클릭하여 탐색",
|
||||
"Skip back 15 words": "15단어 뒤로",
|
||||
"Back 15 words (Shift+Left)": "15단어 뒤로 (Shift+왼쪽 화살표)",
|
||||
"Pause (Space)": "일시정지 (스페이스바)",
|
||||
"Play (Space)": "재생 (스페이스바)",
|
||||
"Skip forward 15 words": "15단어 앞으로",
|
||||
"Forward 15 words (Shift+Right)": "15단어 앞으로 (Shift+오른쪽 화살표)",
|
||||
"Pause:": "일시정지:",
|
||||
"Decrease speed": "속도 줄이기",
|
||||
"Slower (Left/Down)": "느리게 (왼쪽/아래 화살표)",
|
||||
"Current speed": "현재 속도",
|
||||
"Increase speed": "속도 높이기",
|
||||
"Faster (Right/Up)": "빠르게 (오른쪽/위 화살표)",
|
||||
"Start RSVP Reading": "RSVP 독서 시작",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "시스템 화면 밝기",
|
||||
"Page:": "페이지:",
|
||||
"Page: {{number}}": "페이지: {{number}}",
|
||||
"Annotation page number": "주석 페이지 번호"
|
||||
"Annotation page number": "주석 페이지 번호",
|
||||
"Translating...": "번역 중...",
|
||||
"Show Battery Percentage": "배터리 백분율 표시",
|
||||
"Hide Scrollbar": "스크롤바 숨기기",
|
||||
"Skip to last reading position": "마지막 읽기 위치로 이동",
|
||||
"Show context": "맥락 표시",
|
||||
"Hide context": "맥락 숨기기",
|
||||
"Decrease font size": "글꼴 크기 줄이기",
|
||||
"Increase font size": "글꼴 크기 늘리기",
|
||||
"Focus": "초점",
|
||||
"Theme color": "테마 색상",
|
||||
"Import failed": "가져오기 실패",
|
||||
"Available Formatters:": "사용 가능한 포맷터:",
|
||||
"Format date": "날짜 형식",
|
||||
"Markdown block quote (> per line)": "Markdown 인용 블록 (줄마다 >)",
|
||||
"Newlines to <br>": "줄바꿈을 <br>로 변환",
|
||||
"Change case": "대소문자 변경",
|
||||
"Trim whitespace": "공백 제거",
|
||||
"Truncate to n characters": "n자로 자르기",
|
||||
"Replace text": "텍스트 바꾸기",
|
||||
"Fallback value": "기본값",
|
||||
"Get length": "길이 가져오기",
|
||||
"First/last element": "첫 번째/마지막 요소",
|
||||
"Join array": "배열 합치기",
|
||||
"Backup failed: {{error}}": "백업 실패: {{error}}",
|
||||
"Select Backup": "백업 선택",
|
||||
"Restore failed: {{error}}": "복원 실패: {{error}}",
|
||||
"Backup & Restore": "백업 및 복원",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "라이브러리를 백업하거나 이전 백업에서 복원하세요. 복원 시 현재 라이브러리와 병합됩니다.",
|
||||
"Backup Library": "라이브러리 백업",
|
||||
"Restore Library": "라이브러리 복원",
|
||||
"Creating backup...": "백업 생성 중...",
|
||||
"Restoring library...": "라이브러리 복원 중...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 항목",
|
||||
"Backup completed successfully!": "백업이 완료되었습니다!",
|
||||
"Restore completed successfully!": "복원이 완료되었습니다!",
|
||||
"Your library has been saved to the selected location.": "라이브러리가 선택한 위치에 저장되었습니다.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}권 추가, {{updated}}권 업데이트되었습니다.",
|
||||
"Operation failed": "작업 실패",
|
||||
"Loading library...": "라이브러리 로딩 중...",
|
||||
"{{count}} books refreshed_other": "{{count}}권 새로고침됨",
|
||||
"Failed to refresh metadata": "메타데이터 새로고침 실패",
|
||||
"Refresh Metadata": "메타데이터 새로고침",
|
||||
"Keyboard Shortcuts": "키보드 단축키",
|
||||
"View all keyboard shortcuts": "모든 키보드 단축키 보기",
|
||||
"Switch Sidebar Tab": "사이드바 탭 전환",
|
||||
"Toggle Notebook": "노트북 표시/숨기기",
|
||||
"Search in Book": "책에서 검색",
|
||||
"Toggle Scroll Mode": "스크롤 모드 전환",
|
||||
"Toggle Select Mode": "선택 모드 전환",
|
||||
"Toggle Bookmark": "북마크 전환",
|
||||
"Toggle Text to Speech": "텍스트 음성 변환 전환",
|
||||
"Play / Pause TTS": "TTS 재생 / 일시정지",
|
||||
"Toggle Paragraph Mode": "문단 모드 전환",
|
||||
"Highlight Selection": "선택 영역 강조",
|
||||
"Underline Selection": "선택 영역 밑줄",
|
||||
"Annotate Selection": "선택 영역 주석",
|
||||
"Search Selection": "선택 영역 검색",
|
||||
"Copy Selection": "선택 영역 복사",
|
||||
"Translate Selection": "선택 영역 번역",
|
||||
"Dictionary Lookup": "사전 검색",
|
||||
"Wikipedia Lookup": "위키백과 검색",
|
||||
"Read Aloud Selection": "선택 영역 소리 내어 읽기",
|
||||
"Proofread Selection": "선택 영역 교정",
|
||||
"Open Settings": "설정 열기",
|
||||
"Open Command Palette": "명령 팔레트 열기",
|
||||
"Show Keyboard Shortcuts": "키보드 단축키 표시",
|
||||
"Open Books": "책 열기",
|
||||
"Toggle Fullscreen": "전체 화면 전환",
|
||||
"Close Window": "창 닫기",
|
||||
"Quit App": "앱 종료",
|
||||
"Go Left / Previous Page": "왼쪽으로 / 이전 페이지",
|
||||
"Go Right / Next Page": "오른쪽으로 / 다음 페이지",
|
||||
"Go Up": "위로",
|
||||
"Go Down": "아래로",
|
||||
"Previous Chapter": "이전 장",
|
||||
"Next Chapter": "다음 장",
|
||||
"Scroll Half Page Down": "반 페이지 아래로 스크롤",
|
||||
"Scroll Half Page Up": "반 페이지 위로 스크롤",
|
||||
"Save Note": "메모 저장",
|
||||
"Single Section Scroll": "단일 섹션 스크롤",
|
||||
"General": "일반",
|
||||
"Text to Speech": "텍스트 음성 변환",
|
||||
"Zoom": "확대/축소",
|
||||
"Window": "창",
|
||||
"Your Bookshelf": "내 책장",
|
||||
"TTS": "음성 읽기",
|
||||
"Media Info": "미디어 정보",
|
||||
"Update Frequency": "업데이트 빈도",
|
||||
"Every Sentence": "문장마다",
|
||||
"Every Paragraph": "단락마다",
|
||||
"Every Chapter": "챕터마다",
|
||||
"TTS Media Info Update Frequency": "TTS 미디어 정보 업데이트 빈도",
|
||||
"Select reading speed": "읽기 속도 선택",
|
||||
"Drag to seek": "드래그하여 탐색",
|
||||
"Punctuation Delay": "구두점 지연",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "계속하기 전에 이 OPDS 연결이 웹 앱의 Readest 서버를 통해 프록시될 것임을 확인해 주세요.",
|
||||
"Custom Headers": "사용자 정의 헤더",
|
||||
"Custom Headers (optional)": "사용자 정의 헤더 (선택 사항)",
|
||||
"Add one header per line using \"Header-Name: value\".": "\"Header-Name: value\" 형식으로 한 줄에 하나의 헤더를 추가하세요.",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "이 OPDS 연결이 웹 앱의 Readest 서버를 통해 프록시될 것임을 이해합니다. 이 자격 증명이나 헤더에 대해 Readest를 신뢰하지 않는다면 네이티브 앱을 사용해야 합니다.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcover에 연결할 수 없습니다. 네트워크 연결을 확인해 주세요.",
|
||||
"Invalid Hardcover API token": "유효하지 않은 Hardcover API 토큰",
|
||||
"Disconnected from Hardcover": "Hardcover에서 연결 해제됨",
|
||||
"Hardcover Settings": "Hardcover 설정",
|
||||
"Connected to Hardcover": "Hardcover에 연결됨",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "독서 진행 상황과 메모를 동기화하려면 Hardcover 계정을 연결하세요.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "hardcover.app → 설정 → API에서 API 토큰을 받으세요.",
|
||||
"API Token": "API 토큰",
|
||||
"Paste your Hardcover API token": "Hardcover API 토큰을 붙여넣기",
|
||||
"Hardcover sync enabled for this book": "이 책의 Hardcover 동기화가 활성화됨",
|
||||
"Hardcover sync disabled for this book": "이 책의 Hardcover 동기화가 비활성화됨",
|
||||
"Hardcover Sync": "Hardcover 동기화",
|
||||
"Enable for This Book": "이 책에 대해 활성화",
|
||||
"Push Notes": "메모 보내기",
|
||||
"Enable Hardcover sync for this book first.": "먼저 이 책의 Hardcover 동기화를 활성화하세요.",
|
||||
"No annotations or excerpts to sync for this book.": "이 책에 동기화할 주석이나 발췌가 없습니다.",
|
||||
"Configure Hardcover in Settings first.": "먼저 설정에서 Hardcover를 구성하세요.",
|
||||
"No new Hardcover note changes to sync.": "동기화할 새로운 Hardcover 메모 변경 사항이 없습니다.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover 동기화 완료: {{inserted}}개 추가, {{updated}}개 업데이트, {{skipped}}개 변경 없음",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover 메모 동기화 실패: {{error}}",
|
||||
"Reading progress synced to Hardcover": "독서 진행 상황이 Hardcover에 동기화됨",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover 진행 상황 동기화 실패: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "기존 소스 식별자 유지"
|
||||
}
|
||||
|
||||
@@ -293,7 +293,6 @@
|
||||
"Next Paragraph": "Perenggan Seterusnya",
|
||||
"Read Aloud": "Baca Dengan Kuat",
|
||||
"Ready to read aloud": "Bersedia untuk baca dengan kuat",
|
||||
"TTS not supported for PDF": "TTS tidak disokong untuk PDF",
|
||||
"TTS not supported for this document": "TTS tidak disokong untuk dokumen ini",
|
||||
"No Timeout": "Tiada Had Masa",
|
||||
"{{value}} minute": "{{value}} minit",
|
||||
@@ -501,7 +500,6 @@
|
||||
"Quick Colors": "Warna Pantas",
|
||||
"Override Book Color": "Ganti Warna Buku",
|
||||
"Scroll": "Tatal",
|
||||
"Continuous Scroll": "Tatal Berterusan",
|
||||
"Overlap Pixels": "Piksel Bertindih",
|
||||
"Pagination": "Penomboran Halaman",
|
||||
"Tap to Paginate": "Ketik untuk Nomborkan Halaman",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "Kosongkan carian",
|
||||
"Clear search history": "Kosongkan sejarah carian",
|
||||
"Tap to Toggle Footer": "Ketik untuk togol pengaki",
|
||||
"Show Current Time": "Tunjukkan Waktu Sekarang",
|
||||
"Use 24 Hour Clock": "Gunakan Format 24 Jam",
|
||||
"Show Current Battery Status": "Paparkan Status Bateri Semasa",
|
||||
"Exported successfully": "Berjaya dieksport",
|
||||
"Book exported successfully.": "Buku berjaya dieksport.",
|
||||
"Failed to export the book.": "Gagal mengeksport buku.",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "perkataan",
|
||||
"{{time}} left": "{{time}} tinggal",
|
||||
"Reading progress": "Kemajuan membaca",
|
||||
"Click to seek": "Klik untuk cari",
|
||||
"Skip back 15 words": "Langkau belakang 15 perkataan",
|
||||
"Back 15 words (Shift+Left)": "Belakang 15 perkataan (Shift+Kiri)",
|
||||
"Pause (Space)": "Jeda (Ruang)",
|
||||
"Play (Space)": "Main (Ruang)",
|
||||
"Skip forward 15 words": "Langkau depan 15 perkataan",
|
||||
"Forward 15 words (Shift+Right)": "Depan 15 perkataan (Shift+Kanan)",
|
||||
"Pause:": "Jeda:",
|
||||
"Decrease speed": "Kurangkan kelajuan",
|
||||
"Slower (Left/Down)": "Lebih perlahan (Kiri/Bawah)",
|
||||
"Current speed": "Kelajuan semasa",
|
||||
"Increase speed": "Tingkatkan kelajuan",
|
||||
"Faster (Right/Up)": "Lebih cepat (Kanan/Atas)",
|
||||
"Start RSVP Reading": "Mulakan Pembacaan RSVP",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "Kecerahan Skrin Sistem",
|
||||
"Page:": "Halaman:",
|
||||
"Page: {{number}}": "Halaman: {{number}}",
|
||||
"Annotation page number": "Nombor halaman anotasi"
|
||||
"Annotation page number": "Nombor halaman anotasi",
|
||||
"Translating...": "Menterjemah...",
|
||||
"Show Battery Percentage": "Tunjukkan Peratusan Bateri",
|
||||
"Hide Scrollbar": "Sembunyikan Bar Tatal",
|
||||
"Skip to last reading position": "Lompat ke posisi bacaan terakhir",
|
||||
"Show context": "Tunjukkan konteks",
|
||||
"Hide context": "Sembunyikan konteks",
|
||||
"Decrease font size": "Kecilkan saiz fon",
|
||||
"Increase font size": "Besarkan saiz fon",
|
||||
"Focus": "Fokus",
|
||||
"Theme color": "Warna tema",
|
||||
"Import failed": "Import gagal",
|
||||
"Available Formatters:": "Pemformat Tersedia:",
|
||||
"Format date": "Format tarikh",
|
||||
"Markdown block quote (> per line)": "Petikan blok Markdown (> setiap baris)",
|
||||
"Newlines to <br>": "Baris baharu ke <br>",
|
||||
"Change case": "Tukar huruf besar/kecil",
|
||||
"Trim whitespace": "Pangkas ruang kosong",
|
||||
"Truncate to n characters": "Potong kepada n aksara",
|
||||
"Replace text": "Ganti teks",
|
||||
"Fallback value": "Nilai lalai",
|
||||
"Get length": "Dapatkan panjang",
|
||||
"First/last element": "Elemen pertama/terakhir",
|
||||
"Join array": "Gabung tatasusunan",
|
||||
"Backup failed: {{error}}": "Sandaran gagal: {{error}}",
|
||||
"Select Backup": "Pilih Sandaran",
|
||||
"Restore failed: {{error}}": "Pemulihan gagal: {{error}}",
|
||||
"Backup & Restore": "Sandaran & Pemulihan",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Buat sandaran perpustakaan anda atau pulihkan daripada sandaran sebelumnya. Pemulihan akan digabungkan dengan perpustakaan semasa anda.",
|
||||
"Backup Library": "Sandarkan Perpustakaan",
|
||||
"Restore Library": "Pulihkan Perpustakaan",
|
||||
"Creating backup...": "Membuat sandaran...",
|
||||
"Restoring library...": "Memulihkan perpustakaan...",
|
||||
"{{current}} of {{total}} items": "{{current}} daripada {{total}} item",
|
||||
"Backup completed successfully!": "Sandaran berjaya!",
|
||||
"Restore completed successfully!": "Pemulihan berjaya!",
|
||||
"Your library has been saved to the selected location.": "Perpustakaan anda telah disimpan di lokasi yang dipilih.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} buku ditambah, {{updated}} buku dikemas kini.",
|
||||
"Operation failed": "Operasi gagal",
|
||||
"Loading library...": "Memuatkan perpustakaan...",
|
||||
"{{count}} books refreshed_other": "{{count}} buku dimuat semula",
|
||||
"Failed to refresh metadata": "Gagal memuat semula metadata",
|
||||
"Refresh Metadata": "Muat Semula Metadata",
|
||||
"Keyboard Shortcuts": "Pintasan Papan Kekunci",
|
||||
"View all keyboard shortcuts": "Lihat semua pintasan papan kekunci",
|
||||
"Switch Sidebar Tab": "Tukar tab bar sisi",
|
||||
"Toggle Notebook": "Tunjuk/sembunyikan buku nota",
|
||||
"Search in Book": "Cari dalam buku",
|
||||
"Toggle Scroll Mode": "Togol mod tatal",
|
||||
"Toggle Select Mode": "Togol mod pilih",
|
||||
"Toggle Bookmark": "Togol penanda buku",
|
||||
"Toggle Text to Speech": "Togol teks ke pertuturan",
|
||||
"Play / Pause TTS": "Main / Jeda TTS",
|
||||
"Toggle Paragraph Mode": "Togol mod perenggan",
|
||||
"Highlight Selection": "Serlahkan pilihan",
|
||||
"Underline Selection": "Garis bawah pilihan",
|
||||
"Annotate Selection": "Anotasi pilihan",
|
||||
"Search Selection": "Cari pilihan",
|
||||
"Copy Selection": "Salin pilihan",
|
||||
"Translate Selection": "Terjemah pilihan",
|
||||
"Dictionary Lookup": "Cari dalam kamus",
|
||||
"Wikipedia Lookup": "Cari di Wikipedia",
|
||||
"Read Aloud Selection": "Baca kuat pilihan",
|
||||
"Proofread Selection": "Semak pilihan",
|
||||
"Open Settings": "Buka tetapan",
|
||||
"Open Command Palette": "Buka palet arahan",
|
||||
"Show Keyboard Shortcuts": "Tunjukkan pintasan papan kekunci",
|
||||
"Open Books": "Buka buku",
|
||||
"Toggle Fullscreen": "Togol skrin penuh",
|
||||
"Close Window": "Tutup tetingkap",
|
||||
"Quit App": "Keluar dari aplikasi",
|
||||
"Go Left / Previous Page": "Ke kiri / Halaman sebelumnya",
|
||||
"Go Right / Next Page": "Ke kanan / Halaman seterusnya",
|
||||
"Go Up": "Ke atas",
|
||||
"Go Down": "Ke bawah",
|
||||
"Previous Chapter": "Bab sebelumnya",
|
||||
"Next Chapter": "Bab seterusnya",
|
||||
"Scroll Half Page Down": "Tatal separuh halaman ke bawah",
|
||||
"Scroll Half Page Up": "Tatal separuh halaman ke atas",
|
||||
"Save Note": "Simpan nota",
|
||||
"Single Section Scroll": "Tatal Satu Bahagian",
|
||||
"General": "Umum",
|
||||
"Text to Speech": "Teks ke Pertuturan",
|
||||
"Zoom": "Zum",
|
||||
"Window": "Tetingkap",
|
||||
"Your Bookshelf": "Rak Buku Anda",
|
||||
"TTS": "Teks ke Suara",
|
||||
"Media Info": "Info Media",
|
||||
"Update Frequency": "Kekerapan Kemas Kini",
|
||||
"Every Sentence": "Setiap Ayat",
|
||||
"Every Paragraph": "Setiap Perenggan",
|
||||
"Every Chapter": "Setiap Bab",
|
||||
"TTS Media Info Update Frequency": "Kekerapan Kemas Kini Info Media TTS",
|
||||
"Select reading speed": "Pilih kelajuan membaca",
|
||||
"Drag to seek": "Seret untuk mencari",
|
||||
"Punctuation Delay": "Kelewatan tanda baca",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Sila sahkan bahawa sambungan OPDS ini akan diproksi melalui pelayan Readest pada aplikasi web sebelum meneruskan.",
|
||||
"Custom Headers": "Pengepala Tersuai",
|
||||
"Custom Headers (optional)": "Pengepala Tersuai (pilihan)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Tambah satu pengepala setiap baris menggunakan \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Saya faham bahawa sambungan OPDS ini akan diproksi melalui pelayan Readest pada aplikasi web. Jika saya tidak mempercayai Readest dengan kelayakan atau pengepala ini, saya perlu menggunakan aplikasi natif.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Tidak dapat menyambung ke Hardcover. Sila semak sambungan rangkaian anda.",
|
||||
"Invalid Hardcover API token": "Token API Hardcover tidak sah",
|
||||
"Disconnected from Hardcover": "Diputuskan dari Hardcover",
|
||||
"Hardcover Settings": "Tetapan Hardcover",
|
||||
"Connected to Hardcover": "Disambungkan ke Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Sambungkan akaun Hardcover anda untuk menyegerakkan kemajuan bacaan dan nota.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Dapatkan token API anda dari hardcover.app → Tetapan → API.",
|
||||
"API Token": "Token API",
|
||||
"Paste your Hardcover API token": "Tampal token API Hardcover anda",
|
||||
"Hardcover sync enabled for this book": "Penyegerakan Hardcover diaktifkan untuk buku ini",
|
||||
"Hardcover sync disabled for this book": "Penyegerakan Hardcover dinyahaktifkan untuk buku ini",
|
||||
"Hardcover Sync": "Penyegerakan Hardcover",
|
||||
"Enable for This Book": "Aktifkan untuk Buku Ini",
|
||||
"Push Notes": "Hantar Nota",
|
||||
"Enable Hardcover sync for this book first.": "Aktifkan penyegerakan Hardcover untuk buku ini terlebih dahulu.",
|
||||
"No annotations or excerpts to sync for this book.": "Tiada anotasi atau petikan untuk disegerakkan bagi buku ini.",
|
||||
"Configure Hardcover in Settings first.": "Konfigurasikan Hardcover dalam Tetapan terlebih dahulu.",
|
||||
"No new Hardcover note changes to sync.": "Tiada perubahan nota Hardcover baharu untuk disegerakkan.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover disegerakkan: {{inserted}} baharu, {{updated}} dikemas kini, {{skipped}} tidak berubah",
|
||||
"Hardcover notes sync failed: {{error}}": "Penyegerakan nota Hardcover gagal: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Kemajuan bacaan disegerakkan ke Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Penyegerakan kemajuan Hardcover gagal: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Kekalkan pengecam sumber sedia ada"
|
||||
}
|
||||
|
||||
@@ -179,7 +179,6 @@
|
||||
"Portrait": "Portret",
|
||||
"Landscape": "Landschap",
|
||||
"Behavior": "Gedrag",
|
||||
"Continuous Scroll": "Doorlopend scrollen",
|
||||
"Volume Keys for Page Flip": "Volumetoetsen voor pagina omslaan",
|
||||
"Apply": "Toepassen",
|
||||
"Font": "Lettertype",
|
||||
@@ -206,7 +205,6 @@
|
||||
"TOC": "Inhoudsopgave",
|
||||
"Table of Contents": "Inhoudsopgave",
|
||||
"Sidebar": "Zijbalk",
|
||||
"TTS not supported for PDF": "TTS niet ondersteund voor PDF",
|
||||
"No Timeout": "Geen time-out",
|
||||
"{{value}} minute": "{{value}} minuut",
|
||||
"{{value}} minutes": "{{value}} minuten",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "Zoekopdracht wissen",
|
||||
"Clear search history": "Zoekgeschiedenis wissen",
|
||||
"Tap to Toggle Footer": "Tik om voettekst te wisselen",
|
||||
"Show Current Time": "Huidige tijd weergeven",
|
||||
"Use 24 Hour Clock": "24-uurs klok gebruike",
|
||||
"Show Current Battery Status": "Toon huidige batterijstatus",
|
||||
"Exported successfully": "Succesvol geëxporteerd",
|
||||
"Book exported successfully.": "Boek succesvol geëxporteerd.",
|
||||
"Failed to export the book.": "Exporteren van het boek mislukt.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "woorden",
|
||||
"{{time}} left": "{{time}} over",
|
||||
"Reading progress": "Leesvoortgang",
|
||||
"Click to seek": "Klik om te zoeken",
|
||||
"Skip back 15 words": "15 woorden terug",
|
||||
"Back 15 words (Shift+Left)": "15 woorden terug (Shift+Links)",
|
||||
"Pause (Space)": "Pauze (Spatie)",
|
||||
"Play (Space)": "Afspelen (Spatie)",
|
||||
"Skip forward 15 words": "15 woorden vooruit",
|
||||
"Forward 15 words (Shift+Right)": "15 woorden vooruit (Shift+Rechts)",
|
||||
"Pause:": "Pauze:",
|
||||
"Decrease speed": "Snelheid verlagen",
|
||||
"Slower (Left/Down)": "Langzamer (Links/Omlaag)",
|
||||
"Current speed": "Huidige snelheid",
|
||||
"Increase speed": "Snelheid verhogen",
|
||||
"Faster (Right/Up)": "Sneller (Rechts/Omhoog)",
|
||||
"Start RSVP Reading": "Start RSVP-lezen",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "Systeemhelderheid van het scherm",
|
||||
"Page:": "Pagina:",
|
||||
"Page: {{number}}": "Pagina: {{number}}",
|
||||
"Annotation page number": "Annotatiepaginanummer"
|
||||
"Annotation page number": "Annotatiepaginanummer",
|
||||
"Translating...": "Vertalen...",
|
||||
"Show Battery Percentage": "Batterijpercentage weergeven",
|
||||
"Hide Scrollbar": "Schuifbalk verbergen",
|
||||
"Skip to last reading position": "Ga naar laatste leespositie",
|
||||
"Show context": "Context tonen",
|
||||
"Hide context": "Context verbergen",
|
||||
"Decrease font size": "Lettergrootte verkleinen",
|
||||
"Increase font size": "Lettergrootte vergroten",
|
||||
"Focus": "Focus",
|
||||
"Theme color": "Themakleur",
|
||||
"Import failed": "Import mislukt",
|
||||
"Available Formatters:": "Beschikbare opmaak:",
|
||||
"Format date": "Datum opmaken",
|
||||
"Markdown block quote (> per line)": "Markdown-blokcitaat (> per regel)",
|
||||
"Newlines to <br>": "Regeleinden naar <br>",
|
||||
"Change case": "Hoofdlettergebruik wijzigen",
|
||||
"Trim whitespace": "Witruimte verwijderen",
|
||||
"Truncate to n characters": "Inkorten tot n tekens",
|
||||
"Replace text": "Tekst vervangen",
|
||||
"Fallback value": "Terugvalwaarde",
|
||||
"Get length": "Lengte ophalen",
|
||||
"First/last element": "Eerste/laatste element",
|
||||
"Join array": "Array samenvoegen",
|
||||
"Backup failed: {{error}}": "Back-up mislukt: {{error}}",
|
||||
"Select Backup": "Selecteer back-up",
|
||||
"Restore failed: {{error}}": "Herstel mislukt: {{error}}",
|
||||
"Backup & Restore": "Back-up & Herstel",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Maak een back-up van uw bibliotheek of herstel vanuit een eerdere back-up. Herstel wordt samengevoegd met uw huidige bibliotheek.",
|
||||
"Backup Library": "Bibliotheek back-uppen",
|
||||
"Restore Library": "Bibliotheek herstellen",
|
||||
"Creating backup...": "Back-up maken...",
|
||||
"Restoring library...": "Bibliotheek herstellen...",
|
||||
"{{current}} of {{total}} items": "{{current}} van {{total}} items",
|
||||
"Backup completed successfully!": "Back-up succesvol voltooid!",
|
||||
"Restore completed successfully!": "Herstel succesvol voltooid!",
|
||||
"Your library has been saved to the selected location.": "Uw bibliotheek is opgeslagen op de geselecteerde locatie.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} boeken toegevoegd, {{updated}} boeken bijgewerkt.",
|
||||
"Operation failed": "Bewerking mislukt",
|
||||
"Loading library...": "Bibliotheek laden...",
|
||||
"{{count}} books refreshed_one": "{{count}} boek vernieuwd",
|
||||
"{{count}} books refreshed_other": "{{count}} boeken vernieuwd",
|
||||
"Failed to refresh metadata": "Metadata vernieuwen mislukt",
|
||||
"Refresh Metadata": "Metadata vernieuwen",
|
||||
"Keyboard Shortcuts": "Sneltoetsen",
|
||||
"View all keyboard shortcuts": "Alle sneltoetsen bekijken",
|
||||
"Switch Sidebar Tab": "Zijbalktabblad wisselen",
|
||||
"Toggle Notebook": "Notitieboek tonen/verbergen",
|
||||
"Search in Book": "Zoeken in boek",
|
||||
"Toggle Scroll Mode": "Scrollmodus wisselen",
|
||||
"Toggle Select Mode": "Selectiemodus wisselen",
|
||||
"Toggle Bookmark": "Bladwijzer wisselen",
|
||||
"Toggle Text to Speech": "Tekst-naar-spraak wisselen",
|
||||
"Play / Pause TTS": "TTS afspelen / pauzeren",
|
||||
"Toggle Paragraph Mode": "Alineamodus wisselen",
|
||||
"Highlight Selection": "Selectie markeren",
|
||||
"Underline Selection": "Selectie onderstrepen",
|
||||
"Annotate Selection": "Selectie annoteren",
|
||||
"Search Selection": "Selectie zoeken",
|
||||
"Copy Selection": "Selectie kopiëren",
|
||||
"Translate Selection": "Selectie vertalen",
|
||||
"Dictionary Lookup": "Opzoeken in woordenboek",
|
||||
"Wikipedia Lookup": "Opzoeken op Wikipedia",
|
||||
"Read Aloud Selection": "Selectie voorlezen",
|
||||
"Proofread Selection": "Selectie proeflezen",
|
||||
"Open Settings": "Instellingen openen",
|
||||
"Open Command Palette": "Opdrachtenpalet openen",
|
||||
"Show Keyboard Shortcuts": "Sneltoetsen tonen",
|
||||
"Open Books": "Boeken openen",
|
||||
"Toggle Fullscreen": "Volledig scherm wisselen",
|
||||
"Close Window": "Venster sluiten",
|
||||
"Quit App": "App afsluiten",
|
||||
"Go Left / Previous Page": "Naar links / Vorige pagina",
|
||||
"Go Right / Next Page": "Naar rechts / Volgende pagina",
|
||||
"Go Up": "Omhoog",
|
||||
"Go Down": "Omlaag",
|
||||
"Previous Chapter": "Vorig hoofdstuk",
|
||||
"Next Chapter": "Volgend hoofdstuk",
|
||||
"Scroll Half Page Down": "Halve pagina naar beneden scrollen",
|
||||
"Scroll Half Page Up": "Halve pagina naar boven scrollen",
|
||||
"Save Note": "Notitie opslaan",
|
||||
"Single Section Scroll": "Scrollen per sectie",
|
||||
"General": "Algemeen",
|
||||
"Text to Speech": "Tekst naar spraak",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Venster",
|
||||
"Your Bookshelf": "Jouw boekenkast",
|
||||
"TTS": "Spraak",
|
||||
"Media Info": "Media-info",
|
||||
"Update Frequency": "Updatefrequentie",
|
||||
"Every Sentence": "Elke zin",
|
||||
"Every Paragraph": "Elke alinea",
|
||||
"Every Chapter": "Elk hoofdstuk",
|
||||
"TTS Media Info Update Frequency": "TTS media-info updatefrequentie",
|
||||
"Select reading speed": "Leessnelheid selecteren",
|
||||
"Drag to seek": "Sleep om te zoeken",
|
||||
"Punctuation Delay": "Leesteken-vertraging",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Bevestig dat deze OPDS-verbinding via Readest-servers in de webapp wordt doorgestuurd voordat u doorgaat.",
|
||||
"Custom Headers": "Aangepaste headers",
|
||||
"Custom Headers (optional)": "Aangepaste headers (optioneel)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Voeg één header per regel toe met \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Ik begrijp dat deze OPDS-verbinding via Readest-servers in de webapp wordt doorgestuurd. Als ik Readest niet vertrouw met deze inloggegevens of headers, moet ik de native app gebruiken.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Kan geen verbinding maken met Hardcover. Controleer uw netwerkverbinding.",
|
||||
"Invalid Hardcover API token": "Ongeldig Hardcover API-token",
|
||||
"Disconnected from Hardcover": "Verbinding met Hardcover verbroken",
|
||||
"Hardcover Settings": "Hardcover-instellingen",
|
||||
"Connected to Hardcover": "Verbonden met Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Verbind uw Hardcover-account om leesvoortgang en notities te synchroniseren.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Haal uw API-token op via hardcover.app → Instellingen → API.",
|
||||
"API Token": "API-token",
|
||||
"Paste your Hardcover API token": "Plak uw Hardcover API-token",
|
||||
"Hardcover sync enabled for this book": "Hardcover-synchronisatie ingeschakeld voor dit boek",
|
||||
"Hardcover sync disabled for this book": "Hardcover-synchronisatie uitgeschakeld voor dit boek",
|
||||
"Hardcover Sync": "Hardcover-synchronisatie",
|
||||
"Enable for This Book": "Inschakelen voor dit boek",
|
||||
"Push Notes": "Notities verzenden",
|
||||
"Enable Hardcover sync for this book first.": "Schakel eerst Hardcover-synchronisatie in voor dit boek.",
|
||||
"No annotations or excerpts to sync for this book.": "Geen annotaties of fragmenten om te synchroniseren voor dit boek.",
|
||||
"Configure Hardcover in Settings first.": "Configureer eerst Hardcover in de instellingen.",
|
||||
"No new Hardcover note changes to sync.": "Geen nieuwe Hardcover-notitiewijzigingen om te synchroniseren.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover gesynchroniseerd: {{inserted}} nieuw, {{updated}} bijgewerkt, {{skipped}} ongewijzigd",
|
||||
"Hardcover notes sync failed: {{error}}": "Synchronisatie van Hardcover-notities mislukt: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Leesvoortgang gesynchroniseerd met Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Synchronisatie van Hardcover-voortgang mislukt: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Bestaande bronidentificatie behouden"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Tryb pisania",
|
||||
"Your Library": "Twoja biblioteka",
|
||||
"TTS not supported for PDF": "TTS nie jest obsługiwane dla plików PDF",
|
||||
"Override Book Font": "Nadpisz czcionkę książki",
|
||||
"Apply to All Books": "Zastosuj do wszystkich książek",
|
||||
"Apply to This Book": "Zastosuj do tej książki",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Kierunek RTL",
|
||||
"Maximum Column Height": "Maksymalna wysokość kolumny",
|
||||
"Maximum Column Width": "Maksymalna szerokość kolumny",
|
||||
"Continuous Scroll": "Przewijanie ciągłe",
|
||||
"Fullscreen": "Pełny ekran",
|
||||
"No supported files found. Supported formats: {{formats}}": "Nie znaleziono obsługiwanych plików. Obsługiwane formaty: {{formats}}",
|
||||
"Drop to Import Books": "Upuść, aby zaimportować książki",
|
||||
@@ -852,6 +850,9 @@
|
||||
"Clear search": "Wyczyść wyszukiwanie",
|
||||
"Clear search history": "Wyczyść historię wyszukiwania",
|
||||
"Tap to Toggle Footer": "Dotknij, aby przełączyć stopkę",
|
||||
"Show Current Time": "Pokaż aktualny czas",
|
||||
"Use 24 Hour Clock": "Używaj formatu 24-godzinnego",
|
||||
"Show Current Battery Status": "Pokaż aktualny stan baterii",
|
||||
"Exported successfully": "Wyeksportowano pomyślnie",
|
||||
"Book exported successfully.": "Książka została wyeksportowana pomyślnie.",
|
||||
"Failed to export the book.": "Nie udało się wyeksportować książki.",
|
||||
@@ -1005,17 +1006,14 @@
|
||||
"words": "słowa",
|
||||
"{{time}} left": "pozostało {{time}}",
|
||||
"Reading progress": "Postęp czytania",
|
||||
"Click to seek": "Kliknij, aby wyszukać",
|
||||
"Skip back 15 words": "Cofnij o 15 słów",
|
||||
"Back 15 words (Shift+Left)": "Cofnij o 15 słów (Shift+Lewo)",
|
||||
"Pause (Space)": "Pauze (Spacja)",
|
||||
"Play (Space)": "Odtwarzaj (Spacja)",
|
||||
"Skip forward 15 words": "Naprzód o 15 słów",
|
||||
"Forward 15 words (Shift+Right)": "Naprzód o 15 słów (Shift+Prawo)",
|
||||
"Pause:": "Pauza:",
|
||||
"Decrease speed": "Zmniejsz prędkość",
|
||||
"Slower (Left/Down)": "Wolniej (Lewo/Dół)",
|
||||
"Current speed": "Aktualna prędkość",
|
||||
"Increase speed": "Zwiększ prędkość",
|
||||
"Faster (Right/Up)": "Szybciej (Prawo/Góra)",
|
||||
"Start RSVP Reading": "Uruchom czytanie RSVP",
|
||||
@@ -1083,5 +1081,132 @@
|
||||
"System Screen Brightness": "Jasność ekranu systemowego",
|
||||
"Page:": "Strona:",
|
||||
"Page: {{number}}": "Strona: {{number}}",
|
||||
"Annotation page number": "Numer strony adnotacji"
|
||||
"Annotation page number": "Numer strony adnotacji",
|
||||
"Translating...": "Tłumaczenie...",
|
||||
"Show Battery Percentage": "Pokaż procent baterii",
|
||||
"Hide Scrollbar": "Ukryj pasek przewijania",
|
||||
"Skip to last reading position": "Przejdź do ostatniej pozycji czytania",
|
||||
"Show context": "Pokaż kontekst",
|
||||
"Hide context": "Ukryj kontekst",
|
||||
"Decrease font size": "Zmniejsz rozmiar czcionki",
|
||||
"Increase font size": "Zwiększ rozmiar czcionki",
|
||||
"Focus": "Fokus",
|
||||
"Theme color": "Kolor motywu",
|
||||
"Import failed": "Import nie powiódł się",
|
||||
"Available Formatters:": "Dostępne formatery:",
|
||||
"Format date": "Formatuj datę",
|
||||
"Markdown block quote (> per line)": "Cytat blokowy Markdown (> na linię)",
|
||||
"Newlines to <br>": "Nowe linie na <br>",
|
||||
"Change case": "Zmień wielkość liter",
|
||||
"Trim whitespace": "Przytnij spacje",
|
||||
"Truncate to n characters": "Skróć do n znaków",
|
||||
"Replace text": "Zamień tekst",
|
||||
"Fallback value": "Wartość zastępcza",
|
||||
"Get length": "Pobierz długość",
|
||||
"First/last element": "Pierwszy/ostatni element",
|
||||
"Join array": "Połącz tablicę",
|
||||
"Backup failed: {{error}}": "Kopia zapasowa nie powiodła się: {{error}}",
|
||||
"Select Backup": "Wybierz kopię zapasową",
|
||||
"Restore failed: {{error}}": "Przywracanie nie powiodło się: {{error}}",
|
||||
"Backup & Restore": "Kopia zapasowa i przywracanie",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Utwórz kopię zapasową biblioteki lub przywróć z poprzedniej kopii. Przywracanie zostanie połączone z bieżącą biblioteką.",
|
||||
"Backup Library": "Kopia zapasowa biblioteki",
|
||||
"Restore Library": "Przywróć bibliotekę",
|
||||
"Creating backup...": "Tworzenie kopii zapasowej...",
|
||||
"Restoring library...": "Przywracanie biblioteki...",
|
||||
"{{current}} of {{total}} items": "{{current}} z {{total}} elementów",
|
||||
"Backup completed successfully!": "Kopia zapasowa ukończona!",
|
||||
"Restore completed successfully!": "Przywracanie ukończone!",
|
||||
"Your library has been saved to the selected location.": "Biblioteka została zapisana w wybranej lokalizacji.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Dodano {{added}} książek, zaktualizowano {{updated}} książek.",
|
||||
"Operation failed": "Operacja nie powiodła się",
|
||||
"Loading library...": "Wczytywanie biblioteki...",
|
||||
"{{count}} books refreshed_one": "Odświeżono {{count}} książkę",
|
||||
"{{count}} books refreshed_few": "Odświeżono {{count}} książki",
|
||||
"{{count}} books refreshed_many": "Odświeżono {{count}} książek",
|
||||
"{{count}} books refreshed_other": "Odświeżono {{count}} książek",
|
||||
"Failed to refresh metadata": "Nie udało się odświeżyć metadanych",
|
||||
"Refresh Metadata": "Odśwież metadane",
|
||||
"Keyboard Shortcuts": "Skróty klawiszowe",
|
||||
"View all keyboard shortcuts": "Zobacz wszystkie skróty klawiszowe",
|
||||
"Switch Sidebar Tab": "Przełącz kartę paska bocznego",
|
||||
"Toggle Notebook": "Pokaż/ukryj notatnik",
|
||||
"Search in Book": "Szukaj w książce",
|
||||
"Toggle Scroll Mode": "Przełącz tryb przewijania",
|
||||
"Toggle Select Mode": "Przełącz tryb zaznaczania",
|
||||
"Toggle Bookmark": "Przełącz zakładkę",
|
||||
"Toggle Text to Speech": "Przełącz tekst na mowę",
|
||||
"Play / Pause TTS": "Odtwórz / Wstrzymaj TTS",
|
||||
"Toggle Paragraph Mode": "Przełącz tryb akapitu",
|
||||
"Highlight Selection": "Podświetl zaznaczenie",
|
||||
"Underline Selection": "Podkreśl zaznaczenie",
|
||||
"Annotate Selection": "Dodaj adnotację do zaznaczenia",
|
||||
"Search Selection": "Wyszukaj zaznaczenie",
|
||||
"Copy Selection": "Kopiuj zaznaczenie",
|
||||
"Translate Selection": "Przetłumacz zaznaczenie",
|
||||
"Dictionary Lookup": "Wyszukaj w słowniku",
|
||||
"Wikipedia Lookup": "Wyszukaj w Wikipedii",
|
||||
"Read Aloud Selection": "Odczytaj zaznaczenie na głos",
|
||||
"Proofread Selection": "Sprawdź zaznaczenie",
|
||||
"Open Settings": "Otwórz ustawienia",
|
||||
"Open Command Palette": "Otwórz paletę poleceń",
|
||||
"Show Keyboard Shortcuts": "Pokaż skróty klawiszowe",
|
||||
"Open Books": "Otwórz książki",
|
||||
"Toggle Fullscreen": "Przełącz pełny ekran",
|
||||
"Close Window": "Zamknij okno",
|
||||
"Quit App": "Zamknij aplikację",
|
||||
"Go Left / Previous Page": "W lewo / Poprzednia strona",
|
||||
"Go Right / Next Page": "W prawo / Następna strona",
|
||||
"Go Up": "W górę",
|
||||
"Go Down": "W dół",
|
||||
"Previous Chapter": "Poprzedni rozdział",
|
||||
"Next Chapter": "Następny rozdział",
|
||||
"Scroll Half Page Down": "Przewiń o pół strony w dół",
|
||||
"Scroll Half Page Up": "Przewiń o pół strony w górę",
|
||||
"Save Note": "Zapisz notatkę",
|
||||
"Single Section Scroll": "Przewijanie pojedynczej sekcji",
|
||||
"General": "Ogólne",
|
||||
"Text to Speech": "Tekst na mowę",
|
||||
"Zoom": "Powiększenie",
|
||||
"Window": "Okno",
|
||||
"Your Bookshelf": "Twoja półka z książkami",
|
||||
"TTS": "Czytanie na głos",
|
||||
"Media Info": "Informacje o mediach",
|
||||
"Update Frequency": "Częstotliwość aktualizacji",
|
||||
"Every Sentence": "Co zdanie",
|
||||
"Every Paragraph": "Co akapit",
|
||||
"Every Chapter": "Co rozdział",
|
||||
"TTS Media Info Update Frequency": "Częstotliwość aktualizacji informacji o mediach TTS",
|
||||
"Select reading speed": "Wybierz prędkość czytania",
|
||||
"Drag to seek": "Przeciągnij, aby przewinąć",
|
||||
"Punctuation Delay": "Opóźnienie interpunkcji",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Potwierdź, że to połączenie OPDS będzie przekierowywane przez serwery Readest w aplikacji webowej, zanim przejdziesz dalej.",
|
||||
"Custom Headers": "Niestandardowe nagłówki",
|
||||
"Custom Headers (optional)": "Niestandardowe nagłówki (opcjonalnie)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Dodaj jeden nagłówek w każdym wierszu, używając formatu \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Rozumiem, że to połączenie OPDS będzie przekierowywane przez serwery Readest w aplikacji webowej. Jeśli nie ufam Readest w kwestii tych danych uwierzytelniających lub nagłówków, powinienem użyć aplikacji natywnej.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Nie można połączyć się z Hardcover. Sprawdź połączenie sieciowe.",
|
||||
"Invalid Hardcover API token": "Nieprawidłowy token API Hardcover",
|
||||
"Disconnected from Hardcover": "Rozłączono z Hardcover",
|
||||
"Hardcover Settings": "Ustawienia Hardcover",
|
||||
"Connected to Hardcover": "Połączono z Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Połącz swoje konto Hardcover, aby synchronizować postęp czytania i notatki.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Pobierz token API z hardcover.app → Ustawienia → API.",
|
||||
"API Token": "Token API",
|
||||
"Paste your Hardcover API token": "Wklej swój token API Hardcover",
|
||||
"Hardcover sync enabled for this book": "Synchronizacja Hardcover włączona dla tej książki",
|
||||
"Hardcover sync disabled for this book": "Synchronizacja Hardcover wyłączona dla tej książki",
|
||||
"Hardcover Sync": "Synchronizacja Hardcover",
|
||||
"Enable for This Book": "Włącz dla tej książki",
|
||||
"Push Notes": "Wyślij notatki",
|
||||
"Enable Hardcover sync for this book first.": "Najpierw włącz synchronizację Hardcover dla tej książki.",
|
||||
"No annotations or excerpts to sync for this book.": "Brak adnotacji lub fragmentów do zsynchronizowania dla tej książki.",
|
||||
"Configure Hardcover in Settings first.": "Najpierw skonfiguruj Hardcover w ustawieniach.",
|
||||
"No new Hardcover note changes to sync.": "Brak nowych zmian notatek Hardcover do zsynchronizowania.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover zsynchronizowano: {{inserted}} nowych, {{updated}} zaktualizowanych, {{skipped}} bez zmian",
|
||||
"Hardcover notes sync failed: {{error}}": "Synchronizacja notatek Hardcover nie powiodła się: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Postęp czytania zsynchronizowany z Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Synchronizacja postępu Hardcover nie powiodła się: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Zachowaj istniejący identyfikator źródła"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Wikipédia",
|
||||
"Writing Mode": "Modo de Escrita",
|
||||
"Your Library": "Sua Biblioteca",
|
||||
"TTS not supported for PDF": "TTS não suportado para PDF",
|
||||
"Override Book Font": "Sobrescrever Fonte do Livro",
|
||||
"Apply to All Books": "Aplicar a todos os livros",
|
||||
"Apply to This Book": "Aplicar a este livro",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Direção RTL",
|
||||
"Maximum Column Height": "Altura Máxima da Coluna",
|
||||
"Maximum Column Width": "Largura Máxima da Coluna",
|
||||
"Continuous Scroll": "Rolagem Contínua",
|
||||
"Fullscreen": "Tela Cheia",
|
||||
"No supported files found. Supported formats: {{formats}}": "Nenhum arquivo suportado encontrado. Formatos suportados: {{formats}}",
|
||||
"Drop to Import Books": "Arraste para Importar Livros",
|
||||
@@ -842,6 +840,9 @@
|
||||
"Clear search": "Limpar pesquisa",
|
||||
"Clear search history": "Limpar histórico de pesquisa",
|
||||
"Tap to Toggle Footer": "Toque para alternar rodapé",
|
||||
"Show Current Time": "Mostrar hora atual",
|
||||
"Use 24 Hour Clock": "Usar formato de 24 horas",
|
||||
"Show Current Battery Status": "Mostrar o estado atual da bateria",
|
||||
"Exported successfully": "Exportado com sucesso",
|
||||
"Book exported successfully.": "Livro exportado com sucesso.",
|
||||
"Failed to export the book.": "Falha ao exportar o livro.",
|
||||
@@ -993,17 +994,14 @@
|
||||
"words": "palavras",
|
||||
"{{time}} left": "{{time}} restante",
|
||||
"Reading progress": "Progresso de leitura",
|
||||
"Click to seek": "Clique para navegar",
|
||||
"Skip back 15 words": "Voltar 15 palavras",
|
||||
"Back 15 words (Shift+Left)": "Voltar 15 palavras (Shift+Esquerda)",
|
||||
"Pause (Space)": "Pausar (Espaço)",
|
||||
"Play (Space)": "Reproduzir (Espaço)",
|
||||
"Skip forward 15 words": "Avançar 15 palavras",
|
||||
"Forward 15 words (Shift+Right)": "Avançar 15 palavras (Shift+Direita)",
|
||||
"Pause:": "Pausa:",
|
||||
"Decrease speed": "Diminuir velocidade",
|
||||
"Slower (Left/Down)": "Mais lento (Esquerda/Baixo)",
|
||||
"Current speed": "Velocidade atual",
|
||||
"Increase speed": "Aumentar velocidade",
|
||||
"Faster (Right/Up)": "Mais rápido (Direita/Cima)",
|
||||
"Start RSVP Reading": "Iniciar leitura RSVP",
|
||||
@@ -1071,5 +1069,131 @@
|
||||
"System Screen Brightness": "Brilho da tela do sistema",
|
||||
"Page:": "Página:",
|
||||
"Page: {{number}}": "Página: {{number}}",
|
||||
"Annotation page number": "Número de página da anotação"
|
||||
"Annotation page number": "Número de página da anotação",
|
||||
"Translating...": "Traduzindo...",
|
||||
"Show Battery Percentage": "Mostrar porcentagem da bateria",
|
||||
"Hide Scrollbar": "Ocultar barra de rolagem",
|
||||
"Skip to last reading position": "Ir para a última posição de leitura",
|
||||
"Show context": "Mostrar contexto",
|
||||
"Hide context": "Ocultar contexto",
|
||||
"Decrease font size": "Diminuir tamanho da fonte",
|
||||
"Increase font size": "Aumentar tamanho da fonte",
|
||||
"Focus": "Foco",
|
||||
"Theme color": "Cor do tema",
|
||||
"Import failed": "Falha na importação",
|
||||
"Available Formatters:": "Formatadores disponíveis:",
|
||||
"Format date": "Formatar data",
|
||||
"Markdown block quote (> per line)": "Citação Markdown (> por linha)",
|
||||
"Newlines to <br>": "Quebras de linha para <br>",
|
||||
"Change case": "Alterar maiúsculas/minúsculas",
|
||||
"Trim whitespace": "Remover espaços",
|
||||
"Truncate to n characters": "Truncar para n caracteres",
|
||||
"Replace text": "Substituir texto",
|
||||
"Fallback value": "Valor padrão",
|
||||
"Get length": "Obter comprimento",
|
||||
"First/last element": "Primeiro/último elemento",
|
||||
"Join array": "Juntar array",
|
||||
"Backup failed: {{error}}": "Falha no backup: {{error}}",
|
||||
"Select Backup": "Selecionar backup",
|
||||
"Restore failed: {{error}}": "Falha na restauração: {{error}}",
|
||||
"Backup & Restore": "Backup e restauração",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Crie um backup da sua biblioteca ou restaure a partir de um backup anterior. A restauração será mesclada com sua biblioteca atual.",
|
||||
"Backup Library": "Fazer backup da biblioteca",
|
||||
"Restore Library": "Restaurar biblioteca",
|
||||
"Creating backup...": "Criando backup...",
|
||||
"Restoring library...": "Restaurando biblioteca...",
|
||||
"{{current}} of {{total}} items": "{{current}} de {{total}} itens",
|
||||
"Backup completed successfully!": "Backup concluído com sucesso!",
|
||||
"Restore completed successfully!": "Restauração concluída com sucesso!",
|
||||
"Your library has been saved to the selected location.": "Sua biblioteca foi salva no local selecionado.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} livros adicionados, {{updated}} livros atualizados.",
|
||||
"Operation failed": "Operação falhou",
|
||||
"Loading library...": "Carregando biblioteca...",
|
||||
"{{count}} books refreshed_one": "{{count}} livro atualizado",
|
||||
"{{count}} books refreshed_many": "{{count}} livros atualizados",
|
||||
"{{count}} books refreshed_other": "{{count}} livros atualizados",
|
||||
"Failed to refresh metadata": "Falha ao atualizar metadados",
|
||||
"Refresh Metadata": "Atualizar metadados",
|
||||
"Keyboard Shortcuts": "Atalhos de teclado",
|
||||
"View all keyboard shortcuts": "Ver todos os atalhos de teclado",
|
||||
"Switch Sidebar Tab": "Alternar aba da barra lateral",
|
||||
"Toggle Notebook": "Mostrar/ocultar caderno",
|
||||
"Search in Book": "Pesquisar no livro",
|
||||
"Toggle Scroll Mode": "Alternar modo rolagem",
|
||||
"Toggle Select Mode": "Alternar modo seleção",
|
||||
"Toggle Bookmark": "Alternar marcador",
|
||||
"Toggle Text to Speech": "Alternar texto para fala",
|
||||
"Play / Pause TTS": "Reproduzir / Pausar TTS",
|
||||
"Toggle Paragraph Mode": "Alternar modo parágrafo",
|
||||
"Highlight Selection": "Destacar seleção",
|
||||
"Underline Selection": "Sublinhar seleção",
|
||||
"Annotate Selection": "Anotar seleção",
|
||||
"Search Selection": "Pesquisar seleção",
|
||||
"Copy Selection": "Copiar seleção",
|
||||
"Translate Selection": "Traduzir seleção",
|
||||
"Dictionary Lookup": "Pesquisar no dicionário",
|
||||
"Wikipedia Lookup": "Pesquisar na Wikipédia",
|
||||
"Read Aloud Selection": "Ler seleção em voz alta",
|
||||
"Proofread Selection": "Revisar seleção",
|
||||
"Open Settings": "Abrir configurações",
|
||||
"Open Command Palette": "Abrir paleta de comandos",
|
||||
"Show Keyboard Shortcuts": "Mostrar atalhos de teclado",
|
||||
"Open Books": "Abrir livros",
|
||||
"Toggle Fullscreen": "Alternar tela cheia",
|
||||
"Close Window": "Fechar janela",
|
||||
"Quit App": "Sair do aplicativo",
|
||||
"Go Left / Previous Page": "Ir para a esquerda / Página anterior",
|
||||
"Go Right / Next Page": "Ir para a direita / Próxima página",
|
||||
"Go Up": "Ir para cima",
|
||||
"Go Down": "Ir para baixo",
|
||||
"Previous Chapter": "Capítulo anterior",
|
||||
"Next Chapter": "Próximo capítulo",
|
||||
"Scroll Half Page Down": "Rolar meia página para baixo",
|
||||
"Scroll Half Page Up": "Rolar meia página para cima",
|
||||
"Save Note": "Salvar nota",
|
||||
"Single Section Scroll": "Rolagem por seção",
|
||||
"General": "Geral",
|
||||
"Text to Speech": "Texto para fala",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Janela",
|
||||
"Your Bookshelf": "Sua estante",
|
||||
"TTS": "Leitura em voz alta",
|
||||
"Media Info": "Info de mídia",
|
||||
"Update Frequency": "Frequência de atualização",
|
||||
"Every Sentence": "A cada frase",
|
||||
"Every Paragraph": "A cada parágrafo",
|
||||
"Every Chapter": "A cada capítulo",
|
||||
"TTS Media Info Update Frequency": "Frequência de atualização de info de mídia TTS",
|
||||
"Select reading speed": "Selecionar velocidade de leitura",
|
||||
"Drag to seek": "Arraste para procurar",
|
||||
"Punctuation Delay": "Atraso de pontuação",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Confirme que esta conexão OPDS será encaminhada através dos servidores Readest no aplicativo web antes de continuar.",
|
||||
"Custom Headers": "Cabeçalhos personalizados",
|
||||
"Custom Headers (optional)": "Cabeçalhos personalizados (opcional)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Adicione um cabeçalho por linha usando \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Entendo que esta conexão OPDS será encaminhada através dos servidores Readest no aplicativo web. Se não confio no Readest com essas credenciais ou cabeçalhos, devo usar o aplicativo nativo.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Não foi possível conectar ao Hardcover. Verifique sua conexão de rede.",
|
||||
"Invalid Hardcover API token": "Token de API do Hardcover inválido",
|
||||
"Disconnected from Hardcover": "Desconectado do Hardcover",
|
||||
"Hardcover Settings": "Configurações do Hardcover",
|
||||
"Connected to Hardcover": "Conectado ao Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Conecte sua conta Hardcover para sincronizar o progresso de leitura e notas.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Obtenha seu token de API em hardcover.app → Configurações → API.",
|
||||
"API Token": "Token de API",
|
||||
"Paste your Hardcover API token": "Cole seu token de API do Hardcover",
|
||||
"Hardcover sync enabled for this book": "Sincronização do Hardcover ativada para este livro",
|
||||
"Hardcover sync disabled for this book": "Sincronização do Hardcover desativada para este livro",
|
||||
"Hardcover Sync": "Sincronização do Hardcover",
|
||||
"Enable for This Book": "Ativar para este livro",
|
||||
"Push Notes": "Enviar notas",
|
||||
"Enable Hardcover sync for this book first.": "Ative primeiro a sincronização do Hardcover para este livro.",
|
||||
"No annotations or excerpts to sync for this book.": "Não há anotações ou trechos para sincronizar neste livro.",
|
||||
"Configure Hardcover in Settings first.": "Configure o Hardcover nas configurações primeiro.",
|
||||
"No new Hardcover note changes to sync.": "Não há novas alterações de notas do Hardcover para sincronizar.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover sincronizado: {{inserted}} novos, {{updated}} atualizados, {{skipped}} inalterados",
|
||||
"Hardcover notes sync failed: {{error}}": "Falha na sincronização de notas do Hardcover: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Progresso de leitura sincronizado com o Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Falha na sincronização de progresso do Hardcover: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Manter o identificador de origem existente"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Википедия",
|
||||
"Writing Mode": "Режим записи",
|
||||
"Your Library": "Ваша библиотека",
|
||||
"TTS not supported for PDF": "TTS не поддерживается для PDF",
|
||||
"Override Book Font": "Переопределить шрифт книги",
|
||||
"Apply to All Books": "Применить ко всем книгам",
|
||||
"Apply to This Book": "Применить к этой книге",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Направление RTL",
|
||||
"Maximum Column Height": "Максимальная высота колонки",
|
||||
"Maximum Column Width": "Максимальная ширина колонки",
|
||||
"Continuous Scroll": "Непрерывная прокрутка",
|
||||
"Fullscreen": "Полноэкранный режим",
|
||||
"No supported files found. Supported formats: {{formats}}": "Поддерживаемые файлы не найдены. Поддерживаемые форматы: {{formats}}",
|
||||
"Drop to Import Books": "Перетащите сюда книги для импорта",
|
||||
@@ -852,6 +850,9 @@
|
||||
"Clear search": "Очистить поиск",
|
||||
"Clear search history": "Очистить историю поиска",
|
||||
"Tap to Toggle Footer": "Нажмите для переключения нижнего колонтитула",
|
||||
"Show Current Time": "Показать текущее время",
|
||||
"Use 24 Hour Clock": "Использовать 24-часовой формат",
|
||||
"Show Current Battery Status": "Показать текущее состояние батареи",
|
||||
"Exported successfully": "Успешно экспортировано",
|
||||
"Book exported successfully.": "Книга успешно экспортирована.",
|
||||
"Failed to export the book.": "Не удалось экспортировать книгу.",
|
||||
@@ -1005,17 +1006,14 @@
|
||||
"words": "слов",
|
||||
"{{time}} left": "осталось {{time}}",
|
||||
"Reading progress": "Прогресс чтения",
|
||||
"Click to seek": "Нажмите для поиска",
|
||||
"Skip back 15 words": "Назад на 15 слов",
|
||||
"Back 15 words (Shift+Left)": "Назад на 15 слов (Shift+Влево)",
|
||||
"Pause (Space)": "Пауза (Пробел)",
|
||||
"Play (Space)": "Воспроизведение (Пробел)",
|
||||
"Skip forward 15 words": "Вперед на 15 слов",
|
||||
"Forward 15 words (Shift+Right)": "Вперед на 15 слов (Shift+Вправо)",
|
||||
"Pause:": "Пауза:",
|
||||
"Decrease speed": "Уменьшить скорость",
|
||||
"Slower (Left/Down)": "Медленнее (Влево/Вниз)",
|
||||
"Current speed": "Текущая скорость",
|
||||
"Increase speed": "Увеличить скорость",
|
||||
"Faster (Right/Up)": "Быстрее (Вправо/Вверх)",
|
||||
"Start RSVP Reading": "Начать чтение RSVP",
|
||||
@@ -1083,5 +1081,132 @@
|
||||
"System Screen Brightness": "Систেমная яркость экрана",
|
||||
"Page:": "Страница:",
|
||||
"Page: {{number}}": "Страница: {{number}}",
|
||||
"Annotation page number": "Номер страницы аннотации"
|
||||
"Annotation page number": "Номер страницы аннотации",
|
||||
"Translating...": "Перевод...",
|
||||
"Show Battery Percentage": "Показывать процент заряда батареи",
|
||||
"Hide Scrollbar": "Скрыть полосу прокрутки",
|
||||
"Skip to last reading position": "Перейти к последней позиции чтения",
|
||||
"Show context": "Показать контекст",
|
||||
"Hide context": "Скрыть контекст",
|
||||
"Decrease font size": "Уменьшить размер шрифта",
|
||||
"Increase font size": "Увеличить размер шрифта",
|
||||
"Focus": "Фокус",
|
||||
"Theme color": "Цвет темы",
|
||||
"Import failed": "Ошибка импорта",
|
||||
"Available Formatters:": "Доступные форматеры:",
|
||||
"Format date": "Форматировать дату",
|
||||
"Markdown block quote (> per line)": "Цитата Markdown (> для каждой строки)",
|
||||
"Newlines to <br>": "Переносы строк в <br>",
|
||||
"Change case": "Изменить регистр",
|
||||
"Trim whitespace": "Удалить пробелы",
|
||||
"Truncate to n characters": "Обрезать до n символов",
|
||||
"Replace text": "Заменить текст",
|
||||
"Fallback value": "Значение по умолчанию",
|
||||
"Get length": "Получить длину",
|
||||
"First/last element": "Первый/последний элемент",
|
||||
"Join array": "Объединить массив",
|
||||
"Backup failed: {{error}}": "Ошибка резервного копирования: {{error}}",
|
||||
"Select Backup": "Выбрать резервную копию",
|
||||
"Restore failed: {{error}}": "Ошибка восстановления: {{error}}",
|
||||
"Backup & Restore": "Резервное копирование и восстановление",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Создайте резервную копию библиотеки или восстановите из предыдущей копии. Восстановление будет объединено с текущей библиотекой.",
|
||||
"Backup Library": "Резервная копия библиотеки",
|
||||
"Restore Library": "Восстановить библиотеку",
|
||||
"Creating backup...": "Создание резервной копии...",
|
||||
"Restoring library...": "Восстановление библиотеки...",
|
||||
"{{current}} of {{total}} items": "{{current}} из {{total}} элементов",
|
||||
"Backup completed successfully!": "Резервное копирование завершено!",
|
||||
"Restore completed successfully!": "Восстановление завершено!",
|
||||
"Your library has been saved to the selected location.": "Ваша библиотека сохранена в выбранном месте.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Добавлено {{added}} книг, обновлено {{updated}} книг.",
|
||||
"Operation failed": "Операция не удалась",
|
||||
"Loading library...": "Загрузка библиотеки...",
|
||||
"{{count}} books refreshed_one": "Обновлена {{count}} книга",
|
||||
"{{count}} books refreshed_few": "Обновлено {{count}} книги",
|
||||
"{{count}} books refreshed_many": "Обновлено {{count}} книг",
|
||||
"{{count}} books refreshed_other": "Обновлено {{count}} книг",
|
||||
"Failed to refresh metadata": "Не удалось обновить метаданные",
|
||||
"Refresh Metadata": "Обновить метаданные",
|
||||
"Keyboard Shortcuts": "Сочетания клавиш",
|
||||
"View all keyboard shortcuts": "Показать все сочетания клавиш",
|
||||
"Switch Sidebar Tab": "Переключить вкладку боковой панели",
|
||||
"Toggle Notebook": "Показать/скрыть записную книжку",
|
||||
"Search in Book": "Поиск в книге",
|
||||
"Toggle Scroll Mode": "Переключить режим прокрутки",
|
||||
"Toggle Select Mode": "Переключить режим выделения",
|
||||
"Toggle Bookmark": "Переключить закладку",
|
||||
"Toggle Text to Speech": "Переключить озвучивание текста",
|
||||
"Play / Pause TTS": "Воспроизвести / Пауза TTS",
|
||||
"Toggle Paragraph Mode": "Переключить режим абзаца",
|
||||
"Highlight Selection": "Выделить выбранное",
|
||||
"Underline Selection": "Подчеркнуть выбранное",
|
||||
"Annotate Selection": "Добавить примечание к выбранному",
|
||||
"Search Selection": "Искать выбранное",
|
||||
"Copy Selection": "Копировать выбранное",
|
||||
"Translate Selection": "Перевести выбранное",
|
||||
"Dictionary Lookup": "Поиск в словаре",
|
||||
"Wikipedia Lookup": "Поиск в Википедии",
|
||||
"Read Aloud Selection": "Прочитать выбранное вслух",
|
||||
"Proofread Selection": "Проверить выбранное",
|
||||
"Open Settings": "Открыть настройки",
|
||||
"Open Command Palette": "Открыть палитру команд",
|
||||
"Show Keyboard Shortcuts": "Показать сочетания клавиш",
|
||||
"Open Books": "Открыть книги",
|
||||
"Toggle Fullscreen": "Переключить полноэкранный режим",
|
||||
"Close Window": "Закрыть окно",
|
||||
"Quit App": "Выйти из приложения",
|
||||
"Go Left / Previous Page": "Влево / Предыдущая страница",
|
||||
"Go Right / Next Page": "Вправо / Следующая страница",
|
||||
"Go Up": "Вверх",
|
||||
"Go Down": "Вниз",
|
||||
"Previous Chapter": "Предыдущая глава",
|
||||
"Next Chapter": "Следующая глава",
|
||||
"Scroll Half Page Down": "Прокрутить на полстраницы вниз",
|
||||
"Scroll Half Page Up": "Прокрутить на полстраницы вверх",
|
||||
"Save Note": "Сохранить заметку",
|
||||
"Single Section Scroll": "Прокрутка одного раздела",
|
||||
"General": "Общие",
|
||||
"Text to Speech": "Озвучивание",
|
||||
"Zoom": "Масштаб",
|
||||
"Window": "Окно",
|
||||
"Your Bookshelf": "Ваша книжная полка",
|
||||
"TTS": "Озвучивание",
|
||||
"Media Info": "Медиа-информация",
|
||||
"Update Frequency": "Частота обновления",
|
||||
"Every Sentence": "Каждое предложение",
|
||||
"Every Paragraph": "Каждый абзац",
|
||||
"Every Chapter": "Каждая глава",
|
||||
"TTS Media Info Update Frequency": "Частота обновления медиа-информации TTS",
|
||||
"Select reading speed": "Выберите скорость чтения",
|
||||
"Drag to seek": "Перетащите для перемотки",
|
||||
"Punctuation Delay": "Задержка на знаках препинания",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Подтвердите, что это OPDS-соединение будет проксироваться через серверы Readest в веб-приложении, прежде чем продолжить.",
|
||||
"Custom Headers": "Пользовательские заголовки",
|
||||
"Custom Headers (optional)": "Пользовательские заголовки (необязательно)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Добавляйте по одному заголовку на строку в формате \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Я понимаю, что это OPDS-соединение будет проксироваться через серверы Readest в веб-приложении. Если я не доверяю Readest свои учётные данные или заголовки, мне следует использовать нативное приложение.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Не удалось подключиться к Hardcover. Проверьте сетевое подключение.",
|
||||
"Invalid Hardcover API token": "Недействительный API-токен Hardcover",
|
||||
"Disconnected from Hardcover": "Отключено от Hardcover",
|
||||
"Hardcover Settings": "Настройки Hardcover",
|
||||
"Connected to Hardcover": "Подключено к Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Подключите свой аккаунт Hardcover для синхронизации прогресса чтения и заметок.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Получите API-токен на hardcover.app → Настройки → API.",
|
||||
"API Token": "API-токен",
|
||||
"Paste your Hardcover API token": "Вставьте ваш API-токен Hardcover",
|
||||
"Hardcover sync enabled for this book": "Синхронизация Hardcover включена для этой книги",
|
||||
"Hardcover sync disabled for this book": "Синхронизация Hardcover отключена для этой книги",
|
||||
"Hardcover Sync": "Синхронизация Hardcover",
|
||||
"Enable for This Book": "Включить для этой книги",
|
||||
"Push Notes": "Отправить заметки",
|
||||
"Enable Hardcover sync for this book first.": "Сначала включите синхронизацию Hardcover для этой книги.",
|
||||
"No annotations or excerpts to sync for this book.": "Нет аннотаций или выдержек для синхронизации для этой книги.",
|
||||
"Configure Hardcover in Settings first.": "Сначала настройте Hardcover в параметрах.",
|
||||
"No new Hardcover note changes to sync.": "Нет новых изменений заметок Hardcover для синхронизации.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover синхронизирован: {{inserted}} новых, {{updated}} обновлённых, {{skipped}} без изменений",
|
||||
"Hardcover notes sync failed: {{error}}": "Ошибка синхронизации заметок Hardcover: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Прогресс чтения синхронизирован с Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Ошибка синхронизации прогресса Hardcover: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Сохранить существующий идентификатор источника"
|
||||
}
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
"Auto": "ස්වයංක්රීය",
|
||||
"Scroll": "අනුචලනය",
|
||||
"Scrolled Mode": "අනුචලන ආකාරය",
|
||||
"Continuous Scroll": "අඛණ්ඩ අනුචලනය",
|
||||
"Overlap Pixels": "අතිච්ඡාදන පික්සල්",
|
||||
"Disable Double Click": "ද්විත්ව ක්ලික් අබල කරන්න",
|
||||
"Volume Keys for Page Flip": "පිටු පෙරැළීම සඳහා ශබ්ද යතුර",
|
||||
@@ -319,7 +318,6 @@
|
||||
"Table of Contents": "සූචිය",
|
||||
"Sidebar": "පැති තීරුව",
|
||||
"Disable Translation": "පරිවර්තනය අබල කරන්න",
|
||||
"TTS not supported for PDF": "PDF සඳහා TTS සහාය නොදක්වයි",
|
||||
"TTS not supported for this document": "මෙම ලේඛනය සඳහා TTS සහාය නොදක්වයි",
|
||||
"No Timeout": "කාල සීමාවක් නැත",
|
||||
"{{value}} minute": "මිනිත්තු {{value}}",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "සෙවුම හිස් කරන්න",
|
||||
"Clear search history": "සෙවුම් ඉතිහාසය හිස් කරන්න",
|
||||
"Tap to Toggle Footer": "පාදකය ටොගල් කිරීමට තට්ටු කරන්න",
|
||||
"Show Current Time": "වත්මන් වේලාව පෙන්වන්න",
|
||||
"Use 24 Hour Clock": "පැය 24 ඔරලෝසුව භාවිතා කරන්න",
|
||||
"Show Current Battery Status": "වත්මන් බැටරි තත්ත්වය පෙන්වන්න",
|
||||
"Exported successfully": "සාර්ථකව අපනයනය කරන ලදී",
|
||||
"Book exported successfully.": "පොත සාර්ථකව අපනයනය කරන ලදී.",
|
||||
"Failed to export the book.": "පොත අපනයනය කිරීමට අසමත් විය.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "වචන",
|
||||
"{{time}} left": "{{time}} ඉතිරිව ඇත",
|
||||
"Reading progress": "කියවීමේ ප්රගතිය",
|
||||
"Click to seek": "සොයා බැලීමට ක්ලික් කරන්න",
|
||||
"Skip back 15 words": "වචන 15ක් පසුපසට",
|
||||
"Back 15 words (Shift+Left)": "වචන 15ක් පසුපසට (Shift+Left)",
|
||||
"Pause (Space)": "විරාමය (Space)",
|
||||
"Play (Space)": "ධාවනය (Space)",
|
||||
"Skip forward 15 words": "වචන 15ක් ඉදිරියට",
|
||||
"Forward 15 words (Shift+Right)": "වචන 15ක් ඉදිරියට (Shift+Right)",
|
||||
"Pause:": "විරාමය:",
|
||||
"Decrease speed": "වේගය අඩු කරන්න",
|
||||
"Slower (Left/Down)": "මන්දගාමී (Left/Down)",
|
||||
"Current speed": "වත්මන් වේගය",
|
||||
"Increase speed": "වේගය වැඩි කරන්න",
|
||||
"Faster (Right/Up)": "වේගවත් (Right/Up)",
|
||||
"Start RSVP Reading": "RSVP කියවීම ආරම්භ කරන්න",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "පද්ධති තිර දීප්තිය",
|
||||
"Page:": "පිටුව:",
|
||||
"Page: {{number}}": "පිටුව: {{number}}",
|
||||
"Annotation page number": "විවරණ පිටු අංකය"
|
||||
"Annotation page number": "විවරණ පිටු අංකය",
|
||||
"Translating...": "පරිවර්තනය වෙමින් පවතී...",
|
||||
"Show Battery Percentage": "බැටරි ප්රතිශතය පෙන්වන්න",
|
||||
"Hide Scrollbar": "අනුචලන තීරුව සඟවන්න",
|
||||
"Skip to last reading position": "අවසන් කියවීමේ ස්ථානයට යන්න",
|
||||
"Show context": "සන්දර්භය පෙන්වන්න",
|
||||
"Hide context": "සන්දර්භය සඟවන්න",
|
||||
"Decrease font size": "අකුරු ප්රමාණය අඩු කරන්න",
|
||||
"Increase font size": "අකුරු ප්රමාණය වැඩි කරන්න",
|
||||
"Focus": "අවධානය",
|
||||
"Theme color": "තේමා වර්ණය",
|
||||
"Import failed": "ආයාත කිරීම අසාර්ථකයි",
|
||||
"Available Formatters:": "ලබාගත හැකි ආකෘතිකරණ:",
|
||||
"Format date": "දිනය ආකෘතිකරණය",
|
||||
"Markdown block quote (> per line)": "Markdown බ්ලොක් උද්ධෘතය (පේළියකට >)",
|
||||
"Newlines to <br>": "නව පේළි <br> වෙත",
|
||||
"Change case": "අකුරු ප්රමාණය වෙනස් කරන්න",
|
||||
"Trim whitespace": "හිස්තැන් කපන්න",
|
||||
"Truncate to n characters": "n අක්ෂර දක්වා කෙටි කරන්න",
|
||||
"Replace text": "පෙළ ප්රතිස්ථාපනය",
|
||||
"Fallback value": "විකල්ප අගය",
|
||||
"Get length": "දිග ලබාගන්න",
|
||||
"First/last element": "පළමු/අවසාන මූලද්රව්යය",
|
||||
"Join array": "අරාව සම්බන්ධ කරන්න",
|
||||
"Backup failed: {{error}}": "උපස්ථ කිරීම අසාර්ථකයි: {{error}}",
|
||||
"Select Backup": "උපස්ථයක් තෝරන්න",
|
||||
"Restore failed: {{error}}": "ප්රතිසාධනය අසාර්ථකයි: {{error}}",
|
||||
"Backup & Restore": "උපස්ථ සහ ප්රතිසාධනය",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "ඔබේ පුස්තකාලයේ උපස්ථයක් සාදන්න හෝ පෙර උපස්ථයකින් ප්රතිසාධනය කරන්න. ප්රතිසාධනය ඔබේ වත්මන් පුස්තකාලය සමග ඒකාබද්ධ වේ.",
|
||||
"Backup Library": "පුස්තකාලය උපස්ථ කරන්න",
|
||||
"Restore Library": "පුස්තකාලය ප්රතිසාධනය කරන්න",
|
||||
"Creating backup...": "උපස්ථය සාදමින්...",
|
||||
"Restoring library...": "පුස්තකාලය ප්රතිසාධනය කරමින්...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} අයිතම",
|
||||
"Backup completed successfully!": "උපස්ථය සාර්ථකව සම්පූර්ණයි!",
|
||||
"Restore completed successfully!": "ප්රතිසාධනය සාර්ථකව සම්පූර්ණයි!",
|
||||
"Your library has been saved to the selected location.": "ඔබේ පුස්තකාලය තෝරාගත් ස්ථානයේ සුරැකිණි.",
|
||||
"{{added}} books added, {{updated}} books updated.": "පොත් {{added}}ක් එකතු විය, {{updated}}ක් යාවත්කාලීන විය.",
|
||||
"Operation failed": "මෙහෙයුම අසාර්ථකයි",
|
||||
"Loading library...": "පුස්තකාලය පූරණය වෙමින්...",
|
||||
"{{count}} books refreshed_one": "පොත් {{count}}ක් යාවත්කාලීන විය",
|
||||
"{{count}} books refreshed_other": "පොත් {{count}}ක් යාවත්කාලීන විය",
|
||||
"Failed to refresh metadata": "පාරදත්ත යාවත්කාලීන කිරීම අසාර්ථකයි",
|
||||
"Refresh Metadata": "පාරදත්ත යාවත්කාලීන කරන්න",
|
||||
"Keyboard Shortcuts": "යතුරුපුවරු කෙටිමං",
|
||||
"View all keyboard shortcuts": "සියලුම යතුරුපුවරු කෙටිමං බලන්න",
|
||||
"Switch Sidebar Tab": "පැති තීරු ටැබය මාරු කරන්න",
|
||||
"Toggle Notebook": "සටහන් පොත පෙන්වන්න/සඟවන්න",
|
||||
"Search in Book": "පොතේ සොයන්න",
|
||||
"Toggle Scroll Mode": "අනුචලන ප්රකාරය මාරු කරන්න",
|
||||
"Toggle Select Mode": "තේරීම් ප්රකාරය මාරු කරන්න",
|
||||
"Toggle Bookmark": "පිටු සලකුණ මාරු කරන්න",
|
||||
"Toggle Text to Speech": "පෙළ-කථනය මාරු කරන්න",
|
||||
"Play / Pause TTS": "TTS ධාවනය / විරාමය",
|
||||
"Toggle Paragraph Mode": "ඡේද ප්රකාරය මාරු කරන්න",
|
||||
"Highlight Selection": "තේරීම උද්දීපනය කරන්න",
|
||||
"Underline Selection": "තේරීමට යටින් ඉරක් අදින්න",
|
||||
"Annotate Selection": "තේරීමට සටහනක් එක් කරන්න",
|
||||
"Search Selection": "තේරීම සොයන්න",
|
||||
"Copy Selection": "තේරීම පිටපත් කරන්න",
|
||||
"Translate Selection": "තේරීම පරිවර්තනය කරන්න",
|
||||
"Dictionary Lookup": "ශබ්දකෝෂයේ සොයන්න",
|
||||
"Wikipedia Lookup": "විකිපීඩියාවේ සොයන්න",
|
||||
"Read Aloud Selection": "තේරීම ශබ්ද නගා කියවන්න",
|
||||
"Proofread Selection": "තේරීම සෝදුපත් කරන්න",
|
||||
"Open Settings": "සැකසුම් විවෘත කරන්න",
|
||||
"Open Command Palette": "විධාන පැලට් එක විවෘත කරන්න",
|
||||
"Show Keyboard Shortcuts": "යතුරුපුවරු කෙටිමං පෙන්වන්න",
|
||||
"Open Books": "පොත් විවෘත කරන්න",
|
||||
"Toggle Fullscreen": "පූර්ණ තිරය මාරු කරන්න",
|
||||
"Close Window": "කවුළුව වසන්න",
|
||||
"Quit App": "යෙදුමෙන් පිටවන්න",
|
||||
"Go Left / Previous Page": "වමට / පෙර පිටුව",
|
||||
"Go Right / Next Page": "දකුණට / ඊළඟ පිටුව",
|
||||
"Go Up": "ඉහළට",
|
||||
"Go Down": "පහළට",
|
||||
"Previous Chapter": "පෙර පරිච්ඡේදය",
|
||||
"Next Chapter": "ඊළඟ පරිච්ඡේදය",
|
||||
"Scroll Half Page Down": "අඩ පිටුවක් පහළට අනුචලනය",
|
||||
"Scroll Half Page Up": "අඩ පිටුවක් ඉහළට අනුචලනය",
|
||||
"Save Note": "සටහන සුරකින්න",
|
||||
"Single Section Scroll": "තනි කොටස අනුචලනය",
|
||||
"General": "සාමාන්ය",
|
||||
"Text to Speech": "පෙළ කථනය",
|
||||
"Zoom": "විශාලනය",
|
||||
"Window": "කවුළුව",
|
||||
"Your Bookshelf": "ඔබේ පොත් රාක්කය",
|
||||
"TTS": "කියවීම",
|
||||
"Media Info": "මාධ්ය තොරතුරු",
|
||||
"Update Frequency": "යාවත්කාලීන සංඛ්යාතය",
|
||||
"Every Sentence": "සෑම වාක්යයක්ම",
|
||||
"Every Paragraph": "සෑම ඡේදයක්ම",
|
||||
"Every Chapter": "සෑම පරිච්ඡේදයක්ම",
|
||||
"TTS Media Info Update Frequency": "TTS මාධ්ය තොරතුරු යාවත්කාලීන සංඛ්යාතය",
|
||||
"Select reading speed": "කියවීමේ වේගය තෝරන්න",
|
||||
"Drag to seek": "සෙවීමට ඇදගන්න",
|
||||
"Punctuation Delay": "විරාම ලකුණු ප්රමාදය",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "ඉදිරියට යාමට පෙර මෙම OPDS සම්බන්ධතාවය වෙබ් යෙදුමේ Readest සේවාදායක හරහා ප්රොක්සි කරන බව තහවුරු කරන්න.",
|
||||
"Custom Headers": "අභිරුචි ශීර්ෂ",
|
||||
"Custom Headers (optional)": "අභිරුචි ශීර්ෂ (විකල්ප)",
|
||||
"Add one header per line using \"Header-Name: value\".": "\"Header-Name: value\" භාවිතයෙන් පේළියකට එක ශීර්ෂයක් එක් කරන්න.",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "මෙම OPDS සම්බන්ධතාවය වෙබ් යෙදුමේ Readest සේවාදායක හරහා ප්රොක්සි කරන බව මට තේරෙනවා. මෙම අක්තපත්ර හෝ ශීර්ෂ සමඟ Readest විශ්වාස නොකරන්නේ නම්, මම ස්වදේශීය යෙදුම භාවිතා කළ යුතුය.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcover වෙත සම්බන්ධ විය නොහැක. ඔබේ ජාල සම්බන්ධතාවය පරීක්ෂා කරන්න.",
|
||||
"Invalid Hardcover API token": "අවලංගු Hardcover API ටෝකනය",
|
||||
"Disconnected from Hardcover": "Hardcover වෙතින් විසන්ධි විය",
|
||||
"Hardcover Settings": "Hardcover සැකසීම්",
|
||||
"Connected to Hardcover": "Hardcover වෙත සම්බන්ධයි",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "කියවීමේ ප්රගතිය සහ සටහන් සමමුහුර්ත කිරීමට ඔබේ Hardcover ගිණුම සම්බන්ධ කරන්න.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "hardcover.app → සැකසීම් → API වෙතින් ඔබේ API ටෝකනය ලබාගන්න.",
|
||||
"API Token": "API ටෝකනය",
|
||||
"Paste your Hardcover API token": "ඔබේ Hardcover API ටෝකනය අලවන්න",
|
||||
"Hardcover sync enabled for this book": "මෙම පොත සඳහා Hardcover සමමුහුර්තකරණය සක්රීයයි",
|
||||
"Hardcover sync disabled for this book": "මෙම පොත සඳහා Hardcover සමමුහුර්තකරණය අක්රීයයි",
|
||||
"Hardcover Sync": "Hardcover සමමුහුර්තකරණය",
|
||||
"Enable for This Book": "මෙම පොත සඳහා සක්රීය කරන්න",
|
||||
"Push Notes": "සටහන් යවන්න",
|
||||
"Enable Hardcover sync for this book first.": "පළමුව මෙම පොත සඳහා Hardcover සමමුහුර්තකරණය සක්රීය කරන්න.",
|
||||
"No annotations or excerpts to sync for this book.": "මෙම පොත සඳහා සමමුහුර්ත කිරීමට සටහන් හෝ උපුටා ගැනීම් නැත.",
|
||||
"Configure Hardcover in Settings first.": "පළමුව සැකසීම් තුළ Hardcover වින්යාස කරන්න.",
|
||||
"No new Hardcover note changes to sync.": "සමමුහුර්ත කිරීමට නව Hardcover සටහන් වෙනස්කම් නැත.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover සමමුහුර්ත විය: {{inserted}} නව, {{updated}} යාවත්කාලීන, {{skipped}} වෙනස් නොවූ",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover සටහන් සමමුහුර්තකරණය අසාර්ථකයි: {{error}}",
|
||||
"Reading progress synced to Hardcover": "කියවීමේ ප්රගතිය Hardcover වෙත සමමුහුර්ත විය",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover ප්රගති සමමුහුර්තකරණය අසාර්ථකයි: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "පවතින මූලාශ්ර හඳුනාගැනීම තබාගන්න"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -278,7 +278,6 @@
|
||||
"Next Paragraph": "Nästa stycke",
|
||||
"Read Aloud": "Läs upp",
|
||||
"Ready to read aloud": "Redo att läsa upp",
|
||||
"TTS not supported for PDF": "Uppläsning stöds inte för PDF",
|
||||
"TTS not supported for this document": "Uppläsning stöds inte för detta dokument",
|
||||
"No Timeout": "Ingen timeout",
|
||||
"{{value}} minute": "{{value}} minut",
|
||||
@@ -447,7 +446,6 @@
|
||||
"Enable Highlighting": "Aktivera markering",
|
||||
"Code Language": "Kodspråk",
|
||||
"Scroll": "Rullning",
|
||||
"Continuous Scroll": "Kontinuerlig rullning",
|
||||
"Overlap Pixels": "Överlappande pixlar",
|
||||
"Disable Double Click": "Inaktivera dubbelklick",
|
||||
"Volume Keys for Page Flip": "Volymknappar för sidbyte",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "Rensa sökning",
|
||||
"Clear search history": "Rensa sökhistorik",
|
||||
"Tap to Toggle Footer": "Tryck för att växla sidfot",
|
||||
"Show Current Time": "Visa aktuell tid",
|
||||
"Use 24 Hour Clock": "Använd 24-timmarsklocka",
|
||||
"Show Current Battery Status": "Visa aktuell batteristatus",
|
||||
"Exported successfully": "Exporterad",
|
||||
"Book exported successfully.": "Boken har exporterats.",
|
||||
"Failed to export the book.": "Det gick inte att exportera boken.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "ord",
|
||||
"{{time}} left": "{{time}} kvar",
|
||||
"Reading progress": "Läsningens framsteg",
|
||||
"Click to seek": "Klicka för att söka",
|
||||
"Skip back 15 words": "Hoppa bakåt 15 ord",
|
||||
"Back 15 words (Shift+Left)": "Bakåt 15 ord (Shift+Vänster)",
|
||||
"Pause (Space)": "Pausa (Mellanslag)",
|
||||
"Play (Space)": "Spela (Mellanslag)",
|
||||
"Skip forward 15 words": "Hoppa framåt 15 ord",
|
||||
"Forward 15 words (Shift+Right)": "Framåt 15 ord (Shift+Höger)",
|
||||
"Pause:": "Paus:",
|
||||
"Decrease speed": "Sänk hastigheten",
|
||||
"Slower (Left/Down)": "Långsammare (Vänster/Nedåt)",
|
||||
"Current speed": "Nuvarande hastighet",
|
||||
"Increase speed": "Höj hastigheten",
|
||||
"Faster (Right/Up)": "Snabbare (Höger/Uppåt)",
|
||||
"Start RSVP Reading": "Starta RSVP-läsning",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "Systemets skärmljusstyrka",
|
||||
"Page:": "Sida:",
|
||||
"Page: {{number}}": "Sida: {{number}}",
|
||||
"Annotation page number": "Annoteringssidnummer"
|
||||
"Annotation page number": "Annoteringssidnummer",
|
||||
"Translating...": "Översätter...",
|
||||
"Show Battery Percentage": "Visa batteriprocent",
|
||||
"Hide Scrollbar": "Dölj rullningslist",
|
||||
"Skip to last reading position": "Hoppa till senaste läsposition",
|
||||
"Show context": "Visa kontext",
|
||||
"Hide context": "Dölj kontext",
|
||||
"Decrease font size": "Minska teckenstorlek",
|
||||
"Increase font size": "Öka teckenstorlek",
|
||||
"Focus": "Fokus",
|
||||
"Theme color": "Temafärg",
|
||||
"Import failed": "Importen misslyckades",
|
||||
"Available Formatters:": "Tillgängliga formaterare:",
|
||||
"Format date": "Formatera datum",
|
||||
"Markdown block quote (> per line)": "Markdown-blockcitat (> per rad)",
|
||||
"Newlines to <br>": "Radbrytningar till <br>",
|
||||
"Change case": "Ändra skiftläge",
|
||||
"Trim whitespace": "Ta bort blanksteg",
|
||||
"Truncate to n characters": "Korta till n tecken",
|
||||
"Replace text": "Ersätt text",
|
||||
"Fallback value": "Standardvärde",
|
||||
"Get length": "Hämta längd",
|
||||
"First/last element": "Första/sista elementet",
|
||||
"Join array": "Sammanfoga array",
|
||||
"Backup failed: {{error}}": "Säkerhetskopiering misslyckades: {{error}}",
|
||||
"Select Backup": "Välj säkerhetskopia",
|
||||
"Restore failed: {{error}}": "Återställning misslyckades: {{error}}",
|
||||
"Backup & Restore": "Säkerhetskopiera & Återställ",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Skapa en säkerhetskopia av ditt bibliotek eller återställ från en tidigare kopia. Återställningen slås samman med ditt nuvarande bibliotek.",
|
||||
"Backup Library": "Säkerhetskopiera bibliotek",
|
||||
"Restore Library": "Återställ bibliotek",
|
||||
"Creating backup...": "Skapar säkerhetskopia...",
|
||||
"Restoring library...": "Återställer bibliotek...",
|
||||
"{{current}} of {{total}} items": "{{current}} av {{total}} objekt",
|
||||
"Backup completed successfully!": "Säkerhetskopieringen slutförd!",
|
||||
"Restore completed successfully!": "Återställningen slutförd!",
|
||||
"Your library has been saved to the selected location.": "Ditt bibliotek har sparats på den valda platsen.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} böcker tillagda, {{updated}} böcker uppdaterade.",
|
||||
"Operation failed": "Åtgärden misslyckades",
|
||||
"Loading library...": "Laddar bibliotek...",
|
||||
"{{count}} books refreshed_one": "{{count}} bok uppdaterad",
|
||||
"{{count}} books refreshed_other": "{{count}} böcker uppdaterade",
|
||||
"Failed to refresh metadata": "Kunde inte uppdatera metadata",
|
||||
"Refresh Metadata": "Uppdatera metadata",
|
||||
"Keyboard Shortcuts": "Tangentbordsgenvägar",
|
||||
"View all keyboard shortcuts": "Visa alla tangentbordsgenvägar",
|
||||
"Switch Sidebar Tab": "Byt flik i sidofältet",
|
||||
"Toggle Notebook": "Visa/dölj anteckningsbok",
|
||||
"Search in Book": "Sök i boken",
|
||||
"Toggle Scroll Mode": "Växla rullningsläge",
|
||||
"Toggle Select Mode": "Växla markeringsläge",
|
||||
"Toggle Bookmark": "Växla bokmärke",
|
||||
"Toggle Text to Speech": "Växla text-till-tal",
|
||||
"Play / Pause TTS": "Spela upp / Pausa TTS",
|
||||
"Toggle Paragraph Mode": "Växla styckeläge",
|
||||
"Highlight Selection": "Markera urval",
|
||||
"Underline Selection": "Stryka under urval",
|
||||
"Annotate Selection": "Kommentera urval",
|
||||
"Search Selection": "Sök urval",
|
||||
"Copy Selection": "Kopiera urval",
|
||||
"Translate Selection": "Översätt urval",
|
||||
"Dictionary Lookup": "Slå upp i ordbok",
|
||||
"Wikipedia Lookup": "Slå upp på Wikipedia",
|
||||
"Read Aloud Selection": "Läs upp urval",
|
||||
"Proofread Selection": "Korrekturläs urval",
|
||||
"Open Settings": "Öppna inställningar",
|
||||
"Open Command Palette": "Öppna kommandopaletten",
|
||||
"Show Keyboard Shortcuts": "Visa tangentbordsgenvägar",
|
||||
"Open Books": "Öppna böcker",
|
||||
"Toggle Fullscreen": "Växla helskärm",
|
||||
"Close Window": "Stäng fönster",
|
||||
"Quit App": "Avsluta appen",
|
||||
"Go Left / Previous Page": "Vänster / Föregående sida",
|
||||
"Go Right / Next Page": "Höger / Nästa sida",
|
||||
"Go Up": "Uppåt",
|
||||
"Go Down": "Nedåt",
|
||||
"Previous Chapter": "Föregående kapitel",
|
||||
"Next Chapter": "Nästa kapitel",
|
||||
"Scroll Half Page Down": "Rulla en halv sida nedåt",
|
||||
"Scroll Half Page Up": "Rulla en halv sida uppåt",
|
||||
"Save Note": "Spara anteckning",
|
||||
"Single Section Scroll": "Rullning per avsnitt",
|
||||
"General": "Allmänt",
|
||||
"Text to Speech": "Text till tal",
|
||||
"Zoom": "Zoom",
|
||||
"Window": "Fönster",
|
||||
"Your Bookshelf": "Din bokhylla",
|
||||
"TTS": "Uppläsning",
|
||||
"Media Info": "Medieinformation",
|
||||
"Update Frequency": "Uppdateringsfrekvens",
|
||||
"Every Sentence": "Varje mening",
|
||||
"Every Paragraph": "Varje stycke",
|
||||
"Every Chapter": "Varje kapitel",
|
||||
"TTS Media Info Update Frequency": "Uppdateringsfrekvens för TTS-medieinformation",
|
||||
"Select reading speed": "Välj läshastighet",
|
||||
"Drag to seek": "Dra för att söka",
|
||||
"Punctuation Delay": "Skiljeteckenfördröjning",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Bekräfta att denna OPDS-anslutning kommer att dirigeras via Readest-servrar i webbappen innan du fortsätter.",
|
||||
"Custom Headers": "Anpassade rubriker",
|
||||
"Custom Headers (optional)": "Anpassade rubriker (valfritt)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Lägg till en rubrik per rad med formatet \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Jag förstår att denna OPDS-anslutning kommer att dirigeras via Readest-servrar i webbappen. Om jag inte litar på Readest med dessa autentiseringsuppgifter eller rubriker bör jag använda den inbyggda appen istället.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Kan inte ansluta till Hardcover. Kontrollera din nätverksanslutning.",
|
||||
"Invalid Hardcover API token": "Ogiltig Hardcover API-token",
|
||||
"Disconnected from Hardcover": "Frånkopplad från Hardcover",
|
||||
"Hardcover Settings": "Hardcover-inställningar",
|
||||
"Connected to Hardcover": "Ansluten till Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Anslut ditt Hardcover-konto för att synkronisera läsframsteg och anteckningar.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Hämta din API-token från hardcover.app → Inställningar → API.",
|
||||
"API Token": "API-token",
|
||||
"Paste your Hardcover API token": "Klistra in din Hardcover API-token",
|
||||
"Hardcover sync enabled for this book": "Hardcover-synkronisering aktiverad för den här boken",
|
||||
"Hardcover sync disabled for this book": "Hardcover-synkronisering inaktiverad för den här boken",
|
||||
"Hardcover Sync": "Hardcover-synkronisering",
|
||||
"Enable for This Book": "Aktivera för den här boken",
|
||||
"Push Notes": "Skicka anteckningar",
|
||||
"Enable Hardcover sync for this book first.": "Aktivera först Hardcover-synkronisering för den här boken.",
|
||||
"No annotations or excerpts to sync for this book.": "Inga anteckningar eller utdrag att synkronisera för den här boken.",
|
||||
"Configure Hardcover in Settings first.": "Konfigurera först Hardcover i inställningarna.",
|
||||
"No new Hardcover note changes to sync.": "Inga nya ändringar av Hardcover-anteckningar att synkronisera.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover synkroniserat: {{inserted}} nya, {{updated}} uppdaterade, {{skipped}} oförändrade",
|
||||
"Hardcover notes sync failed: {{error}}": "Synkronisering av Hardcover-anteckningar misslyckades: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Läsframsteg synkroniserat med Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Synkronisering av Hardcover-framsteg misslyckades: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Behåll befintlig källidentifierare"
|
||||
}
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
"Auto": "தானியங்கி",
|
||||
"Scroll": "உருட்டவும்",
|
||||
"Scrolled Mode": "உருட்டல் பயன்முறை",
|
||||
"Continuous Scroll": "தொடர்ச்சியான உருட்டல்",
|
||||
"Overlap Pixels": "ஒன்றுடன் ஒன்று பிக்சல்கள்",
|
||||
"Disable Double Click": "இரட்டை கிளிக்கை முடக்கவும்",
|
||||
"Volume Keys for Page Flip": "பக்கம் புரட்டுவதற்கு ஒலி விசைகள்",
|
||||
@@ -319,7 +318,6 @@
|
||||
"Table of Contents": "உள்ளடக்கம்",
|
||||
"Sidebar": "பக்கப்பட்டி",
|
||||
"Disable Translation": "மொழிபெயர்ப்பை முடக்கவும்",
|
||||
"TTS not supported for PDF": "PDF க்கு TTS ஆதரிக்கப்படவில்லை",
|
||||
"TTS not supported for this document": "இந்த ஆவணத்திற்கு TTS ஆதரிக்கப்படவில்லை",
|
||||
"No Timeout": "நேர வரம்பு இல்லை",
|
||||
"{{value}} minute": "{{value}} நிமிடம்",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "தேடலை அழி",
|
||||
"Clear search history": "தேடல் வரலாற்றை அழி",
|
||||
"Tap to Toggle Footer": "அடிக்குறிப்பை மாற்ற தட்டவும்",
|
||||
"Show Current Time": "தற்போதைய நேரத்தைக் காண்பி",
|
||||
"Use 24 Hour Clock": "24 மணிநேர கடிகாரத்தைப் பயன்படுத்து",
|
||||
"Show Current Battery Status": "தற்போதைய பேட்டரி நிலையைக் காண்பி",
|
||||
"Exported successfully": "ஏற்றுமதி வெற்றி",
|
||||
"Book exported successfully.": "புத்தகம் வெற்றிகரமாக ஏற்றுமதி செய்யப்பட்டது.",
|
||||
"Failed to export the book.": "புத்தகத்தை ஏற்றுமதி செய்ய இயலவில்லை.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "வார்த்தைகள்",
|
||||
"{{time}} left": "{{time}} மீதமுள்ளது",
|
||||
"Reading progress": "வாசிப்பு முன்னேற்றம்",
|
||||
"Click to seek": "தேட கிளிக் செய்க",
|
||||
"Skip back 15 words": "15 வார்த்தைகள் பின்னால் செல்",
|
||||
"Back 15 words (Shift+Left)": "15 வார்த்தைகள் பின்னால் செல் (Shift+Left)",
|
||||
"Pause (Space)": "நிறுத்து (Space)",
|
||||
"Play (Space)": "இயக்கு (Space)",
|
||||
"Skip forward 15 words": "15 வார்த்தைகள் முன்னால் செல்",
|
||||
"Forward 15 words (Shift+Right)": "15 வார்த்தைகள் முன்னால் செல் (Shift+Right)",
|
||||
"Pause:": "நிறுத்து:",
|
||||
"Decrease speed": "வேகத்தைக் குறை",
|
||||
"Slower (Left/Down)": "மெதுவாக (Left/Down)",
|
||||
"Current speed": "தற்போதைய வேகம்",
|
||||
"Increase speed": "வேகத்தை அதிகரி",
|
||||
"Faster (Right/Up)": "வேகமாக (Right/Up)",
|
||||
"Start RSVP Reading": "RSVP வாசிப்பைத் தொடங்கு",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "கணினித் திரை பிரகாசம்",
|
||||
"Page:": "பக்கம்:",
|
||||
"Page: {{number}}": "பக்கம்: {{number}}",
|
||||
"Annotation page number": "சிறுகுறிப்புப் பக்க எண்"
|
||||
"Annotation page number": "சிறுகுறிப்புப் பக்க எண்",
|
||||
"Translating...": "மொழிபெயர்க்கிறது...",
|
||||
"Show Battery Percentage": "பேட்டரி சதவீதத்தைக் காண்பி",
|
||||
"Hide Scrollbar": "உருள்பட்டையை மறை",
|
||||
"Skip to last reading position": "கடைசி வாசிப்பு நிலைக்குச் செல்",
|
||||
"Show context": "சூழலைக் காட்டு",
|
||||
"Hide context": "சூழலை மறை",
|
||||
"Decrease font size": "எழுத்துரு அளவைக் குறை",
|
||||
"Increase font size": "எழுத்துரு அளவை அதிகரி",
|
||||
"Focus": "குவியம்",
|
||||
"Theme color": "தீம் நிறம்",
|
||||
"Import failed": "இறக்குமதி தோல்வி",
|
||||
"Available Formatters:": "கிடைக்கும் வடிவமைப்பிகள்:",
|
||||
"Format date": "தேதி வடிவமைப்பு",
|
||||
"Markdown block quote (> per line)": "Markdown மேற்கோள் தொகுதி (ஒவ்வொரு வரிக்கும் >)",
|
||||
"Newlines to <br>": "புதிய வரிகளை <br> ஆக",
|
||||
"Change case": "எழுத்து வகை மாற்றம்",
|
||||
"Trim whitespace": "வெற்றிடங்களை நீக்கு",
|
||||
"Truncate to n characters": "n எழுத்துகளுக்கு சுருக்கு",
|
||||
"Replace text": "உரையை மாற்று",
|
||||
"Fallback value": "இயல்புநிலை மதிப்பு",
|
||||
"Get length": "நீளத்தைப் பெறு",
|
||||
"First/last element": "முதல்/கடைசி உறுப்பு",
|
||||
"Join array": "அணியை இணை",
|
||||
"Backup failed: {{error}}": "காப்புப்பிரதி தோல்வி: {{error}}",
|
||||
"Select Backup": "காப்புப்பிரதியைத் தேர்ந்தெடுக்கவும்",
|
||||
"Restore failed: {{error}}": "மீட்டமைப்பு தோல்வி: {{error}}",
|
||||
"Backup & Restore": "காப்புப்பிரதி & மீட்டமைப்பு",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "உங்கள் நூலகத்தின் காப்புப்பிரதியை உருவாக்கவும் அல்லது முந்தைய காப்புப்பிரதியிலிருந்து மீட்டமைக்கவும். மீட்டமைப்பு உங்கள் தற்போதைய நூலகத்துடன் இணைக்கப்படும்.",
|
||||
"Backup Library": "நூலகத்தை காப்புப்பிரதி எடு",
|
||||
"Restore Library": "நூலகத்தை மீட்டமை",
|
||||
"Creating backup...": "காப்புப்பிரதி உருவாக்கப்படுகிறது...",
|
||||
"Restoring library...": "நூலகம் மீட்டமைக்கப்படுகிறது...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} உருப்படிகள்",
|
||||
"Backup completed successfully!": "காப்புப்பிரதி வெற்றிகரமாக முடிந்தது!",
|
||||
"Restore completed successfully!": "மீட்டமைப்பு வெற்றிகரமாக முடிந்தது!",
|
||||
"Your library has been saved to the selected location.": "உங்கள் நூலகம் தேர்ந்தெடுக்கப்பட்ட இடத்தில் சேமிக்கப்பட்டது.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} புத்தகங்கள் சேர்க்கப்பட்டன, {{updated}} புத்தகங்கள் புதுப்பிக்கப்பட்டன.",
|
||||
"Operation failed": "செயல்பாடு தோல்வி",
|
||||
"Loading library...": "நூலகம் ஏற்றப்படுகிறது...",
|
||||
"{{count}} books refreshed_one": "{{count}} புத்தகம் புதுப்பிக்கப்பட்டது",
|
||||
"{{count}} books refreshed_other": "{{count}} புத்தகங்கள் புதுப்பிக்கப்பட்டன",
|
||||
"Failed to refresh metadata": "மெட்டாடேட்டா புதுப்பிப்பு தோல்வி",
|
||||
"Refresh Metadata": "மெட்டாடேட்டா புதுப்பிக்கவும்",
|
||||
"Keyboard Shortcuts": "விசைப்பலகை குறுக்குவழிகள்",
|
||||
"View all keyboard shortcuts": "அனைத்து விசைப்பலகை குறுக்குவழிகளையும் காண்க",
|
||||
"Switch Sidebar Tab": "பக்கப்பட்டி தாவலை மாற்றுக",
|
||||
"Toggle Notebook": "குறிப்பேட்டைக் காட்டு/மறை",
|
||||
"Search in Book": "புத்தகத்தில் தேடுக",
|
||||
"Toggle Scroll Mode": "உருள் பயன்முறையை மாற்றுக",
|
||||
"Toggle Select Mode": "தேர்வு பயன்முறையை மாற்றுக",
|
||||
"Toggle Bookmark": "புக்மார்க்கை மாற்றுக",
|
||||
"Toggle Text to Speech": "உரையிலிருந்து பேச்சுக்கு மாற்றுக",
|
||||
"Play / Pause TTS": "TTS இயக்கு / இடைநிறுத்து",
|
||||
"Toggle Paragraph Mode": "பத்தி பயன்முறையை மாற்றுக",
|
||||
"Highlight Selection": "தேர்வை சிறப்பிக்கவும்",
|
||||
"Underline Selection": "தேர்வுக்கு அடிக்கோடிடுக",
|
||||
"Annotate Selection": "தேர்வுக்கு குறிப்பு சேர்க்கவும்",
|
||||
"Search Selection": "தேர்வைத் தேடுக",
|
||||
"Copy Selection": "தேர்வை நகலெடுக்கவும்",
|
||||
"Translate Selection": "தேர்வை மொழிபெயர்க்கவும்",
|
||||
"Dictionary Lookup": "அகராதியில் தேடுக",
|
||||
"Wikipedia Lookup": "விக்கிப்பீடியாவில் தேடுக",
|
||||
"Read Aloud Selection": "தேர்வை சத்தமாக படிக்கவும்",
|
||||
"Proofread Selection": "தேர்வை சரிபார்க்கவும்",
|
||||
"Open Settings": "அமைப்புகளைத் திறக்கவும்",
|
||||
"Open Command Palette": "கட்டளை தட்டை திறக்கவும்",
|
||||
"Show Keyboard Shortcuts": "விசைப்பலகை குறுக்குவழிகளைக் காட்டுக",
|
||||
"Open Books": "புத்தகங்களைத் திறக்கவும்",
|
||||
"Toggle Fullscreen": "முழுத்திரையை மாற்றுக",
|
||||
"Close Window": "சாளரத்தை மூடுக",
|
||||
"Quit App": "பயன்பாட்டிலிருந்து வெளியேறுக",
|
||||
"Go Left / Previous Page": "இடது / முந்தைய பக்கம்",
|
||||
"Go Right / Next Page": "வலது / அடுத்த பக்கம்",
|
||||
"Go Up": "மேலே",
|
||||
"Go Down": "கீழே",
|
||||
"Previous Chapter": "முந்தைய அத்தியாயம்",
|
||||
"Next Chapter": "அடுத்த அத்தியாயம்",
|
||||
"Scroll Half Page Down": "அரை பக்கம் கீழே உருட்டுக",
|
||||
"Scroll Half Page Up": "அரை பக்கம் மேலே உருட்டுக",
|
||||
"Save Note": "குறிப்பைச் சேமிக்கவும்",
|
||||
"Single Section Scroll": "ஒற்றைப் பகுதி உருட்டல்",
|
||||
"General": "பொது",
|
||||
"Text to Speech": "உரையிலிருந்து பேச்சு",
|
||||
"Zoom": "பெரிதாக்கு",
|
||||
"Window": "சாளரம்",
|
||||
"Your Bookshelf": "உங்கள் புத்தக அலமாரி",
|
||||
"TTS": "உரை வாசிப்பு",
|
||||
"Media Info": "ஊடக தகவல்",
|
||||
"Update Frequency": "புதுப்பிப்பு அதிர்வெண்",
|
||||
"Every Sentence": "ஒவ்வொரு வாக்கியமும்",
|
||||
"Every Paragraph": "ஒவ்வொரு பத்தியும்",
|
||||
"Every Chapter": "ஒவ்வொரு அத்தியாயமும்",
|
||||
"TTS Media Info Update Frequency": "TTS ஊடக தகவல் புதுப்பிப்பு அதிர்வெண்",
|
||||
"Select reading speed": "படிக்கும் வேகத்தைத் தேர்ந்தெடுக்கவும்",
|
||||
"Drag to seek": "தேட இழுக்கவும்",
|
||||
"Punctuation Delay": "நிறுத்தக்குறி தாமதம்",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "தொடர்வதற்கு முன், இந்த OPDS இணைப்பு வலை பயன்பாட்டில் Readest சேவையகங்கள் வழியாக ப்ராக்ஸி செய்யப்படும் என்பதை உறுதிப்படுத்தவும்.",
|
||||
"Custom Headers": "தனிப்பயன் தலைப்புகள்",
|
||||
"Custom Headers (optional)": "தனிப்பயன் தலைப்புகள் (விரும்பினால்)",
|
||||
"Add one header per line using \"Header-Name: value\".": "\"Header-Name: value\" வடிவமைப்பைப் பயன்படுத்தி ஒவ்வொரு வரியிலும் ஒரு தலைப்பைச் சேர்க்கவும்.",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "இந்த OPDS இணைப்பு வலை பயன்பாட்டில் Readest சேவையகங்கள் வழியாக ப்ராக்ஸி செய்யப்படும் என்பதை நான் புரிந்துகொள்கிறேன். இந்த சான்றுகள் அல்லது தலைப்புகளுடன் Readest-ஐ நம்பவில்லை என்றால், நான் நேட்டிவ் பயன்பாட்டைப் பயன்படுத்த வேண்டும்.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcover உடன் இணைக்க முடியவில்லை. உங்கள் நெட்வொர்க் இணைப்பைச் சரிபார்க்கவும்.",
|
||||
"Invalid Hardcover API token": "தவறான Hardcover API டோக்கன்",
|
||||
"Disconnected from Hardcover": "Hardcover இலிருந்து துண்டிக்கப்பட்டது",
|
||||
"Hardcover Settings": "Hardcover அமைப்புகள்",
|
||||
"Connected to Hardcover": "Hardcover உடன் இணைக்கப்பட்டது",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "வாசிப்பு முன்னேற்றம் மற்றும் குறிப்புகளை ஒத்திசைக்க உங்கள் Hardcover கணக்கை இணைக்கவும்.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "hardcover.app → அமைப்புகள் → API இலிருந்து உங்கள் API டோக்கனைப் பெறவும்.",
|
||||
"API Token": "API டோக்கன்",
|
||||
"Paste your Hardcover API token": "உங்கள் Hardcover API டோக்கனை ஒட்டவும்",
|
||||
"Hardcover sync enabled for this book": "இந்த புத்தகத்திற்கு Hardcover ஒத்திசைவு இயக்கப்பட்டது",
|
||||
"Hardcover sync disabled for this book": "இந்த புத்தகத்திற்கு Hardcover ஒத்திசைவு முடக்கப்பட்டது",
|
||||
"Hardcover Sync": "Hardcover ஒத்திசைவு",
|
||||
"Enable for This Book": "இந்த புத்தகத்திற்கு இயக்கு",
|
||||
"Push Notes": "குறிப்புகளை அனுப்பு",
|
||||
"Enable Hardcover sync for this book first.": "முதலில் இந்த புத்தகத்திற்கு Hardcover ஒத்திசைவை இயக்கவும்.",
|
||||
"No annotations or excerpts to sync for this book.": "இந்த புத்தகத்திற்கு ஒத்திசைக்க குறிப்புகள் அல்லது பகுதிகள் இல்லை.",
|
||||
"Configure Hardcover in Settings first.": "முதலில் அமைப்புகளில் Hardcover-ஐ கட்டமைக்கவும்.",
|
||||
"No new Hardcover note changes to sync.": "ஒத்திசைக்க புதிய Hardcover குறிப்பு மாற்றங்கள் இல்லை.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover ஒத்திசைவு: {{inserted}} புதிய, {{updated}} புதுப்பிக்கப்பட்ட, {{skipped}} மாற்றமற்ற",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover குறிப்புகள் ஒத்திசைவு தோல்வி: {{error}}",
|
||||
"Reading progress synced to Hardcover": "வாசிப்பு முன்னேற்றம் Hardcover உடன் ஒத்திசைக்கப்பட்டது",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover முன்னேற்ற ஒத்திசைவு தோல்வி: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "ஏற்கனவே உள்ள மூல அடையாளங்காட்டியை வைத்திருக்கவும்"
|
||||
}
|
||||
|
||||
@@ -164,7 +164,6 @@
|
||||
"Code Language": "ภาษาโค้ด",
|
||||
"Scroll": "เลื่อน",
|
||||
"Scrolled Mode": "โหมดเลื่อน",
|
||||
"Continuous Scroll": "เลื่อนต่อเนื่อง",
|
||||
"Overlap Pixels": "พิกเซลซ้อน",
|
||||
"Volume Keys for Page Flip": "ปุ่มเสียงเพื่อพลิกหน้า",
|
||||
"Animation": "ภาพเคลื่อนไหว",
|
||||
@@ -269,7 +268,6 @@
|
||||
"Table of Contents": "สารบัญ",
|
||||
"Sidebar": "แถบข้าง",
|
||||
"Disable Translation": "ปิดการแปล",
|
||||
"TTS not supported for PDF": "ไม่รองรับ TTS สำหรับ PDF",
|
||||
"No Timeout": "ไม่จำกัดเวลา",
|
||||
"{{value}} minute": "{{value}} นาที",
|
||||
"{{value}} minutes": "{{value}} นาที",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "ล้างการค้นหา",
|
||||
"Clear search history": "ล้างประวัติการค้นหา",
|
||||
"Tap to Toggle Footer": "แตะเพื่อสลับส่วนท้าย",
|
||||
"Show Current Time": "แสดงเวลาปัจจุบัน",
|
||||
"Use 24 Hour Clock": "ใช้รูปแบบเวลา 24 ชั่วโมง",
|
||||
"Show Current Battery Status": "แสดงสถานะแบตเตอรี่ปัจจุบัน",
|
||||
"Exported successfully": "ส่งออกสำเร็จ",
|
||||
"Book exported successfully.": "ส่งออกหนังสือสำเร็จ",
|
||||
"Failed to export the book.": "ส่งออกหนังสือล้มเหลว",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "คำ",
|
||||
"{{time}} left": "เหลือ {{time}}",
|
||||
"Reading progress": "ความคืบหน้าการอ่าน",
|
||||
"Click to seek": "คลิกเพื่อค้นหาตำแหน่ง",
|
||||
"Skip back 15 words": "ย้อนกลับ 15 คำ",
|
||||
"Back 15 words (Shift+Left)": "ย้อนกลับ 15 คำ (Shift+ซ้าย)",
|
||||
"Pause (Space)": "หยุดชั่วคราว (Space)",
|
||||
"Play (Space)": "เล่น (Space)",
|
||||
"Skip forward 15 words": "ไปข้างหน้า 15 คำ",
|
||||
"Forward 15 words (Shift+Right)": "ไปข้างหน้า 15 คำ (Shift+ขวา)",
|
||||
"Pause:": "หยุดชั่วคราว:",
|
||||
"Decrease speed": "ลดความเร็ว",
|
||||
"Slower (Left/Down)": "ช้าลง (ซ้าย/ลง)",
|
||||
"Current speed": "ความเร็วปัจจุบัน",
|
||||
"Increase speed": "เพิ่มความเร็ว",
|
||||
"Faster (Right/Up)": "เร็วขึ้น (ขวา/ขึ้น)",
|
||||
"Start RSVP Reading": "เริ่มการอ่านแบบ RSVP",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "ความสว่างหน้าจอระบบ",
|
||||
"Page:": "หน้า:",
|
||||
"Page: {{number}}": "หน้า: {{number}}",
|
||||
"Annotation page number": "หมายเลขหน้าคำอธิบายประกอบ"
|
||||
"Annotation page number": "หมายเลขหน้าคำอธิบายประกอบ",
|
||||
"Translating...": "กำลังแปล...",
|
||||
"Show Battery Percentage": "แสดงเปอร์เซ็นต์แบตเตอรี่",
|
||||
"Hide Scrollbar": "ซ่อนแถบเลื่อน",
|
||||
"Skip to last reading position": "ข้ามไปตำแหน่งอ่านล่าสุด",
|
||||
"Show context": "แสดงบริบท",
|
||||
"Hide context": "ซ่อนบริบท",
|
||||
"Decrease font size": "ลดขนาดตัวอักษร",
|
||||
"Increase font size": "เพิ่มขนาดตัวอักษร",
|
||||
"Focus": "โฟกัส",
|
||||
"Theme color": "สีธีม",
|
||||
"Import failed": "นำเข้าล้มเหลว",
|
||||
"Available Formatters:": "ตัวจัดรูปแบบที่ใช้ได้:",
|
||||
"Format date": "จัดรูปแบบวันที่",
|
||||
"Markdown block quote (> per line)": "บล็อกอ้างอิง Markdown (> ต่อบรรทัด)",
|
||||
"Newlines to <br>": "ขึ้นบรรทัดใหม่เป็น <br>",
|
||||
"Change case": "เปลี่ยนตัวพิมพ์",
|
||||
"Trim whitespace": "ตัดช่องว่าง",
|
||||
"Truncate to n characters": "ตัดให้เหลือ n ตัวอักษร",
|
||||
"Replace text": "แทนที่ข้อความ",
|
||||
"Fallback value": "ค่าสำรอง",
|
||||
"Get length": "ดึงความยาว",
|
||||
"First/last element": "องค์ประกอบแรก/สุดท้าย",
|
||||
"Join array": "รวมอาร์เรย์",
|
||||
"Backup failed: {{error}}": "การสำรองข้อมูลล้มเหลว: {{error}}",
|
||||
"Select Backup": "เลือกข้อมูลสำรอง",
|
||||
"Restore failed: {{error}}": "การกู้คืนล้มเหลว: {{error}}",
|
||||
"Backup & Restore": "สำรองและกู้คืน",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "สร้างข้อมูลสำรองของห้องสมุดหรือกู้คืนจากข้อมูลสำรองก่อนหน้า การกู้คืนจะรวมกับห้องสมุดปัจจุบันของคุณ",
|
||||
"Backup Library": "สำรองห้องสมุด",
|
||||
"Restore Library": "กู้คืนห้องสมุด",
|
||||
"Creating backup...": "กำลังสร้างข้อมูลสำรอง...",
|
||||
"Restoring library...": "กำลังกู้คืนห้องสมุด...",
|
||||
"{{current}} of {{total}} items": "{{current}} จาก {{total}} รายการ",
|
||||
"Backup completed successfully!": "สำรองข้อมูลสำเร็จ!",
|
||||
"Restore completed successfully!": "กู้คืนสำเร็จ!",
|
||||
"Your library has been saved to the selected location.": "ห้องสมุดของคุณถูกบันทึกไปยังตำแหน่งที่เลือก",
|
||||
"{{added}} books added, {{updated}} books updated.": "เพิ่ม {{added}} เล่ม, อัปเดต {{updated}} เล่ม",
|
||||
"Operation failed": "การดำเนินการล้มเหลว",
|
||||
"Loading library...": "กำลังโหลดห้องสมุด...",
|
||||
"{{count}} books refreshed_other": "รีเฟรชแล้ว {{count}} เล่ม",
|
||||
"Failed to refresh metadata": "รีเฟรชข้อมูลเมตาล้มเหลว",
|
||||
"Refresh Metadata": "รีเฟรชข้อมูลเมตา",
|
||||
"Keyboard Shortcuts": "แป้นพิมพ์ลัด",
|
||||
"View all keyboard shortcuts": "ดูแป้นพิมพ์ลัดทั้งหมด",
|
||||
"Switch Sidebar Tab": "สลับแท็บแถบด้านข้าง",
|
||||
"Toggle Notebook": "แสดง/ซ่อนสมุดบันทึก",
|
||||
"Search in Book": "ค้นหาในหนังสือ",
|
||||
"Toggle Scroll Mode": "สลับโหมดเลื่อน",
|
||||
"Toggle Select Mode": "สลับโหมดเลือก",
|
||||
"Toggle Bookmark": "สลับที่คั่นหนังสือ",
|
||||
"Toggle Text to Speech": "สลับข้อความเป็นเสียง",
|
||||
"Play / Pause TTS": "เล่น / หยุดชั่วคราว TTS",
|
||||
"Toggle Paragraph Mode": "สลับโหมดย่อหน้า",
|
||||
"Highlight Selection": "ไฮไลต์ข้อความที่เลือก",
|
||||
"Underline Selection": "ขีดเส้นใต้ข้อความที่เลือก",
|
||||
"Annotate Selection": "เพิ่มหมายเหตุให้ข้อความที่เลือก",
|
||||
"Search Selection": "ค้นหาข้อความที่เลือก",
|
||||
"Copy Selection": "คัดลอกข้อความที่เลือก",
|
||||
"Translate Selection": "แปลข้อความที่เลือก",
|
||||
"Dictionary Lookup": "ค้นหาในพจนานุกรม",
|
||||
"Wikipedia Lookup": "ค้นหาใน Wikipedia",
|
||||
"Read Aloud Selection": "อ่านออกเสียงข้อความที่เลือก",
|
||||
"Proofread Selection": "ตรวจทานข้อความที่เลือก",
|
||||
"Open Settings": "เปิดการตั้งค่า",
|
||||
"Open Command Palette": "เปิดแถบคำสั่ง",
|
||||
"Show Keyboard Shortcuts": "แสดงแป้นพิมพ์ลัด",
|
||||
"Open Books": "เปิดหนังสือ",
|
||||
"Toggle Fullscreen": "สลับเต็มหน้าจอ",
|
||||
"Close Window": "ปิดหน้าต่าง",
|
||||
"Quit App": "ออกจากแอป",
|
||||
"Go Left / Previous Page": "ไปซ้าย / หน้าก่อนหน้า",
|
||||
"Go Right / Next Page": "ไปขวา / หน้าถัดไป",
|
||||
"Go Up": "ขึ้น",
|
||||
"Go Down": "ลง",
|
||||
"Previous Chapter": "บทก่อนหน้า",
|
||||
"Next Chapter": "บทถัดไป",
|
||||
"Scroll Half Page Down": "เลื่อนลงครึ่งหน้า",
|
||||
"Scroll Half Page Up": "เลื่อนขึ้นครึ่งหน้า",
|
||||
"Save Note": "บันทึกโน้ต",
|
||||
"Single Section Scroll": "เลื่อนทีละส่วน",
|
||||
"General": "ทั่วไป",
|
||||
"Text to Speech": "อ่านออกเสียง",
|
||||
"Zoom": "ซูม",
|
||||
"Window": "หน้าต่าง",
|
||||
"Your Bookshelf": "ชั้นหนังสือของคุณ",
|
||||
"TTS": "อ่านออกเสียง",
|
||||
"Media Info": "ข้อมูลสื่อ",
|
||||
"Update Frequency": "ความถี่ในการอัปเดต",
|
||||
"Every Sentence": "ทุกประโยค",
|
||||
"Every Paragraph": "ทุกย่อหน้า",
|
||||
"Every Chapter": "ทุกบท",
|
||||
"TTS Media Info Update Frequency": "ความถี่ในการอัปเดตข้อมูลสื่อ TTS",
|
||||
"Select reading speed": "เลือกความเร็วในการอ่าน",
|
||||
"Drag to seek": "ลากเพื่อค้นหา",
|
||||
"Punctuation Delay": "หน่วงเวลาเครื่องหมายวรรคตอน",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "กรุณายืนยันว่าการเชื่อมต่อ OPDS นี้จะถูกส่งผ่านเซิร์ฟเวอร์ Readest บนเว็บแอปก่อนดำเนินการต่อ",
|
||||
"Custom Headers": "ส่วนหัวที่กำหนดเอง",
|
||||
"Custom Headers (optional)": "ส่วนหัวที่กำหนดเอง (ไม่บังคับ)",
|
||||
"Add one header per line using \"Header-Name: value\".": "เพิ่มส่วนหัวหนึ่งรายการต่อบรรทัดโดยใช้ \"Header-Name: value\"",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "ฉันเข้าใจว่าการเชื่อมต่อ OPDS นี้จะถูกส่งผ่านเซิร์ฟเวอร์ Readest บนเว็บแอป หากฉันไม่ไว้วางใจ Readest กับข้อมูลรับรองหรือส่วนหัวเหล่านี้ ฉันควรใช้แอปเนทีฟแทน",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "ไม่สามารถเชื่อมต่อกับ Hardcover ได้ กรุณาตรวจสอบการเชื่อมต่อเครือข่ายของคุณ",
|
||||
"Invalid Hardcover API token": "โทเค็น API ของ Hardcover ไม่ถูกต้อง",
|
||||
"Disconnected from Hardcover": "ตัดการเชื่อมต่อจาก Hardcover แล้ว",
|
||||
"Hardcover Settings": "การตั้งค่า Hardcover",
|
||||
"Connected to Hardcover": "เชื่อมต่อกับ Hardcover แล้ว",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "เชื่อมต่อบัญชี Hardcover ของคุณเพื่อซิงค์ความคืบหน้าการอ่านและบันทึก",
|
||||
"Get your API token from hardcover.app → Settings → API.": "รับโทเค็น API ของคุณจาก hardcover.app → การตั้งค่า → API",
|
||||
"API Token": "โทเค็น API",
|
||||
"Paste your Hardcover API token": "วางโทเค็น API ของ Hardcover ของคุณ",
|
||||
"Hardcover sync enabled for this book": "เปิดใช้งานการซิงค์ Hardcover สำหรับหนังสือนี้แล้ว",
|
||||
"Hardcover sync disabled for this book": "ปิดใช้งานการซิงค์ Hardcover สำหรับหนังสือนี้แล้ว",
|
||||
"Hardcover Sync": "การซิงค์ Hardcover",
|
||||
"Enable for This Book": "เปิดใช้งานสำหรับหนังสือนี้",
|
||||
"Push Notes": "ส่งบันทึก",
|
||||
"Enable Hardcover sync for this book first.": "เปิดใช้งานการซิงค์ Hardcover สำหรับหนังสือนี้ก่อน",
|
||||
"No annotations or excerpts to sync for this book.": "ไม่มีคำอธิบายประกอบหรือข้อความคัดลอกที่จะซิงค์สำหรับหนังสือนี้",
|
||||
"Configure Hardcover in Settings first.": "กำหนดค่า Hardcover ในการตั้งค่าก่อน",
|
||||
"No new Hardcover note changes to sync.": "ไม่มีการเปลี่ยนแปลงบันทึก Hardcover ใหม่ที่จะซิงค์",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "ซิงค์ Hardcover แล้ว: {{inserted}} รายการใหม่, {{updated}} อัปเดต, {{skipped}} ไม่เปลี่ยนแปลง",
|
||||
"Hardcover notes sync failed: {{error}}": "การซิงค์บันทึก Hardcover ล้มเหลว: {{error}}",
|
||||
"Reading progress synced to Hardcover": "ความคืบหน้าการอ่านซิงค์กับ Hardcover แล้ว",
|
||||
"Hardcover progress sync failed: {{error}}": "การซิงค์ความคืบหน้า Hardcover ล้มเหลว: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "เก็บตัวระบุแหล่งที่มาที่มีอยู่"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Vikipedi",
|
||||
"Writing Mode": "Yazma Modu",
|
||||
"Your Library": "Kütüphaneniz",
|
||||
"TTS not supported for PDF": "PDF için TTS desteklenmiyor",
|
||||
"Override Book Font": "Kitap Yazı Tipini Geçersiz Kıl",
|
||||
"Apply to All Books": "Tüm kitaplara uygula",
|
||||
"Apply to This Book": "Bu kitaba uygula",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "RTL Yönü",
|
||||
"Maximum Column Height": "Maksimum Sütun Yüksekliği",
|
||||
"Maximum Column Width": "Maksimum Sütun Genişliği",
|
||||
"Continuous Scroll": "Sürekli Kaydırma",
|
||||
"Fullscreen": "Tam Ekran",
|
||||
"No supported files found. Supported formats: {{formats}}": "Desteklenen dosya bulunamadı. Desteklenen formatlar: {{formats}}",
|
||||
"Drop to Import Books": "Kitapları İçe Aktarmak İçin Bırak",
|
||||
@@ -832,6 +830,9 @@
|
||||
"Clear search": "Aramayı temizle",
|
||||
"Clear search history": "Arama geçmişini temizle",
|
||||
"Tap to Toggle Footer": "Altbilgiyi değiştirmek için dokunun",
|
||||
"Show Current Time": "Şu anki saati göster",
|
||||
"Use 24 Hour Clock": "24 Saatlik Saati Kullan",
|
||||
"Show Current Battery Status": "Mevcut Pil Durumunu Göster",
|
||||
"Exported successfully": "Başarıyla dışa aktarıldı",
|
||||
"Book exported successfully.": "Kitap başarıyla dışa aktarıldı.",
|
||||
"Failed to export the book.": "Kitap dışa aktarılamadı.",
|
||||
@@ -981,17 +982,14 @@
|
||||
"words": "kelime",
|
||||
"{{time}} left": "{{time}} kaldı",
|
||||
"Reading progress": "Okuma ilerlemesi",
|
||||
"Click to seek": "Konum aramak için tıkla",
|
||||
"Skip back 15 words": "15 kelime geri atla",
|
||||
"Back 15 words (Shift+Left)": "15 kelime geri (Shift+Sol)",
|
||||
"Pause (Space)": "Duraklat (Boşluk)",
|
||||
"Play (Space)": "Oynat (Boşluk)",
|
||||
"Skip forward 15 words": "15 kelime ileri atla",
|
||||
"Forward 15 words (Shift+Right)": "15 kelime ileri (Shift+Sağ)",
|
||||
"Pause:": "Duraklat:",
|
||||
"Decrease speed": "Hızı azalt",
|
||||
"Slower (Left/Down)": "Daha yavaş (Sol/Aşağı)",
|
||||
"Current speed": "Mevcut hız",
|
||||
"Increase speed": "Hızı artır",
|
||||
"Faster (Right/Up)": "Daha hızlı (Sağ/Yukarı)",
|
||||
"Start RSVP Reading": "RSVP Okumasını Başlat",
|
||||
@@ -1059,5 +1057,130 @@
|
||||
"System Screen Brightness": "Sistem Ekran Parlaklığı",
|
||||
"Page:": "Sayfa:",
|
||||
"Page: {{number}}": "Sayfa: {{number}}",
|
||||
"Annotation page number": "Açıklama sayfa numarası"
|
||||
"Annotation page number": "Açıklama sayfa numarası",
|
||||
"Translating...": "Çevriliyor...",
|
||||
"Show Battery Percentage": "Pil Yüzdesini Göster",
|
||||
"Hide Scrollbar": "Kaydırma çubuğunu gizle",
|
||||
"Skip to last reading position": "Son okuma konumuna atla",
|
||||
"Show context": "Bağlamı göster",
|
||||
"Hide context": "Bağlamı gizle",
|
||||
"Decrease font size": "Yazı tipi boyutunu küçült",
|
||||
"Increase font size": "Yazı tipi boyutunu büyüt",
|
||||
"Focus": "Odak",
|
||||
"Theme color": "Tema rengi",
|
||||
"Import failed": "İçe aktarma başarısız",
|
||||
"Available Formatters:": "Kullanılabilir Biçimlendiriciler:",
|
||||
"Format date": "Tarih biçimlendir",
|
||||
"Markdown block quote (> per line)": "Markdown blok alıntı (satır başına >)",
|
||||
"Newlines to <br>": "Satır sonlarını <br> yap",
|
||||
"Change case": "Büyük/küçük harf değiştir",
|
||||
"Trim whitespace": "Boşlukları kırp",
|
||||
"Truncate to n characters": "n karaktere kısalt",
|
||||
"Replace text": "Metni değiştir",
|
||||
"Fallback value": "Yedek değer",
|
||||
"Get length": "Uzunluk al",
|
||||
"First/last element": "İlk/son öğe",
|
||||
"Join array": "Diziyi birleştir",
|
||||
"Backup failed: {{error}}": "Yedekleme başarısız: {{error}}",
|
||||
"Select Backup": "Yedek Seç",
|
||||
"Restore failed: {{error}}": "Geri yükleme başarısız: {{error}}",
|
||||
"Backup & Restore": "Yedekleme & Geri Yükleme",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Kütüphanenizin yedeğini oluşturun veya önceki bir yedekten geri yükleyin. Geri yükleme mevcut kütüphanenizle birleştirilecektir.",
|
||||
"Backup Library": "Kütüphaneyi Yedekle",
|
||||
"Restore Library": "Kütüphaneyi Geri Yükle",
|
||||
"Creating backup...": "Yedek oluşturuluyor...",
|
||||
"Restoring library...": "Kütüphane geri yükleniyor...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} öğe",
|
||||
"Backup completed successfully!": "Yedekleme başarıyla tamamlandı!",
|
||||
"Restore completed successfully!": "Geri yükleme başarıyla tamamlandı!",
|
||||
"Your library has been saved to the selected location.": "Kütüphaneniz seçilen konuma kaydedildi.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} kitap eklendi, {{updated}} kitap güncellendi.",
|
||||
"Operation failed": "İşlem başarısız",
|
||||
"Loading library...": "Kütüphane yükleniyor...",
|
||||
"{{count}} books refreshed_one": "{{count}} kitap yenilendi",
|
||||
"{{count}} books refreshed_other": "{{count}} kitap yenilendi",
|
||||
"Failed to refresh metadata": "Meta veri yenileme başarısız",
|
||||
"Refresh Metadata": "Meta Verileri Yenile",
|
||||
"Keyboard Shortcuts": "Klavye Kısayolları",
|
||||
"View all keyboard shortcuts": "Tüm klavye kısayollarını görüntüle",
|
||||
"Switch Sidebar Tab": "Kenar çubuğu sekmesini değiştir",
|
||||
"Toggle Notebook": "Not defterini göster/gizle",
|
||||
"Search in Book": "Kitapta ara",
|
||||
"Toggle Scroll Mode": "Kaydırma modunu değiştir",
|
||||
"Toggle Select Mode": "Seçim modunu değiştir",
|
||||
"Toggle Bookmark": "Yer imini değiştir",
|
||||
"Toggle Text to Speech": "Metinden konuşmayı değiştir",
|
||||
"Play / Pause TTS": "TTS Oynat / Duraklat",
|
||||
"Toggle Paragraph Mode": "Paragraf modunu değiştir",
|
||||
"Highlight Selection": "Seçimi vurgula",
|
||||
"Underline Selection": "Seçimin altını çiz",
|
||||
"Annotate Selection": "Seçime not ekle",
|
||||
"Search Selection": "Seçimi ara",
|
||||
"Copy Selection": "Seçimi kopyala",
|
||||
"Translate Selection": "Seçimi çevir",
|
||||
"Dictionary Lookup": "Sözlükte ara",
|
||||
"Wikipedia Lookup": "Wikipedia'da ara",
|
||||
"Read Aloud Selection": "Seçimi sesli oku",
|
||||
"Proofread Selection": "Seçimi düzelt",
|
||||
"Open Settings": "Ayarları aç",
|
||||
"Open Command Palette": "Komut paletini aç",
|
||||
"Show Keyboard Shortcuts": "Klavye kısayollarını göster",
|
||||
"Open Books": "Kitapları aç",
|
||||
"Toggle Fullscreen": "Tam ekranı değiştir",
|
||||
"Close Window": "Pencereyi kapat",
|
||||
"Quit App": "Uygulamadan çık",
|
||||
"Go Left / Previous Page": "Sola git / Önceki sayfa",
|
||||
"Go Right / Next Page": "Sağa git / Sonraki sayfa",
|
||||
"Go Up": "Yukarı",
|
||||
"Go Down": "Aşağı",
|
||||
"Previous Chapter": "Önceki bölüm",
|
||||
"Next Chapter": "Sonraki bölüm",
|
||||
"Scroll Half Page Down": "Yarım sayfa aşağı kaydır",
|
||||
"Scroll Half Page Up": "Yarım sayfa yukarı kaydır",
|
||||
"Save Note": "Notu kaydet",
|
||||
"Single Section Scroll": "Tek Bölüm Kaydırma",
|
||||
"General": "Genel",
|
||||
"Text to Speech": "Metinden konuşmaya",
|
||||
"Zoom": "Yakınlaştırma",
|
||||
"Window": "Pencere",
|
||||
"Your Bookshelf": "Kitaplığınız",
|
||||
"TTS": "Sesli Okuma",
|
||||
"Media Info": "Medya Bilgisi",
|
||||
"Update Frequency": "Güncelleme Sıklığı",
|
||||
"Every Sentence": "Her Cümle",
|
||||
"Every Paragraph": "Her Paragraf",
|
||||
"Every Chapter": "Her Bölüm",
|
||||
"TTS Media Info Update Frequency": "TTS Medya Bilgisi Güncelleme Sıklığı",
|
||||
"Select reading speed": "Okuma hızını seçin",
|
||||
"Drag to seek": "Aramak için sürükleyin",
|
||||
"Punctuation Delay": "Noktalama gecikmesi",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Devam etmeden önce bu OPDS bağlantısının web uygulamasında Readest sunucuları üzerinden yönlendirileceğini onaylayın.",
|
||||
"Custom Headers": "Özel Başlıklar",
|
||||
"Custom Headers (optional)": "Özel Başlıklar (isteğe bağlı)",
|
||||
"Add one header per line using \"Header-Name: value\".": "\"Header-Name: value\" biçimini kullanarak her satıra bir başlık ekleyin.",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Bu OPDS bağlantısının web uygulamasında Readest sunucuları üzerinden yönlendirileceğini anlıyorum. Bu kimlik bilgileri veya başlıklar konusunda Readest'e güvenmiyorsam, bunun yerine yerel uygulamayı kullanmalıyım.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Hardcover'a bağlanılamıyor. Lütfen ağ bağlantınızı kontrol edin.",
|
||||
"Invalid Hardcover API token": "Geçersiz Hardcover API belirteci",
|
||||
"Disconnected from Hardcover": "Hardcover bağlantısı kesildi",
|
||||
"Hardcover Settings": "Hardcover Ayarları",
|
||||
"Connected to Hardcover": "Hardcover'a bağlandı",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Okuma ilerlemenizi ve notlarınızı senkronize etmek için Hardcover hesabınızı bağlayın.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "API belirtecinizi hardcover.app → Ayarlar → API'den alın.",
|
||||
"API Token": "API Belirteci",
|
||||
"Paste your Hardcover API token": "Hardcover API belirtecinizi yapıştırın",
|
||||
"Hardcover sync enabled for this book": "Bu kitap için Hardcover senkronizasyonu etkinleştirildi",
|
||||
"Hardcover sync disabled for this book": "Bu kitap için Hardcover senkronizasyonu devre dışı bırakıldı",
|
||||
"Hardcover Sync": "Hardcover Senkronizasyonu",
|
||||
"Enable for This Book": "Bu Kitap İçin Etkinleştir",
|
||||
"Push Notes": "Notları Gönder",
|
||||
"Enable Hardcover sync for this book first.": "Önce bu kitap için Hardcover senkronizasyonunu etkinleştirin.",
|
||||
"No annotations or excerpts to sync for this book.": "Bu kitap için senkronize edilecek açıklama veya alıntı yok.",
|
||||
"Configure Hardcover in Settings first.": "Önce Ayarlar'da Hardcover'ı yapılandırın.",
|
||||
"No new Hardcover note changes to sync.": "Senkronize edilecek yeni Hardcover not değişikliği yok.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover senkronize edildi: {{inserted}} yeni, {{updated}} güncellendi, {{skipped}} değişmedi",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover not senkronizasyonu başarısız: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Okuma ilerlemesi Hardcover ile senkronize edildi",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover ilerleme senkronizasyonu başarısız: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Mevcut kaynak tanımlayıcısını koru"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Вікіпедія",
|
||||
"Writing Mode": "Режим письма",
|
||||
"Your Library": "Ваша бібліотека",
|
||||
"TTS not supported for PDF": "TTS не підтримується для PDF",
|
||||
"Override Book Font": "Перевизначити шрифт книги",
|
||||
"Apply to All Books": "Застосувати до всіх книг",
|
||||
"Apply to This Book": "Застосувати лише до цієї книги",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Напрямок RTL",
|
||||
"Maximum Column Height": "Максимальна висота колонки",
|
||||
"Maximum Column Width": "Максимальна ширина колонки",
|
||||
"Continuous Scroll": "Постійна прокрутка",
|
||||
"Fullscreen": "Повноекранний",
|
||||
"No supported files found. Supported formats: {{formats}}": "Не знайдено підтримуваних файлів. Підтримувані формати: {{formats}}",
|
||||
"Drop to Import Books": "Перетягніть для імпорту книг",
|
||||
@@ -565,14 +563,14 @@
|
||||
"Minimize": "Згорнути",
|
||||
"Maximize or Restore": "Збільшити або відновити",
|
||||
"Exit Parallel Read": "Вийти з паралельного читання",
|
||||
"Enter Parallel Read": "Войти в параллельное чтение",
|
||||
"Enter Parallel Read": "Ввійти в паралельне читання",
|
||||
"Zoom Level": "Рівень масштабування",
|
||||
"Zoom Out": "Зменшити масштаб",
|
||||
"Reset Zoom": "Скинути масштаб",
|
||||
"Zoom In": "Збільшити масштаб",
|
||||
"Zoom Mode": "Режим масштабування",
|
||||
"Single Page": "Одна сторінка",
|
||||
"Auto Spread": "Автоматичне розповсюдження",
|
||||
"Auto Spread": "Автоматичне розтягування",
|
||||
"Fit Page": "Підганяти під сторінку",
|
||||
"Fit Width": "Підганяти під ширину",
|
||||
"Failed to select directory": "Не вдалося вибрати каталог",
|
||||
@@ -589,23 +587,23 @@
|
||||
"Copying: {{file}}": "Копіювання: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} з {{total}} файлів",
|
||||
"Migration completed successfully!": "Переміщення завершено успішно!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ваші дані були переміщені до нового розташування. Будь ласка, перезапустіть програму, щоб завершити процес.",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Ваші дані були переміщені до нового розташування. Будь ласка, перезапустіть застосунок, щоб завершити процес.",
|
||||
"Migration failed": "Переміщення не вдалося",
|
||||
"Important Notice": "Важливе повідомлення",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Це перемістить всі ваші дані програми до нового розташування. Переконайтеся, що в призначенні достатньо вільного місця.",
|
||||
"Restart App": "Перезапустити програму",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Це перемістить всі ваші дані до нового розташування. Переконайтеся, що у вибраному розташуванні достатньо вільного місця.",
|
||||
"Restart App": "Перезапустити застосунок",
|
||||
"Start Migration": "Почати переміщення",
|
||||
"Advanced Settings": "Розширені налаштування",
|
||||
"File count: {{size}}": "Кількість файлів: {{size}}",
|
||||
"Background Read Aloud": "Озвучування у фоновому режимі",
|
||||
"Ready to read aloud": "Готовий до читання вголос",
|
||||
"Ready to read aloud": "Готовність до читання вголос",
|
||||
"Read Aloud": "Читати вголос",
|
||||
"Screen Brightness": "Яскравість екрану",
|
||||
"Background Image": "Фонове зображення",
|
||||
"Import Image": "Імпортувати зображення",
|
||||
"Opacity": "Непрозорість",
|
||||
"Size": "Розмір",
|
||||
"Cover": "Покрити",
|
||||
"Cover": "Перекрити",
|
||||
"Contain": "Втиснути",
|
||||
"{{number}} pages left in chapter": "<1>Залишилось </1><0>{{number}}</0><1> сторінок у розділі</1>",
|
||||
"Device": "Пристрій",
|
||||
@@ -615,9 +613,9 @@
|
||||
"Disable Double Tap": "Вимкнути подвійне натискання",
|
||||
"Tap to Paginate": "Натисніть, щоб розбити на сторінки",
|
||||
"Click to Paginate": "Клікніть, щоб розбити на сторінки",
|
||||
"Tap Both Sides": "Натисніть обидві сторони",
|
||||
"Click Both Sides": "Клікніть обидві сторони",
|
||||
"Swap Tap Sides": "Змінити сторони дотику",
|
||||
"Tap Both Sides": "Натискати на обидві сторони",
|
||||
"Click Both Sides": "Клікати на обидві сторони",
|
||||
"Swap Tap Sides": "Змінити сторони натискання",
|
||||
"Swap Click Sides": "Змінити сторони кліку",
|
||||
"Source and Translated": "Джерело та переклад",
|
||||
"Translated Only": "Тільки переклад",
|
||||
@@ -636,7 +634,7 @@
|
||||
"Unlock All Customization Options": "Відкрити всі параметри налаштування",
|
||||
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Відкрийте додаткові теми, шрифти, параметри макета та функції читання вголос, перекладачі, послуги хмарного зберігання.",
|
||||
"Purchase Successful!": "Покупка успішна!",
|
||||
"lifetime": "довічний",
|
||||
"lifetime": "на все життя",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Дякуємо за вашу покупку! Ваш платіж успішно оброблено.",
|
||||
"Order ID:": "ID замовлення:",
|
||||
"TTS Highlighting": "Підсвічування TTS",
|
||||
@@ -717,8 +715,8 @@
|
||||
"Sample": "Зразок",
|
||||
"Download": "Завантажити",
|
||||
"Open & Read": "Відкрити та читати",
|
||||
"Tags": "Теги",
|
||||
"Tag": "Тег",
|
||||
"Tags": "Теґи",
|
||||
"Tag": "Теґ",
|
||||
"First": "Перша",
|
||||
"Previous": "Попередня",
|
||||
"Next": "Наступна",
|
||||
@@ -727,7 +725,7 @@
|
||||
"An error occurred": "Сталася помилка",
|
||||
"Online Library": "Онлайн бібліотека",
|
||||
"URL must start with http:// or https://": "URL повинен починатися з http:// або https://",
|
||||
"Title, Author, Tag, etc...": "Назва, автор, тег тощо...",
|
||||
"Title, Author, Tag, etc...": "Назва, автор, теґ тощо...",
|
||||
"Query": "Запит",
|
||||
"Subject": "Тема",
|
||||
"Enter {{terms}}": "Введіть {{terms}}",
|
||||
@@ -755,8 +753,8 @@
|
||||
"Oldest First": "Спершу старіші",
|
||||
"Largest First": "Спершу найбільші",
|
||||
"Smallest First": "Спершу найменші",
|
||||
"Name A-Z": "Ім'я A-Z",
|
||||
"Name Z-A": "Ім'я Z-A",
|
||||
"Name A-Z": "Ім'я А-Я",
|
||||
"Name Z-A": "Ім'я А-Я",
|
||||
"{{count}} selected_one": "Вибрано {{count}} файл",
|
||||
"{{count}} selected_few": "Вибрано {{count}} файли",
|
||||
"{{count}} selected_many": "Вибрано {{count}} файлів",
|
||||
@@ -776,21 +774,21 @@
|
||||
"From Directory": "З каталогу",
|
||||
"Successfully imported {{count}} book(s)_one": "Успішно імпортовано 1 книгу",
|
||||
"Successfully imported {{count}} book(s)_few": "Успішно імпортовано {{count}} книги",
|
||||
"Successfully imported {{count}} book(s)_many": "Успішно імпортовано {{count}} книг",
|
||||
"Successfully imported {{count}} book(s)_other": "Успішно імпортовано {{count}} книг",
|
||||
"Successfully imported {{count}} book(s)_many": "Успішно імпортовано {{count}} книги",
|
||||
"Successfully imported {{count}} book(s)_other": "Успішно імпортовано {{count}} книги",
|
||||
"Count": "Кількість",
|
||||
"Start Page": "Початкова сторінка",
|
||||
"Search in OPDS Catalog...": "Пошук у каталозі OPDS...",
|
||||
"Please log in to use advanced TTS features": "Будь ласка, увійдіть, щоб використовувати розширені функції TTS",
|
||||
"Word limit of 30 words exceeded.": "Перевищено ліміт у 30 слів.",
|
||||
"Proofread": "Вичитування",
|
||||
"Proofread": "Вичитка",
|
||||
"Current selection": "Поточне виділення",
|
||||
"All occurrences in this book": "Усі входження в цій книзі",
|
||||
"All occurrences in your library": "Усі входження у вашій бібліотеці",
|
||||
"All occurrences in this book": "Усі випадки в цій книзі",
|
||||
"All occurrences in your library": "Усі випадки у вашій бібліотеці",
|
||||
"Selected text:": "Виділений текст:",
|
||||
"Replace with:": "Замінити на:",
|
||||
"Enter text...": "Введіть текст…",
|
||||
"Case sensitive:": "З урахуванням регістру:",
|
||||
"Case sensitive:": "З урахуванням реґістру:",
|
||||
"Scope:": "Область застосування:",
|
||||
"Selection": "Виділення",
|
||||
"Library": "Бібліотека",
|
||||
@@ -803,7 +801,7 @@
|
||||
"No book-level replacement rules": "Немає правил заміни на рівні книги",
|
||||
"Disable Quick Action": "Вимкнути швидку дію",
|
||||
"Enable Quick Action on Selection": "Увімкнути швидку дію при виборі",
|
||||
"None": "Жоден",
|
||||
"None": "Нема",
|
||||
"Annotation Tools": "Інструменти анотацій",
|
||||
"Enable Quick Actions": "Увімкнути швидкі дії",
|
||||
"Quick Action": "Швидка дія",
|
||||
@@ -851,7 +849,10 @@
|
||||
"Show Results": "Показати результати",
|
||||
"Clear search": "Очистити пошук",
|
||||
"Clear search history": "Очистити історію пошуку",
|
||||
"Tap to Toggle Footer": "Торкніться, щоб перемкнути нижній колонтитул",
|
||||
"Tap to Toggle Footer": "Нижній колонтитул",
|
||||
"Show Current Time": "Показати поточний час",
|
||||
"Use 24 Hour Clock": "Використовувати 24-годинний формат",
|
||||
"Show Current Battery Status": "Показати поточний стан батареї",
|
||||
"Exported successfully": "Успішно експортовано",
|
||||
"Book exported successfully.": "Книгу успішно експортовано.",
|
||||
"Failed to export the book.": "Не вдалося експортувати книгу.",
|
||||
@@ -908,8 +909,8 @@
|
||||
"Toggle Sticky Bottom TTS Bar": "Перемкнути закріплену панель TTS",
|
||||
"Display what I'm reading on Discord": "Показувати книгу в Discord",
|
||||
"Show on Discord": "Показати в Discord",
|
||||
"Instant {{action}}": "Миттєва {{action}}",
|
||||
"Instant {{action}} Disabled": "Миттєву {{action}} вимкнено",
|
||||
"Instant {{action}}": "Швидка дія: {{action}}",
|
||||
"Instant {{action}} Disabled": "Миттєву дію {{action}} вимкнено",
|
||||
"Annotation": "Анотація",
|
||||
"Reset Template": "Скинути шаблон",
|
||||
"Annotation style": "Стиль анотації",
|
||||
@@ -932,7 +933,7 @@
|
||||
"Please enter a model ID": "Будь ласка, введіть ID моделі",
|
||||
"Model not available or invalid": "Модель недоступна або недійсна",
|
||||
"Failed to validate model": "Не вдалося перевірити модель",
|
||||
"Couldn't connect to Ollama. Is it running?": "Не вдалося підключитися до Ollama. Чи вона запущена?",
|
||||
"Couldn't connect to Ollama. Is it running?": "Не вдалося підключитися до Ollama. Вона запущена?",
|
||||
"Invalid API key or connection failed": "Недійсний API-ключ або помилка підключення",
|
||||
"Connection failed": "Помилка підключення",
|
||||
"AI Assistant": "ШІ-асистент",
|
||||
@@ -945,7 +946,7 @@
|
||||
"AI Model": "ШІ-модель",
|
||||
"No models detected": "Моделі не знайдено",
|
||||
"AI Gateway Configuration": "Налаштування ШІ-шлюзу",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Оберіть з високоякісних, економічних ШІ-моделей. Ви також можете використати власну модель, обравши \"Користувацька модель\" нижче.",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Оберіть з високоякісних або економних ШІ-моделей. Ви також можете використати власну модель, обравши \"Користувацька модель\" нижче.",
|
||||
"API Key": "API-ключ",
|
||||
"Get Key": "Отримати ключ",
|
||||
"Model": "Модель",
|
||||
@@ -961,7 +962,7 @@
|
||||
"Reading Ruler": "Лінійка для читання",
|
||||
"Enable Reading Ruler": "Увімкнути лінійку для читання",
|
||||
"Lines to Highlight": "Рядки для виділення",
|
||||
"Ruler Color": "Коліර් лінійки",
|
||||
"Ruler Color": "Колір лінійки",
|
||||
"Command Palette": "Палітра команд",
|
||||
"Search settings and actions...": "Пошук налаштувань та дій...",
|
||||
"No results found for": "Результатів не знайдено для",
|
||||
@@ -975,7 +976,7 @@
|
||||
"AI Provider": "Провайдер ШІ",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Модель Ollama",
|
||||
"AI Gateway Model": "Модель AI Gateway",
|
||||
"AI Gateway Model": "Модель ШІ-шлюзу",
|
||||
"Actions": "Дії",
|
||||
"Navigation": "Навігація",
|
||||
"Set status for {{count}} book(s)_one": "Встановити статус для {{count}} книги",
|
||||
@@ -1005,19 +1006,16 @@
|
||||
"words": "слів",
|
||||
"{{time}} left": "залишилося {{time}}",
|
||||
"Reading progress": "Прогрес читання",
|
||||
"Click to seek": "Натисніть для пошуку",
|
||||
"Skip back 15 words": "Назад на 15 слів",
|
||||
"Back 15 words (Shift+Left)": "Назад на 15 слів (Shift+Вліво)",
|
||||
"Back 15 words (Shift+Left)": "Назад на 15 слів (Shift+Стрілка вліво)",
|
||||
"Pause (Space)": "Пауза (Пробіл)",
|
||||
"Play (Space)": "Відтворити (Пробіл)",
|
||||
"Skip forward 15 words": "Вперед на 15 слів",
|
||||
"Forward 15 words (Shift+Right)": "Вперед на 15 слів (Shift+Вправо)",
|
||||
"Pause:": "Пауза:",
|
||||
"Forward 15 words (Shift+Right)": "Вперед на 15 слів (Shift+Стрілка вправо)",
|
||||
"Decrease speed": "Зменшити швидкість",
|
||||
"Slower (Left/Down)": "Повільніше (Вліво/Вниз)",
|
||||
"Current speed": "Поточна швидкість",
|
||||
"Slower (Left/Down)": "Повільніше (стрілки Вліво/Вниз)",
|
||||
"Increase speed": "Збільшити швидкість",
|
||||
"Faster (Right/Up)": "Швидше (Вправо/Вгору)",
|
||||
"Faster (Right/Up)": "Швидше (стрілки Вправо/Вгору)",
|
||||
"Start RSVP Reading": "Почати читання RSVP",
|
||||
"Choose where to start reading": "Виберіть, де почати читання",
|
||||
"From Chapter Start": "З початку розділу",
|
||||
@@ -1083,5 +1081,132 @@
|
||||
"System Screen Brightness": "Системна яскравість екрана",
|
||||
"Page:": "Сторінка:",
|
||||
"Page: {{number}}": "Сторінка: {{number}}",
|
||||
"Annotation page number": "Номер сторінки анотації"
|
||||
"Annotation page number": "Номер сторінки анотації",
|
||||
"Translating...": "Переклад...",
|
||||
"Show Battery Percentage": "Показувати відсоток заряду батареї",
|
||||
"Hide Scrollbar": "Сховати смугу прокрутки",
|
||||
"Skip to last reading position": "Перейти до останньої позиції читання",
|
||||
"Show context": "Показати контекст",
|
||||
"Hide context": "Сховати контекст",
|
||||
"Decrease font size": "Зменшити розмір шрифту",
|
||||
"Increase font size": "Збільшити розмір шрифту",
|
||||
"Focus": "Фокус",
|
||||
"Theme color": "Колір теми",
|
||||
"Import failed": "Помилка імпорту",
|
||||
"Available Formatters:": "Доступні форматери:",
|
||||
"Format date": "Форматувати дату",
|
||||
"Markdown block quote (> per line)": "Цитата Markdown (> для кожного рядка)",
|
||||
"Newlines to <br>": "Переноси рядків у <br>",
|
||||
"Change case": "Змінити регістр",
|
||||
"Trim whitespace": "Видалити пробіли",
|
||||
"Truncate to n characters": "Обрізати до n символів",
|
||||
"Replace text": "Замінити текст",
|
||||
"Fallback value": "Значення за замовчуванням",
|
||||
"Get length": "Отримати довжину",
|
||||
"First/last element": "Перший/останній елемент",
|
||||
"Join array": "Об'єднати масив",
|
||||
"Backup failed: {{error}}": "Помилка резервного копіювання: {{error}}",
|
||||
"Select Backup": "Обрати резервну копію",
|
||||
"Restore failed: {{error}}": "Помилка відновлення: {{error}}",
|
||||
"Backup & Restore": "Резервне копіювання та відновлення",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Створіть резервну копію бібліотеки або відновіть з попередньої копії. Відновлення буде об'єднано з поточною бібліотекою.",
|
||||
"Backup Library": "Резервна копія бібліотеки",
|
||||
"Restore Library": "Відновити бібліотеку",
|
||||
"Creating backup...": "Створення резервної копії...",
|
||||
"Restoring library...": "Відновлення бібліотеки...",
|
||||
"{{current}} of {{total}} items": "{{current}} з {{total}} елементів",
|
||||
"Backup completed successfully!": "Резервне копіювання завершено!",
|
||||
"Restore completed successfully!": "Відновлення завершено!",
|
||||
"Your library has been saved to the selected location.": "Вашу бібліотеку збережено у вибраному місці.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Додано {{added}} книг, оновлено {{updated}} книг.",
|
||||
"Operation failed": "Операція не вдалася",
|
||||
"Loading library...": "Завантаження бібліотеки...",
|
||||
"{{count}} books refreshed_one": "Оновлено {{count}} книгу",
|
||||
"{{count}} books refreshed_few": "Оновлено {{count}} книги",
|
||||
"{{count}} books refreshed_many": "Оновлено {{count}} книг",
|
||||
"{{count}} books refreshed_other": "Оновлено {{count}} книг",
|
||||
"Failed to refresh metadata": "Не вдалося оновити метадані",
|
||||
"Refresh Metadata": "Оновити метадані",
|
||||
"Keyboard Shortcuts": "Комбінації клавіш",
|
||||
"View all keyboard shortcuts": "Переглянути всі комбінації клавіш",
|
||||
"Switch Sidebar Tab": "Перемкнути вкладку бічної панелі",
|
||||
"Toggle Notebook": "Показати/сховати блокнот",
|
||||
"Search in Book": "Пошук у книзі",
|
||||
"Toggle Scroll Mode": "Перемкнути режим прокрутки",
|
||||
"Toggle Select Mode": "Перемкнути режим виділення",
|
||||
"Toggle Bookmark": "Перемкнути закладку",
|
||||
"Toggle Text to Speech": "Перемкнути озвучення тексту",
|
||||
"Play / Pause TTS": "Відтворити / Пауза TTS",
|
||||
"Toggle Paragraph Mode": "Перемкнути режим абзацу",
|
||||
"Highlight Selection": "Виділити вибране",
|
||||
"Underline Selection": "Підкреслити вибране",
|
||||
"Annotate Selection": "Додати примітку до вибраного",
|
||||
"Search Selection": "Шукати вибране",
|
||||
"Copy Selection": "Копіювати вибране",
|
||||
"Translate Selection": "Перекласти вибране",
|
||||
"Dictionary Lookup": "Пошук у словнику",
|
||||
"Wikipedia Lookup": "Пошук у Вікіпедії",
|
||||
"Read Aloud Selection": "Прочитати вибране вголос",
|
||||
"Proofread Selection": "Перевірити вибране",
|
||||
"Open Settings": "Відкрити налаштування",
|
||||
"Open Command Palette": "Відкрити палітру команд",
|
||||
"Show Keyboard Shortcuts": "Показати комбінації клавіш",
|
||||
"Open Books": "Відкрити книги",
|
||||
"Toggle Fullscreen": "Перемкнути повноекранний режим",
|
||||
"Close Window": "Закрити вікно",
|
||||
"Quit App": "Вийти з програми",
|
||||
"Go Left / Previous Page": "Вліво / Попередня сторінка",
|
||||
"Go Right / Next Page": "Вправо / Наступна сторінка",
|
||||
"Go Up": "Вгору",
|
||||
"Go Down": "Вниз",
|
||||
"Previous Chapter": "Попередній розділ",
|
||||
"Next Chapter": "Наступний розділ",
|
||||
"Scroll Half Page Down": "Прокрутити на півсторінки вниз",
|
||||
"Scroll Half Page Up": "Прокрутити на півсторінки вгору",
|
||||
"Save Note": "Зберегти нотатку",
|
||||
"Single Section Scroll": "Прокрутка одного розділу",
|
||||
"General": "Загальне",
|
||||
"Text to Speech": "Озвучення",
|
||||
"Zoom": "Масштаб",
|
||||
"Window": "Вікно",
|
||||
"Your Bookshelf": "Ваша книжкова полиця",
|
||||
"TTS": "Озвучування",
|
||||
"Media Info": "Медіа-інформація",
|
||||
"Update Frequency": "Частота оновлення",
|
||||
"Every Sentence": "Кожне речення",
|
||||
"Every Paragraph": "Кожен абзац",
|
||||
"Every Chapter": "Кожен розділ",
|
||||
"TTS Media Info Update Frequency": "Частота оновлення медіа-інформації TTS",
|
||||
"Select reading speed": "Виберіть швидкість читання",
|
||||
"Drag to seek": "Перетягніть для пошуку",
|
||||
"Punctuation Delay": "Затримка на розділових знаках",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Підтвердіть, що це з'єднання OPDS буде проксовано через сервери Readest у веб-додатку, перш ніж продовжити.",
|
||||
"Custom Headers": "Користувацькі заголовки",
|
||||
"Custom Headers (optional)": "Користувацькі заголовки (необов'язково)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Додайте один заголовок на рядок у форматі \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Я розумію, що це з'єднання OPDS буде проксовано через сервери Readest у веб-додатку. Якщо я не довіряю Readest свої облікові дані або заголовки, мені слід використовувати рідний додаток.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Не вдалося підключитися до Hardcover. Перевірте мережеве з'єднання.",
|
||||
"Invalid Hardcover API token": "Недійсний API-токен Hardcover",
|
||||
"Disconnected from Hardcover": "Від'єднано від Hardcover",
|
||||
"Hardcover Settings": "Налаштування Hardcover",
|
||||
"Connected to Hardcover": "Підключено до Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Підключіть свій обліковий запис Hardcover для синхронізації прогресу читання та нотаток.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Отримайте API-токен на hardcover.app → Налаштування → API.",
|
||||
"API Token": "API-токен",
|
||||
"Paste your Hardcover API token": "Вставте ваш API-токен Hardcover",
|
||||
"Hardcover sync enabled for this book": "Синхронізацію Hardcover увімкнено для цієї книги",
|
||||
"Hardcover sync disabled for this book": "Синхронізацію Hardcover вимкнено для цієї книги",
|
||||
"Hardcover Sync": "Синхронізація Hardcover",
|
||||
"Enable for This Book": "Увімкнути для цієї книги",
|
||||
"Push Notes": "Надіслати нотатки",
|
||||
"Enable Hardcover sync for this book first.": "Спочатку увімкніть синхронізацію Hardcover для цієї книги.",
|
||||
"No annotations or excerpts to sync for this book.": "Немає анотацій або витягів для синхронізації для цієї книги.",
|
||||
"Configure Hardcover in Settings first.": "Спочатку налаштуйте Hardcover у параметрах.",
|
||||
"No new Hardcover note changes to sync.": "Немає нових змін нотаток Hardcover для синхронізації.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover синхронізовано: {{inserted}} нових, {{updated}} оновлених, {{skipped}} без змін",
|
||||
"Hardcover notes sync failed: {{error}}": "Помилка синхронізації нотаток Hardcover: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Прогрес читання синхронізовано з Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Помилка синхронізації прогресу Hardcover: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Зберегти наявний ідентифікатор джерела"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Chế độ viết",
|
||||
"Your Library": "Thư viện của bạn",
|
||||
"TTS not supported for PDF": "TTS không được hỗ trợ cho PDF",
|
||||
"Override Book Font": "Ghi đè phông chữ sách",
|
||||
"Apply to All Books": "Áp dụng cho tất cả sách",
|
||||
"Apply to This Book": "Áp dụng cho sách này",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "Hướng RTL",
|
||||
"Maximum Column Height": "Chiều cao cột tối đa",
|
||||
"Maximum Column Width": "Chiều rộng cột tối đa",
|
||||
"Continuous Scroll": "Cuộn liên tục",
|
||||
"Fullscreen": "Toàn màn hình",
|
||||
"No supported files found. Supported formats: {{formats}}": "Không tìm thấy tệp được hỗ trợ. Định dạng được hỗ trợ: {{formats}}",
|
||||
"Drop to Import Books": "Kéo và thả để nhập sách",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "Xóa tìm kiếm",
|
||||
"Clear search history": "Xóa lịch sử tìm kiếm",
|
||||
"Tap to Toggle Footer": "Nhấn để bật/tắt chân trang",
|
||||
"Show Current Time": "Hiển thị thời gian hiện tại",
|
||||
"Use 24 Hour Clock": "Sử dụng định dạng 24 giờ",
|
||||
"Show Current Battery Status": "Hiển thị trạng thái pin hiện tại",
|
||||
"Exported successfully": "Xuất thành công",
|
||||
"Book exported successfully.": "Sách đã được xuất thành công.",
|
||||
"Failed to export the book.": "Xuất sách thất bại.",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "từ",
|
||||
"{{time}} left": "còn lại {{time}}",
|
||||
"Reading progress": "Tiến độ đọc",
|
||||
"Click to seek": "Nhấp để tua",
|
||||
"Skip back 15 words": "Lùi lại 15 từ",
|
||||
"Back 15 words (Shift+Left)": "Lùi lại 15 từ (Shift+Trái)",
|
||||
"Pause (Space)": "Tạm dừng (Khoảng trắng)",
|
||||
"Play (Space)": "Phát (Khoảng trắng)",
|
||||
"Skip forward 15 words": "Tiến tới 15 từ",
|
||||
"Forward 15 words (Shift+Right)": "Tiến tới 15 từ (Shift+Phải)",
|
||||
"Pause:": "Tạm dừng:",
|
||||
"Decrease speed": "Giảm tốc độ",
|
||||
"Slower (Left/Down)": "Chậm hơn (Trái/Xuống)",
|
||||
"Current speed": "Tốc độ hiện tại",
|
||||
"Increase speed": "Tăng tốc độ",
|
||||
"Faster (Right/Up)": "Nhanh hơn (Phải/Lên)",
|
||||
"Start RSVP Reading": "Bắt đầu đọc RSVP",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "Độ sáng màn hình hệ thống",
|
||||
"Page:": "Trang:",
|
||||
"Page: {{number}}": "Trang: {{number}}",
|
||||
"Annotation page number": "Số trang chú thích"
|
||||
"Annotation page number": "Số trang chú thích",
|
||||
"Translating...": "Đang dịch...",
|
||||
"Show Battery Percentage": "Hiển thị phần trăm pin",
|
||||
"Hide Scrollbar": "Ẩn thanh cuộn",
|
||||
"Skip to last reading position": "Chuyển đến vị trí đọc cuối",
|
||||
"Show context": "Hiển thị ngữ cảnh",
|
||||
"Hide context": "Ẩn ngữ cảnh",
|
||||
"Decrease font size": "Giảm cỡ chữ",
|
||||
"Increase font size": "Tăng cỡ chữ",
|
||||
"Focus": "Tiêu điểm",
|
||||
"Theme color": "Màu chủ đề",
|
||||
"Import failed": "Nhập thất bại",
|
||||
"Available Formatters:": "Bộ định dạng có sẵn:",
|
||||
"Format date": "Định dạng ngày",
|
||||
"Markdown block quote (> per line)": "Trích dẫn khối Markdown (> mỗi dòng)",
|
||||
"Newlines to <br>": "Xuống dòng thành <br>",
|
||||
"Change case": "Đổi chữ hoa/thường",
|
||||
"Trim whitespace": "Cắt khoảng trắng",
|
||||
"Truncate to n characters": "Cắt ngắn còn n ký tự",
|
||||
"Replace text": "Thay thế văn bản",
|
||||
"Fallback value": "Giá trị dự phòng",
|
||||
"Get length": "Lấy độ dài",
|
||||
"First/last element": "Phần tử đầu/cuối",
|
||||
"Join array": "Nối mảng",
|
||||
"Backup failed: {{error}}": "Sao lưu thất bại: {{error}}",
|
||||
"Select Backup": "Chọn bản sao lưu",
|
||||
"Restore failed: {{error}}": "Khôi phục thất bại: {{error}}",
|
||||
"Backup & Restore": "Sao lưu & Khôi phục",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Tạo bản sao lưu thư viện hoặc khôi phục từ bản sao lưu trước đó. Khôi phục sẽ hợp nhất với thư viện hiện tại của bạn.",
|
||||
"Backup Library": "Sao lưu thư viện",
|
||||
"Restore Library": "Khôi phục thư viện",
|
||||
"Creating backup...": "Đang tạo bản sao lưu...",
|
||||
"Restoring library...": "Đang khôi phục thư viện...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} mục",
|
||||
"Backup completed successfully!": "Sao lưu thành công!",
|
||||
"Restore completed successfully!": "Khôi phục thành công!",
|
||||
"Your library has been saved to the selected location.": "Thư viện của bạn đã được lưu tại vị trí đã chọn.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Đã thêm {{added}} sách, cập nhật {{updated}} sách.",
|
||||
"Operation failed": "Thao tác thất bại",
|
||||
"Loading library...": "Đang tải thư viện...",
|
||||
"{{count}} books refreshed_other": "Đã làm mới {{count}} sách",
|
||||
"Failed to refresh metadata": "Không thể làm mới siêu dữ liệu",
|
||||
"Refresh Metadata": "Làm mới siêu dữ liệu",
|
||||
"Keyboard Shortcuts": "Phím tắt",
|
||||
"View all keyboard shortcuts": "Xem tất cả phím tắt",
|
||||
"Switch Sidebar Tab": "Chuyển tab thanh bên",
|
||||
"Toggle Notebook": "Hiện/ẩn sổ ghi chú",
|
||||
"Search in Book": "Tìm kiếm trong sách",
|
||||
"Toggle Scroll Mode": "Chuyển chế độ cuộn",
|
||||
"Toggle Select Mode": "Chuyển chế độ chọn",
|
||||
"Toggle Bookmark": "Bật/tắt đánh dấu",
|
||||
"Toggle Text to Speech": "Bật/tắt đọc văn bản",
|
||||
"Play / Pause TTS": "Phát / Tạm dừng TTS",
|
||||
"Toggle Paragraph Mode": "Chuyển chế độ đoạn văn",
|
||||
"Highlight Selection": "Tô sáng vùng chọn",
|
||||
"Underline Selection": "Gạch chân vùng chọn",
|
||||
"Annotate Selection": "Thêm ghi chú cho vùng chọn",
|
||||
"Search Selection": "Tìm kiếm vùng chọn",
|
||||
"Copy Selection": "Sao chép vùng chọn",
|
||||
"Translate Selection": "Dịch vùng chọn",
|
||||
"Dictionary Lookup": "Tra từ điển",
|
||||
"Wikipedia Lookup": "Tra cứu Wikipedia",
|
||||
"Read Aloud Selection": "Đọc to vùng chọn",
|
||||
"Proofread Selection": "Kiểm tra vùng chọn",
|
||||
"Open Settings": "Mở cài đặt",
|
||||
"Open Command Palette": "Mở bảng lệnh",
|
||||
"Show Keyboard Shortcuts": "Hiển thị phím tắt",
|
||||
"Open Books": "Mở sách",
|
||||
"Toggle Fullscreen": "Chuyển toàn màn hình",
|
||||
"Close Window": "Đóng cửa sổ",
|
||||
"Quit App": "Thoát ứng dụng",
|
||||
"Go Left / Previous Page": "Sang trái / Trang trước",
|
||||
"Go Right / Next Page": "Sang phải / Trang tiếp",
|
||||
"Go Up": "Lên trên",
|
||||
"Go Down": "Xuống dưới",
|
||||
"Previous Chapter": "Chương trước",
|
||||
"Next Chapter": "Chương tiếp",
|
||||
"Scroll Half Page Down": "Cuộn xuống nửa trang",
|
||||
"Scroll Half Page Up": "Cuộn lên nửa trang",
|
||||
"Save Note": "Lưu ghi chú",
|
||||
"Single Section Scroll": "Cuộn từng phần",
|
||||
"General": "Chung",
|
||||
"Text to Speech": "Chuyển văn bản thành giọng nói",
|
||||
"Zoom": "Thu phóng",
|
||||
"Window": "Cửa sổ",
|
||||
"Your Bookshelf": "Tủ sách của bạn",
|
||||
"TTS": "Đọc văn bản",
|
||||
"Media Info": "Thông tin phương tiện",
|
||||
"Update Frequency": "Tần suất cập nhật",
|
||||
"Every Sentence": "Mỗi câu",
|
||||
"Every Paragraph": "Mỗi đoạn văn",
|
||||
"Every Chapter": "Mỗi chương",
|
||||
"TTS Media Info Update Frequency": "Tần suất cập nhật thông tin phương tiện TTS",
|
||||
"Select reading speed": "Chọn tốc độ đọc",
|
||||
"Drag to seek": "Kéo để tìm kiếm",
|
||||
"Punctuation Delay": "Độ trễ dấu câu",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "Vui lòng xác nhận rằng kết nối OPDS này sẽ được chuyển tiếp qua máy chủ Readest trên ứng dụng web trước khi tiếp tục.",
|
||||
"Custom Headers": "Tiêu đề tùy chỉnh",
|
||||
"Custom Headers (optional)": "Tiêu đề tùy chỉnh (tùy chọn)",
|
||||
"Add one header per line using \"Header-Name: value\".": "Thêm một tiêu đề mỗi dòng sử dụng \"Header-Name: value\".",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "Tôi hiểu rằng kết nối OPDS này sẽ được chuyển tiếp qua máy chủ Readest trên ứng dụng web. Nếu tôi không tin tưởng Readest với các thông tin đăng nhập hoặc tiêu đề này, tôi nên sử dụng ứng dụng gốc.",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "Không thể kết nối với Hardcover. Vui lòng kiểm tra kết nối mạng của bạn.",
|
||||
"Invalid Hardcover API token": "Token API Hardcover không hợp lệ",
|
||||
"Disconnected from Hardcover": "Đã ngắt kết nối khỏi Hardcover",
|
||||
"Hardcover Settings": "Cài đặt Hardcover",
|
||||
"Connected to Hardcover": "Đã kết nối với Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "Kết nối tài khoản Hardcover của bạn để đồng bộ tiến trình đọc và ghi chú.",
|
||||
"Get your API token from hardcover.app → Settings → API.": "Lấy token API của bạn từ hardcover.app → Cài đặt → API.",
|
||||
"API Token": "Token API",
|
||||
"Paste your Hardcover API token": "Dán token API Hardcover của bạn",
|
||||
"Hardcover sync enabled for this book": "Đồng bộ Hardcover đã được bật cho cuốn sách này",
|
||||
"Hardcover sync disabled for this book": "Đồng bộ Hardcover đã được tắt cho cuốn sách này",
|
||||
"Hardcover Sync": "Đồng bộ Hardcover",
|
||||
"Enable for This Book": "Bật cho cuốn sách này",
|
||||
"Push Notes": "Gửi ghi chú",
|
||||
"Enable Hardcover sync for this book first.": "Bật đồng bộ Hardcover cho cuốn sách này trước.",
|
||||
"No annotations or excerpts to sync for this book.": "Không có chú thích hoặc trích đoạn nào để đồng bộ cho cuốn sách này.",
|
||||
"Configure Hardcover in Settings first.": "Cấu hình Hardcover trong Cài đặt trước.",
|
||||
"No new Hardcover note changes to sync.": "Không có thay đổi ghi chú Hardcover mới để đồng bộ.",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Đồng bộ Hardcover: {{inserted}} mới, {{updated}} đã cập nhật, {{skipped}} không đổi",
|
||||
"Hardcover notes sync failed: {{error}}": "Đồng bộ ghi chú Hardcover thất bại: {{error}}",
|
||||
"Reading progress synced to Hardcover": "Tiến trình đọc đã đồng bộ với Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Đồng bộ tiến trình Hardcover thất bại: {{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "Giữ mã định danh nguồn hiện tại"
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"Sans-Serif Font": "无衬线字体",
|
||||
"Save": "保存",
|
||||
"Scrolled Mode": "滚动模式",
|
||||
"Hide Scrollbar": "隐藏滚动条",
|
||||
"Search": "搜索",
|
||||
"Search Books...": "搜索书籍...",
|
||||
"Search...": "搜索...",
|
||||
@@ -106,7 +107,6 @@
|
||||
"Wikipedia": "维基百科",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "书库",
|
||||
"TTS not supported for PDF": "PDF 文档暂不支持 TTS",
|
||||
"Override Book Font": "覆盖书籍字体",
|
||||
"Apply to All Books": "应用于所有书籍",
|
||||
"Apply to This Book": "应用于此书籍",
|
||||
@@ -185,7 +185,6 @@
|
||||
"RTL Direction": "从右向左",
|
||||
"Maximum Column Height": "最大列高",
|
||||
"Maximum Column Width": "最大列宽",
|
||||
"Continuous Scroll": "连续滚动",
|
||||
"Fullscreen": "全屏显示",
|
||||
"No supported files found. Supported formats: {{formats}}": "未找到支持的文件。支持的格式:{{formats}}",
|
||||
"Drop to Import Books": "拖放导入书籍",
|
||||
@@ -822,6 +821,9 @@
|
||||
"Clear search": "清除搜索",
|
||||
"Clear search history": "清除搜索历史",
|
||||
"Tap to Toggle Footer": "点击切换页脚",
|
||||
"Show Current Time": "显示当前时间",
|
||||
"Use 24 Hour Clock": "使用 24 小时制",
|
||||
"Show Current Battery Status": "显示当前电量",
|
||||
"Exported successfully": "导出成功",
|
||||
"Book exported successfully.": "书籍导出成功。",
|
||||
"Failed to export the book.": "书籍导出失败。",
|
||||
@@ -969,17 +971,14 @@
|
||||
"words": "词",
|
||||
"{{time}} left": "剩余 {{time}}",
|
||||
"Reading progress": "阅读进度",
|
||||
"Click to seek": "点击跳转",
|
||||
"Skip back 15 words": "回退 15 词",
|
||||
"Back 15 words (Shift+Left)": "回退 15 词 (Shift+Left)",
|
||||
"Pause (Space)": "暂停 (空格)",
|
||||
"Play (Space)": "播放 (空格)",
|
||||
"Skip forward 15 words": "前进 15 词",
|
||||
"Forward 15 words (Shift+Right)": "前进 15 词 (Shift+Right)",
|
||||
"Pause:": "暂停:",
|
||||
"Decrease speed": "减速",
|
||||
"Slower (Left/Down)": "较慢 (左/下)",
|
||||
"Current speed": "当前速度",
|
||||
"Increase speed": "加速",
|
||||
"Faster (Right/Up)": "较快 (右/上)",
|
||||
"Start RSVP Reading": "开始 RSVP 阅读",
|
||||
@@ -1047,5 +1046,128 @@
|
||||
"System Screen Brightness": "系统屏幕亮度",
|
||||
"Page:": "页面:",
|
||||
"Page: {{number}}": "页面: {{number}}",
|
||||
"Annotation page number": "批注页码"
|
||||
"Annotation page number": "批注页码",
|
||||
"Translating...": "翻译中...",
|
||||
"Show Battery Percentage": "显示电量百分比",
|
||||
"Skip to last reading position": "跳转到上次阅读位置",
|
||||
"Show context": "显示上下文",
|
||||
"Hide context": "隐藏上下文",
|
||||
"Decrease font size": "缩小字号",
|
||||
"Increase font size": "放大字号",
|
||||
"Focus": "焦点",
|
||||
"Theme color": "主题颜色",
|
||||
"Import failed": "导入失败",
|
||||
"Available Formatters:": "可用格式化器:",
|
||||
"Format date": "格式化日期",
|
||||
"Markdown block quote (> per line)": "Markdown 引用块(每行 >)",
|
||||
"Newlines to <br>": "换行转为 <br>",
|
||||
"Change case": "更改大小写",
|
||||
"Trim whitespace": "去除空格",
|
||||
"Truncate to n characters": "截断为 n 个字符",
|
||||
"Replace text": "替换文本",
|
||||
"Fallback value": "默认值",
|
||||
"Get length": "获取长度",
|
||||
"First/last element": "第一个/最后一个元素",
|
||||
"Join array": "合并数组",
|
||||
"Backup failed: {{error}}": "备份失败:{{error}}",
|
||||
"Select Backup": "选择备份",
|
||||
"Restore failed: {{error}}": "恢复失败:{{error}}",
|
||||
"Backup & Restore": "备份与恢复",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "创建书库备份或从之前的备份恢复。恢复将与当前书库合并。",
|
||||
"Backup Library": "备份书库",
|
||||
"Restore Library": "恢复书库",
|
||||
"Creating backup...": "正在创建备份...",
|
||||
"Restoring library...": "正在恢复书库...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 项",
|
||||
"Backup completed successfully!": "备份成功!",
|
||||
"Restore completed successfully!": "恢复成功!",
|
||||
"Your library has been saved to the selected location.": "您的书库已保存到所选位置。",
|
||||
"{{added}} books added, {{updated}} books updated.": "新增 {{added}} 本书,更新 {{updated}} 本书。",
|
||||
"Operation failed": "操作失败",
|
||||
"Loading library...": "正在加载书库...",
|
||||
"{{count}} books refreshed_other": "已刷新 {{count}} 本书",
|
||||
"Failed to refresh metadata": "刷新元数据失败",
|
||||
"Refresh Metadata": "刷新元数据",
|
||||
"Keyboard Shortcuts": "键盘快捷键",
|
||||
"View all keyboard shortcuts": "查看所有键盘快捷键",
|
||||
"Switch Sidebar Tab": "切换侧边栏标签",
|
||||
"Toggle Notebook": "显示/隐藏笔记本",
|
||||
"Search in Book": "在书中搜索",
|
||||
"Toggle Scroll Mode": "切换滚动模式",
|
||||
"Toggle Select Mode": "切换选择模式",
|
||||
"Toggle Bookmark": "切换书签",
|
||||
"Toggle Text to Speech": "切换文字转语音",
|
||||
"Play / Pause TTS": "播放 / 暂停朗读",
|
||||
"Toggle Paragraph Mode": "切换段落模式",
|
||||
"Highlight Selection": "高亮选中内容",
|
||||
"Underline Selection": "为选中内容添加下划线",
|
||||
"Annotate Selection": "为选中内容添加注释",
|
||||
"Search Selection": "搜索选中内容",
|
||||
"Copy Selection": "复制选中内容",
|
||||
"Translate Selection": "翻译选中内容",
|
||||
"Dictionary Lookup": "词典查询",
|
||||
"Wikipedia Lookup": "维基百科查询",
|
||||
"Read Aloud Selection": "朗读选中内容",
|
||||
"Proofread Selection": "校对选中内容",
|
||||
"Open Settings": "打开设置",
|
||||
"Open Command Palette": "打开命令面板",
|
||||
"Show Keyboard Shortcuts": "显示键盘快捷键",
|
||||
"Open Books": "打开书库",
|
||||
"Toggle Fullscreen": "切换全屏",
|
||||
"Close Window": "关闭窗口",
|
||||
"Quit App": "退出应用",
|
||||
"Go Left / Previous Page": "向左 / 上一页",
|
||||
"Go Right / Next Page": "向右 / 下一页",
|
||||
"Go Up": "向上",
|
||||
"Go Down": "向下",
|
||||
"Previous Chapter": "上一章",
|
||||
"Next Chapter": "下一章",
|
||||
"Scroll Half Page Down": "向下滚动半页",
|
||||
"Scroll Half Page Up": "向上滚动半页",
|
||||
"Save Note": "保存笔记",
|
||||
"Single Section Scroll": "单章节滚动",
|
||||
"General": "常规",
|
||||
"Text to Speech": "文字转语音",
|
||||
"Zoom": "缩放",
|
||||
"Window": "窗口",
|
||||
"Your Bookshelf": "你的书架",
|
||||
"TTS": "语音朗读",
|
||||
"Media Info": "媒体信息",
|
||||
"Update Frequency": "更新频率",
|
||||
"Every Sentence": "每句",
|
||||
"Every Paragraph": "每段",
|
||||
"Every Chapter": "每章",
|
||||
"TTS Media Info Update Frequency": "TTS 媒体信息更新频率",
|
||||
"Select reading speed": "选择朗读速度",
|
||||
"Drag to seek": "拖动以定位",
|
||||
"Punctuation Delay": "标点符号停顿",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "继续前请确认此 OPDS 连接将通过网页应用中的 Readest 服务器进行代理。",
|
||||
"Custom Headers": "自定义标头",
|
||||
"Custom Headers (optional)": "自定义标头(可选)",
|
||||
"Add one header per line using \"Header-Name: value\".": "每行添加一个标头,格式为 \"Header-Name: value\"。",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "我了解此 OPDS 连接将通过网页应用中的 Readest 服务器进行代理。如果我不信任 Readest 处理这些凭据或标头,我应该使用原生应用。",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "无法连接到 Hardcover。请检查您的网络连接。",
|
||||
"Invalid Hardcover API token": "无效的 Hardcover API 令牌",
|
||||
"Disconnected from Hardcover": "已断开与 Hardcover 的连接",
|
||||
"Hardcover Settings": "Hardcover 设置",
|
||||
"Connected to Hardcover": "已连接到 Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "连接您的 Hardcover 账户以同步阅读进度和笔记。",
|
||||
"Get your API token from hardcover.app → Settings → API.": "从 hardcover.app → 设置 → API 获取您的 API 令牌。",
|
||||
"API Token": "API 令牌",
|
||||
"Paste your Hardcover API token": "粘贴您的 Hardcover API 令牌",
|
||||
"Hardcover sync enabled for this book": "已为此书启用 Hardcover 同步",
|
||||
"Hardcover sync disabled for this book": "已为此书禁用 Hardcover 同步",
|
||||
"Hardcover Sync": "Hardcover 同步",
|
||||
"Enable for This Book": "为此书启用",
|
||||
"Push Notes": "推送笔记",
|
||||
"Enable Hardcover sync for this book first.": "请先为此书启用 Hardcover 同步。",
|
||||
"No annotations or excerpts to sync for this book.": "此书没有可同步的注释或摘录。",
|
||||
"Configure Hardcover in Settings first.": "请先在设置中配置 Hardcover。",
|
||||
"No new Hardcover note changes to sync.": "没有新的 Hardcover 笔记更改需要同步。",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover 已同步:{{inserted}} 个新增、{{updated}} 个更新、{{skipped}} 个未变",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover 笔记同步失败:{{error}}",
|
||||
"Reading progress synced to Hardcover": "阅读进度已同步到 Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover 进度同步失败:{{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "保留现有来源标识符"
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"Wikipedia": "維基百科",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "書庫",
|
||||
"TTS not supported for PDF": "PDF 文檔暫不支持 TTS",
|
||||
"Override Book Font": "覆蓋書籍字體",
|
||||
"Apply to All Books": "應用於所有書籍",
|
||||
"Apply to This Book": "應用於此書籍",
|
||||
@@ -185,7 +184,6 @@
|
||||
"RTL Direction": "從右向左",
|
||||
"Maximum Column Height": "最大列高",
|
||||
"Maximum Column Width": "最大列寬",
|
||||
"Continuous Scroll": "連續滾動",
|
||||
"Fullscreen": "全螢幕",
|
||||
"No supported files found. Supported formats: {{formats}}": "未找到支援的檔案。支援的格式:{{formats}}",
|
||||
"Drop to Import Books": "拖放導入書籍",
|
||||
@@ -822,6 +820,9 @@
|
||||
"Clear search": "清除搜尋",
|
||||
"Clear search history": "清除搜尋歷史",
|
||||
"Tap to Toggle Footer": "點擊切換頁尾",
|
||||
"Show Current Time": "顯示目前時間",
|
||||
"Use 24 Hour Clock": "使用 24 小時制",
|
||||
"Show Current Battery Status": "顯示當前電池狀態",
|
||||
"Exported successfully": "匯出成功",
|
||||
"Book exported successfully.": "書籍匯出成功。",
|
||||
"Failed to export the book.": "書籍匯出失敗。",
|
||||
@@ -969,17 +970,14 @@
|
||||
"words": "詞",
|
||||
"{{time}} left": "剩餘 {{time}}",
|
||||
"Reading progress": "閱讀進度",
|
||||
"Click to seek": "點擊跳轉",
|
||||
"Skip back 15 words": "回退 15 詞",
|
||||
"Back 15 words (Shift+Left)": "回退 15 詞 (Shift+Left)",
|
||||
"Pause (Space)": "暫停 (空格)",
|
||||
"Play (Space)": "播放 (空格)",
|
||||
"Skip forward 15 words": "前進 15 詞",
|
||||
"Forward 15 words (Shift+Right)": "前進 15 詞 (Shift+Right)",
|
||||
"Pause:": "暫停:",
|
||||
"Decrease speed": "減速",
|
||||
"Slower (Left/Down)": "較慢 (左/下)",
|
||||
"Current speed": "當前速度",
|
||||
"Increase speed": "加速",
|
||||
"Faster (Right/Up)": "較快 (右/上)",
|
||||
"Start RSVP Reading": "開始 RSVP 閱讀",
|
||||
@@ -1047,5 +1045,129 @@
|
||||
"System Screen Brightness": "系統螢幕亮度",
|
||||
"Page:": "頁面:",
|
||||
"Page: {{number}}": "頁面: {{number}}",
|
||||
"Annotation page number": "註解頁碼"
|
||||
"Annotation page number": "註解頁碼",
|
||||
"Translating...": "翻譯中...",
|
||||
"Show Battery Percentage": "顯示電池百分比",
|
||||
"Hide Scrollbar": "隱藏滾動條",
|
||||
"Skip to last reading position": "跳至上次閱讀位置",
|
||||
"Show context": "顯示上下文",
|
||||
"Hide context": "隱藏上下文",
|
||||
"Decrease font size": "縮小字型",
|
||||
"Increase font size": "放大字型",
|
||||
"Focus": "焦點",
|
||||
"Theme color": "主題顏色",
|
||||
"Import failed": "匯入失敗",
|
||||
"Available Formatters:": "可用格式化器:",
|
||||
"Format date": "格式化日期",
|
||||
"Markdown block quote (> per line)": "Markdown 引用區塊(每行 >)",
|
||||
"Newlines to <br>": "換行轉為 <br>",
|
||||
"Change case": "變更大小寫",
|
||||
"Trim whitespace": "去除空格",
|
||||
"Truncate to n characters": "截斷為 n 個字元",
|
||||
"Replace text": "取代文字",
|
||||
"Fallback value": "預設值",
|
||||
"Get length": "取得長度",
|
||||
"First/last element": "第一個/最後一個元素",
|
||||
"Join array": "合併陣列",
|
||||
"Backup failed: {{error}}": "備份失敗:{{error}}",
|
||||
"Select Backup": "選擇備份",
|
||||
"Restore failed: {{error}}": "還原失敗:{{error}}",
|
||||
"Backup & Restore": "備份與還原",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "建立書庫備份或從先前的備份還原。還原將與目前的書庫合併。",
|
||||
"Backup Library": "備份書庫",
|
||||
"Restore Library": "還原書庫",
|
||||
"Creating backup...": "正在建立備份...",
|
||||
"Restoring library...": "正在還原書庫...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 項",
|
||||
"Backup completed successfully!": "備份成功!",
|
||||
"Restore completed successfully!": "還原成功!",
|
||||
"Your library has been saved to the selected location.": "您的書庫已儲存至所選位置。",
|
||||
"{{added}} books added, {{updated}} books updated.": "新增 {{added}} 本書,更新 {{updated}} 本書。",
|
||||
"Operation failed": "操作失敗",
|
||||
"Loading library...": "正在載入書庫...",
|
||||
"{{count}} books refreshed_other": "已重新整理 {{count}} 本書",
|
||||
"Failed to refresh metadata": "重新整理中繼資料失敗",
|
||||
"Refresh Metadata": "重新整理中繼資料",
|
||||
"Keyboard Shortcuts": "鍵盤快捷鍵",
|
||||
"View all keyboard shortcuts": "查看所有鍵盤快捷鍵",
|
||||
"Switch Sidebar Tab": "切換側邊欄標籤",
|
||||
"Toggle Notebook": "顯示/隱藏筆記本",
|
||||
"Search in Book": "在書中搜尋",
|
||||
"Toggle Scroll Mode": "切換捲動模式",
|
||||
"Toggle Select Mode": "切換選取模式",
|
||||
"Toggle Bookmark": "切換書籤",
|
||||
"Toggle Text to Speech": "切換文字轉語音",
|
||||
"Play / Pause TTS": "播放 / 暫停朗讀",
|
||||
"Toggle Paragraph Mode": "切換段落模式",
|
||||
"Highlight Selection": "醒目標示選取內容",
|
||||
"Underline Selection": "為選取內容加底線",
|
||||
"Annotate Selection": "為選取內容加註釋",
|
||||
"Search Selection": "搜尋選取內容",
|
||||
"Copy Selection": "複製選取內容",
|
||||
"Translate Selection": "翻譯選取內容",
|
||||
"Dictionary Lookup": "字典查詢",
|
||||
"Wikipedia Lookup": "維基百科查詢",
|
||||
"Read Aloud Selection": "朗讀選取內容",
|
||||
"Proofread Selection": "校對選取內容",
|
||||
"Open Settings": "開啟設定",
|
||||
"Open Command Palette": "開啟指令面板",
|
||||
"Show Keyboard Shortcuts": "顯示鍵盤快捷鍵",
|
||||
"Open Books": "開啟書庫",
|
||||
"Toggle Fullscreen": "切換全螢幕",
|
||||
"Close Window": "關閉視窗",
|
||||
"Quit App": "結束應用程式",
|
||||
"Go Left / Previous Page": "向左 / 上一頁",
|
||||
"Go Right / Next Page": "向右 / 下一頁",
|
||||
"Go Up": "向上",
|
||||
"Go Down": "向下",
|
||||
"Previous Chapter": "上一章",
|
||||
"Next Chapter": "下一章",
|
||||
"Scroll Half Page Down": "向下捲動半頁",
|
||||
"Scroll Half Page Up": "向上捲動半頁",
|
||||
"Save Note": "儲存筆記",
|
||||
"Single Section Scroll": "單章節捲動",
|
||||
"General": "一般",
|
||||
"Text to Speech": "文字轉語音",
|
||||
"Zoom": "縮放",
|
||||
"Window": "視窗",
|
||||
"Your Bookshelf": "你的書架",
|
||||
"TTS": "語音朗讀",
|
||||
"Media Info": "媒體資訊",
|
||||
"Update Frequency": "更新頻率",
|
||||
"Every Sentence": "每句",
|
||||
"Every Paragraph": "每段",
|
||||
"Every Chapter": "每章",
|
||||
"TTS Media Info Update Frequency": "TTS 媒體資訊更新頻率",
|
||||
"Select reading speed": "選擇朗讀速度",
|
||||
"Drag to seek": "拖曳以定位",
|
||||
"Punctuation Delay": "標點符號停頓",
|
||||
"Please confirm that this OPDS connection will be proxied through Readest servers on the web app before continuing.": "繼續前請確認此 OPDS 連線將透過網頁應用程式中的 Readest 伺服器進行代理。",
|
||||
"Custom Headers": "自訂標頭",
|
||||
"Custom Headers (optional)": "自訂標頭(選填)",
|
||||
"Add one header per line using \"Header-Name: value\".": "每行新增一個標頭,格式為 \"Header-Name: value\"。",
|
||||
"I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.": "我了解此 OPDS 連線將透過網頁應用程式中的 Readest 伺服器進行代理。若我不信任 Readest 處理這些憑證或標頭,我應該使用原生應用程式。",
|
||||
"Unable to connect to Hardcover. Please check your network connection.": "無法連線到 Hardcover。請檢查您的網路連線。",
|
||||
"Invalid Hardcover API token": "無效的 Hardcover API 權杖",
|
||||
"Disconnected from Hardcover": "已中斷與 Hardcover 的連線",
|
||||
"Hardcover Settings": "Hardcover 設定",
|
||||
"Connected to Hardcover": "已連線到 Hardcover",
|
||||
"Connect your Hardcover account to sync reading progress and notes.": "連結您的 Hardcover 帳戶以同步閱讀進度和筆記。",
|
||||
"Get your API token from hardcover.app → Settings → API.": "從 hardcover.app → 設定 → API 取得您的 API 權杖。",
|
||||
"API Token": "API 權杖",
|
||||
"Paste your Hardcover API token": "貼上您的 Hardcover API 權杖",
|
||||
"Hardcover sync enabled for this book": "已為此書啟用 Hardcover 同步",
|
||||
"Hardcover sync disabled for this book": "已為此書停用 Hardcover 同步",
|
||||
"Hardcover Sync": "Hardcover 同步",
|
||||
"Enable for This Book": "為此書啟用",
|
||||
"Push Notes": "推送筆記",
|
||||
"Enable Hardcover sync for this book first.": "請先為此書啟用 Hardcover 同步。",
|
||||
"No annotations or excerpts to sync for this book.": "此書沒有可同步的註釋或摘錄。",
|
||||
"Configure Hardcover in Settings first.": "請先在設定中配置 Hardcover。",
|
||||
"No new Hardcover note changes to sync.": "沒有新的 Hardcover 筆記變更需要同步。",
|
||||
"Hardcover synced: {{inserted}} new, {{updated}} updated, {{skipped}} unchanged": "Hardcover 已同步:{{inserted}} 個新增、{{updated}} 個更新、{{skipped}} 個未變",
|
||||
"Hardcover notes sync failed: {{error}}": "Hardcover 筆記同步失敗:{{error}}",
|
||||
"Reading progress synced to Hardcover": "閱讀進度已同步到 Hardcover",
|
||||
"Hardcover progress sync failed: {{error}}": "Hardcover 進度同步失敗:{{error}}",
|
||||
"ISBN": "ISBN",
|
||||
"Keep existing source identifier": "保留現有來源識別碼"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.10.2": {
|
||||
"date": "2026-04-05",
|
||||
"notes": [
|
||||
"Reading: Added single section scroll option, keyboard shortcuts help dialog, and reading ruler improvements",
|
||||
"Reading: Fixed Adobe font deobfuscation and applied theme colors to PDFs",
|
||||
"Reading: Fixed paginator freeze, image sizing on resize, and vertical writing mode compatibility",
|
||||
"Annotations: Fixed annotations grouped under wrong chapter and added highlight opacity setting",
|
||||
"Text-to-Speech: Added dedicated TTS settings tab with highlight opacity and keyboard shortcuts for navigation",
|
||||
"Sync: Added Hardcover.app sync integration and improved KOSync progress accuracy",
|
||||
"KOReader Plugin: Added full annotation sync option and fixed sync-related crashes",
|
||||
"OPDS: Added custom catalog headers with web proxy consent and fixed feed display issues",
|
||||
"Speed Reading: Added per-chapter progress bar and speed selector dropdown",
|
||||
"Android: Added D-pad navigation for Android TV remote controller",
|
||||
"Theme: Added atmosphere Easter egg with video overlay and ambient audio"
|
||||
]
|
||||
},
|
||||
"0.10.1": {
|
||||
"date": "2026-03-22",
|
||||
"notes": [
|
||||
"Reading: Added continuous scroll and spread layout support for EPUBs",
|
||||
"Reading: Added current time and battery display in footer",
|
||||
"Reading: Added TTS, annotation support, pinch-to-zoom, and scroll mode for PDFs",
|
||||
"Library: Added backup/restore to a zip file and batch metadata refresh",
|
||||
"KOReader: Added bookmark and annotation sync between KOReader and Readest devices",
|
||||
"Footnotes: Added back navigation with link history and more responsive popup sizing",
|
||||
"Annotations: Added loupe display when selecting text in instant annotation mode",
|
||||
"Text-to-Speech: Fixed TTS reading position tracking in scrolled mode"
|
||||
]
|
||||
},
|
||||
"0.9.101": {
|
||||
"date": "2026-02-28",
|
||||
"notes": [
|
||||
|
||||
@@ -38,8 +38,9 @@ if grep -q 'MANAGE_EXTERNAL_STORAGE' "$MANIFEST"; then
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🚀 Running: pnpm tauri android build"
|
||||
pnpm tauri android build --config src-tauri/tauri.playstore.conf.json
|
||||
source .env.google-play.local
|
||||
echo "🚀 Running: pnpm tauri android build (googleplay flavor)"
|
||||
ORG_GRADLE_PROJECT_storeFlavor=googleplay pnpm tauri android build --config src-tauri/tauri.playstore.conf.json
|
||||
|
||||
# --- ADD PERMISSION BACK AFTER BUILD ---
|
||||
if ! grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST"; then
|
||||
@@ -56,11 +57,27 @@ if ! grep -q 'MANAGE_EXTERNAL_STORAGE' "$MANIFEST"; then
|
||||
" "$MANIFEST"
|
||||
fi
|
||||
|
||||
source .env.google-play.local
|
||||
if [[ -z "$GOOGLE_PLAY_JSON_KEY_FILE" ]]; then
|
||||
echo "❌ GOOGLE_PLAY_JSON_KEY_FILE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- GENERATE CHANGELOG FOR GOOGLE PLAY ---
|
||||
CHANGELOG_DIR="../../fastlane/metadata/android/en-US/changelogs"
|
||||
mkdir -p "$CHANGELOG_DIR"
|
||||
|
||||
NOTES=$(jq -r --arg ver "$VERSION" '.releases[$ver].notes // empty | map("• " + .) | join("\n")' release-notes.json)
|
||||
if [[ -n "$NOTES" ]]; then
|
||||
# Google Play has a 500-character limit for release notes per language
|
||||
if [[ ${#NOTES} -gt 480 ]]; then
|
||||
NOTES="${NOTES:0:477}..."
|
||||
fi
|
||||
echo "$NOTES" > "$CHANGELOG_DIR/default.txt"
|
||||
echo "📝 Release notes for v$VERSION written to changelogs/default.txt"
|
||||
else
|
||||
echo "⚠️ No release notes found for v$VERSION in release-notes.json"
|
||||
fi
|
||||
|
||||
cd ../../
|
||||
|
||||
fastlane android upload_production
|
||||
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Starts a Next.js dev server, launches the Tauri app with webdriver
|
||||
# (no file watcher, no built-in dev server), waits for the WebDriver
|
||||
# server on port 4445, runs tests, then tears down everything cleanly.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
DEV_PORT=3000
|
||||
WEBDRIVER_PORT=4445
|
||||
POLL_INTERVAL=3
|
||||
TIMEOUT=300
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${TAURI_PID:-}" ]]; then
|
||||
pkill -P "$TAURI_PID" 2>/dev/null || true
|
||||
kill "$TAURI_PID" 2>/dev/null || true
|
||||
wait "$TAURI_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "${DEV_PID:-}" ]]; then
|
||||
pkill -P "$DEV_PID" 2>/dev/null || true
|
||||
kill "$DEV_PID" 2>/dev/null || true
|
||||
wait "$DEV_PID" 2>/dev/null || true
|
||||
fi
|
||||
lsof -ti :"$WEBDRIVER_PORT" 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
lsof -ti :"$DEV_PORT" 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
echo "Starting Next.js dev server..."
|
||||
dotenv -e .env.tauri -- next dev &
|
||||
DEV_PID=$!
|
||||
|
||||
echo "Waiting for dev server on port $DEV_PORT..."
|
||||
elapsed=0
|
||||
while ! curl -sf "http://localhost:${DEV_PORT}" >/dev/null 2>&1; do
|
||||
if ! kill -0 "$DEV_PID" 2>/dev/null; then
|
||||
echo "ERROR: Dev server exited unexpectedly."
|
||||
exit 1
|
||||
fi
|
||||
if (( elapsed >= TIMEOUT )); then
|
||||
echo "ERROR: Timed out waiting for dev server on port $DEV_PORT."
|
||||
exit 1
|
||||
fi
|
||||
sleep "$POLL_INTERVAL"
|
||||
(( elapsed += POLL_INTERVAL ))
|
||||
done
|
||||
|
||||
echo "Starting Tauri app with webdriver (no-watch, skip beforeDevCommand)..."
|
||||
dotenv -e .env.tauri -- tauri dev --features webdriver --no-watch \
|
||||
--config '{"build":{"beforeDevCommand":""}}' &
|
||||
TAURI_PID=$!
|
||||
|
||||
echo "Waiting for WebDriver server on port $WEBDRIVER_PORT (timeout ${TIMEOUT}s)..."
|
||||
elapsed=0
|
||||
while ! curl -sf "http://127.0.0.1:${WEBDRIVER_PORT}/status" >/dev/null 2>&1; do
|
||||
if ! kill -0 "$TAURI_PID" 2>/dev/null; then
|
||||
echo "ERROR: Tauri app exited before WebDriver became ready."
|
||||
exit 1
|
||||
fi
|
||||
if (( elapsed >= TIMEOUT )); then
|
||||
echo "ERROR: Timed out waiting for WebDriver on port $WEBDRIVER_PORT."
|
||||
exit 1
|
||||
fi
|
||||
sleep "$POLL_INTERVAL"
|
||||
(( elapsed += POLL_INTERVAL ))
|
||||
done
|
||||
|
||||
echo "WebDriver is ready. Running Tauri tests..."
|
||||
pnpm vitest --config vitest.tauri.config.mts --watch=false
|
||||
@@ -17,6 +17,8 @@ crate-type = ["staticlib", "cdylib", "lib"]
|
||||
[features]
|
||||
# Internal feature to suppress warnings from old objc crate
|
||||
cargo-clippy = []
|
||||
# Enable WebDriver plugin for E2E testing (use with `tauri build --debug --features webdriver`)
|
||||
webdriver = ["tauri-plugin-webdriver"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
@@ -55,6 +57,9 @@ tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
|
||||
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
|
||||
tauri-plugin-websocket = "2"
|
||||
tauri-plugin-sharekit = "0.3"
|
||||
tauri-plugin-device-info = "1.0.1"
|
||||
tauri-plugin-turso = { path = "./plugins/tauri-plugin-turso" }
|
||||
tauri-plugin-webdriver = { version = "0.2", optional = true }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"identifier": "webdriver-testing",
|
||||
"description": "Grants plugin permissions to remote URLs for Vitest browser-mode tests. Only loaded at runtime when the webdriver feature is enabled.",
|
||||
"remote": {
|
||||
"urls": ["http://127.0.0.1:*", "http://localhost:*"]
|
||||
},
|
||||
"local": false,
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"fs:default",
|
||||
"fs:read-all",
|
||||
"fs:write-all",
|
||||
"fs:scope-appconfig-recursive",
|
||||
"fs:scope-appdata-recursive",
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
{ "path": "**/.readest-test-sandbox-tauri" },
|
||||
{ "path": "**/.readest-test-sandbox-tauri/**" },
|
||||
{ "path": "**/Readest" },
|
||||
{ "path": "**/Readest/**" }
|
||||
]
|
||||
},
|
||||
"os:default",
|
||||
"dialog:default",
|
||||
"core:window:default",
|
||||
"core:path:allow-resolve-directory",
|
||||
"log:default",
|
||||
"shell:default",
|
||||
"process:default",
|
||||
"turso:default",
|
||||
"native-tts:default",
|
||||
"native-bridge:default"
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,16 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"fs:default",
|
||||
"fs:read-meta",
|
||||
"fs:allow-open",
|
||||
"fs:allow-write",
|
||||
"fs:allow-read",
|
||||
"fs:allow-rename",
|
||||
"fs:allow-mkdir",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-stat",
|
||||
"fs:allow-fstat",
|
||||
"fs:allow-lstat",
|
||||
{
|
||||
"identifier": "fs:scope-appconfig-recursive",
|
||||
"allow": [
|
||||
@@ -70,6 +80,9 @@
|
||||
{
|
||||
"path": "**/Readest/**/*"
|
||||
},
|
||||
{
|
||||
"path": "**/com.github.johnfactotum.Foliate/**/*"
|
||||
},
|
||||
{
|
||||
"path": "**/last-book-cover.png"
|
||||
}
|
||||
@@ -178,6 +191,8 @@
|
||||
"haptics:allow-selection-feedback",
|
||||
"deep-link:default",
|
||||
"sharekit:default",
|
||||
"device-info:default",
|
||||
"turso:default",
|
||||
"native-tts:default",
|
||||
"native-bridge:default"
|
||||
]
|
||||
|
||||
@@ -29,6 +29,8 @@ android {
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
val storeFlavor = project.findProperty("storeFlavor")?.toString() ?: "foss"
|
||||
missingDimensionStrategy("store", storeFlavor)
|
||||
}
|
||||
signingConfigs {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
@@ -85,9 +87,10 @@ dependencies {
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
|
||||
+12
-1
@@ -30,10 +30,21 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
flavorDimensions += "store"
|
||||
productFlavors {
|
||||
create("foss") {
|
||||
dimension = "store"
|
||||
}
|
||||
create("googleplay") {
|
||||
dimension = "store"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.android.billingclient:billing-ktx:7.1.1")
|
||||
"googleplayImplementation"("com.android.billingclient:billing-ktx:7.1.1")
|
||||
"googleplayImplementation"("com.google.android.gms:play-services-base:18.5.0")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.readest.native_bridge
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
|
||||
class BillingManager(private val activity: Activity) {
|
||||
companion object {
|
||||
private const val TAG = "BillingManager"
|
||||
}
|
||||
|
||||
fun isBillingAvailable(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun initialize(callback: (Boolean) -> Unit) {
|
||||
Log.d(TAG, "Google Play billing not available in this build")
|
||||
callback(false)
|
||||
}
|
||||
|
||||
fun fetchProducts(productIds: List<String>, callback: (List<ProductData>) -> Unit) {
|
||||
callback(emptyList())
|
||||
}
|
||||
|
||||
fun purchaseProduct(productId: String, callback: (PurchaseData?) -> Unit) {
|
||||
callback(null)
|
||||
}
|
||||
|
||||
fun restorePurchases(callback: (List<PurchaseData>) -> Unit) {
|
||||
callback(emptyList())
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -836,8 +836,18 @@ class NativeBridgePlugin: Plugin {
|
||||
return invoke.reject("URI and destination path must be provided")
|
||||
}
|
||||
|
||||
guard let uri = URL(string: uriString) else {
|
||||
return invoke.reject("Invalid URI")
|
||||
let uri: URL
|
||||
if uriString.hasPrefix("file://") {
|
||||
let path = String(uriString.dropFirst("file://".count))
|
||||
guard let decodedPath = path.removingPercentEncoding else {
|
||||
return invoke.reject("Invalid URI encoding")
|
||||
}
|
||||
uri = URL(fileURLWithPath: decodedPath)
|
||||
} else {
|
||||
guard let parsed = URL(string: uriString) else {
|
||||
return invoke.reject("Invalid URI")
|
||||
}
|
||||
uri = parsed
|
||||
}
|
||||
|
||||
let fileManager = FileManager.default
|
||||
|
||||
+120
-48
@@ -2,6 +2,8 @@ package com.readest.native_tts
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
@@ -110,25 +112,33 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
companion object {
|
||||
private const val TAG = "NativeTTSPlugin"
|
||||
private const val CHANNEL_NAME = "tts_events"
|
||||
private const val IDLE_TIMEOUT_MS = 30L * 60 * 1000 // 30 minutes
|
||||
var NOTIFICATION_TITLE = "Read Aloud"
|
||||
var NOTIFICATION_TEXT = "Ready to read aloud"
|
||||
var FOREGROUND_SERVICE_TITLE = "Read Aloud"
|
||||
var FOREGROUND_SERVICE_TEXT = "Ready to read aloud"
|
||||
}
|
||||
|
||||
|
||||
private var textToSpeech: TextToSpeech? = null
|
||||
private var isInitialized = AtomicBoolean(false)
|
||||
private var isPaused = AtomicBoolean(false)
|
||||
private var isSpeaking = AtomicBoolean(false)
|
||||
private var currentRate = AtomicReference<Float>(1.0f)
|
||||
private var currentPitch = AtomicReference<Float>(1.0f)
|
||||
|
||||
|
||||
private val eventChannels = ConcurrentHashMap<String, Channel<TTSMessageEvent>>()
|
||||
private val speakingJobs = ConcurrentHashMap<String, Job>()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
private val idleHandler = Handler(Looper.getMainLooper())
|
||||
private val idleShutdownRunnable = Runnable {
|
||||
Log.d(TAG, "Idle timeout reached, shutting down TTS engine to save battery")
|
||||
shutdownTTSEngine()
|
||||
}
|
||||
|
||||
@Command
|
||||
fun init(invoke: Invoke) {
|
||||
cancelIdleTimer()
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val success = initializeTTS()
|
||||
@@ -215,34 +225,46 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
@Command
|
||||
fun speak(invoke: Invoke) {
|
||||
cancelIdleTimer()
|
||||
|
||||
val args = invoke.parseArgs(SpeakArgs::class.java)
|
||||
val text = args.text ?: ""
|
||||
|
||||
|
||||
if (text.isEmpty()) {
|
||||
invoke.reject("Text cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val utteranceId = UUID.randomUUID().toString()
|
||||
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
// Re-initialize TTS engine if it was shut down by the idle timer
|
||||
if (!isInitialized.get()) {
|
||||
val success = initializeTTS()
|
||||
if (!success) {
|
||||
invoke.reject("Failed to re-initialize TTS engine")
|
||||
return@launch
|
||||
}
|
||||
Log.d(TAG, "TTS engine re-initialized after idle shutdown")
|
||||
}
|
||||
|
||||
val eventChannel = Channel<TTSMessageEvent>(Channel.UNLIMITED)
|
||||
eventChannels[utteranceId] = eventChannel
|
||||
|
||||
|
||||
val speakJob = launch {
|
||||
speakText(text, utteranceId, args.preload ?: false)
|
||||
}
|
||||
speakingJobs[utteranceId] = speakJob
|
||||
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("utteranceId", utteranceId)
|
||||
}
|
||||
invoke.resolve(result)
|
||||
|
||||
|
||||
// Start sending events to the frontend
|
||||
startEventStream(utteranceId)
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start speaking", e)
|
||||
invoke.reject("Failed to start speaking: ${e.message}")
|
||||
@@ -317,6 +339,7 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
try {
|
||||
if (textToSpeech?.stop() == TextToSpeech.SUCCESS) {
|
||||
isPaused.set(true)
|
||||
startIdleTimer()
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to pause TTS")
|
||||
@@ -328,6 +351,7 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
@Command
|
||||
fun resume(invoke: Invoke) {
|
||||
cancelIdleTimer()
|
||||
try {
|
||||
isPaused.set(false)
|
||||
invoke.resolve()
|
||||
@@ -346,7 +370,8 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
eventChannels.values.forEach { it.close() }
|
||||
speakingJobs.clear()
|
||||
eventChannels.clear()
|
||||
|
||||
startIdleTimer()
|
||||
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to stop TTS")
|
||||
@@ -377,57 +402,67 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
invoke.reject("Exception setting pitch: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Command
|
||||
fun set_voice(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetVoiceArgs::class.java)
|
||||
try {
|
||||
val voices = textToSpeech?.voices
|
||||
val targetVoice = voices?.find { voice ->
|
||||
val languageTag = voice.locale.toLanguageTag()
|
||||
voice.name == args.voice || (languageTag.contains(voice.name) && languageTag == args.voice)
|
||||
}
|
||||
|
||||
if (targetVoice != null) {
|
||||
val result = textToSpeech?.setVoice(targetVoice)
|
||||
if (result == TextToSpeech.SUCCESS) {
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to set voice: ${args.voice}")
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
if (!isInitialized.get()) {
|
||||
initializeTTS()
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Voice not found: ${args.voice}")
|
||||
val voices = textToSpeech?.voices
|
||||
val targetVoice = voices?.find { voice ->
|
||||
val languageTag = voice.locale.toLanguageTag()
|
||||
voice.name == args.voice || (languageTag.contains(voice.name) && languageTag == args.voice)
|
||||
}
|
||||
|
||||
if (targetVoice != null) {
|
||||
val result = textToSpeech?.setVoice(targetVoice)
|
||||
if (result == TextToSpeech.SUCCESS) {
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to set voice: ${args.voice}")
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Voice not found: ${args.voice}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting voice: ${e.message}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting voice: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_all_voices(invoke: Invoke) {
|
||||
try {
|
||||
val voices = textToSpeech?.voices?.map { voice ->
|
||||
val voiceName = voice.name
|
||||
val language = voice.locale.toLanguageTag()
|
||||
val (id, name) = if (language.contains(voiceName)) {
|
||||
language to language
|
||||
} else {
|
||||
voiceName to voiceName
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
if (!isInitialized.get()) {
|
||||
initializeTTS()
|
||||
}
|
||||
JSObject().apply {
|
||||
put("id", id)
|
||||
put("name", name)
|
||||
put("lang", language)
|
||||
put("disabled", false)
|
||||
val voices = textToSpeech?.voices?.map { voice ->
|
||||
val voiceName = voice.name
|
||||
val language = voice.locale.toLanguageTag()
|
||||
val (id, name) = if (language.contains(voiceName)) {
|
||||
language to language
|
||||
} else {
|
||||
voiceName to voiceName
|
||||
}
|
||||
JSObject().apply {
|
||||
put("id", id)
|
||||
put("name", name)
|
||||
put("lang", language)
|
||||
put("disabled", false)
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("voices", JSONArray(voices))
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("voices", JSONArray(voices))
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting voices: ${e.message}")
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting voices: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +551,7 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
try {
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java)
|
||||
if (active) {
|
||||
cancelIdleTimer()
|
||||
MediaPlaybackService.pluginEventTrigger = { event, data -> trigger(event, data) }
|
||||
MediaPlaybackService.currentTitle = FOREGROUND_SERVICE_TITLE
|
||||
MediaPlaybackService.currentArtist = FOREGROUND_SERVICE_TEXT
|
||||
@@ -530,13 +566,49 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startIdleTimer() {
|
||||
idleHandler.removeCallbacks(idleShutdownRunnable)
|
||||
idleHandler.postDelayed(idleShutdownRunnable, IDLE_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
private fun cancelIdleTimer() {
|
||||
idleHandler.removeCallbacks(idleShutdownRunnable)
|
||||
}
|
||||
|
||||
private fun shutdownTTSEngine() {
|
||||
try {
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java)
|
||||
activity.stopService(intent)
|
||||
MediaPlaybackService.pluginEventTrigger = null
|
||||
|
||||
textToSpeech?.shutdown()
|
||||
textToSpeech = null
|
||||
isInitialized.set(false)
|
||||
isSpeaking.set(false)
|
||||
isPaused.set(false)
|
||||
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
eventChannels.values.forEach { it.close() }
|
||||
speakingJobs.clear()
|
||||
eventChannels.clear()
|
||||
|
||||
Log.d(TAG, "TTS engine shut down due to idle timeout")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during idle TTS shutdown", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
try {
|
||||
cancelIdleTimer()
|
||||
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java)
|
||||
activity.stopService(intent)
|
||||
|
||||
coroutineScope.cancel()
|
||||
textToSpeech?.shutdown()
|
||||
textToSpeech = null
|
||||
isInitialized.set(false)
|
||||
eventChannels.values.forEach { it.close() }
|
||||
eventChannels.clear()
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
|
||||
Submodule apps/readest-app/src-tauri/plugins/tauri-plugin-turso added at 21125f0769
@@ -19,7 +19,7 @@ pub fn read_dir(
|
||||
let scope = app.fs_scope();
|
||||
let path_buf = std::path::PathBuf::from(&path);
|
||||
|
||||
if !scope.is_allowed(&path_buf) {
|
||||
if !scope.is_allowed(&path_buf) && !path_buf.to_string_lossy().contains("Readest") {
|
||||
return Err("Permission denied: Path not in filesystem scope".to_string());
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ pub fn run() {
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.level(log::LevelFilter::Info)
|
||||
.level_for("tracing", log::LevelFilter::Warn)
|
||||
.level_for("tantivy", log::LevelFilter::Warn)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_websocket::init())
|
||||
@@ -184,22 +186,29 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_sharekit::init())
|
||||
.plugin(tauri_plugin_device_info::init())
|
||||
.plugin(tauri_plugin_turso::init())
|
||||
.plugin(tauri_plugin_native_bridge::init())
|
||||
.plugin(tauri_plugin_native_tts::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
let _ = app
|
||||
.get_webview_window("main")
|
||||
.expect("no main window")
|
||||
.set_focus();
|
||||
let files = get_files_from_argv(argv.clone());
|
||||
if !files.is_empty() {
|
||||
allow_file_in_scopes(app, files.clone());
|
||||
}
|
||||
app.emit("single-instance", SingleInstancePayload { args: argv, cwd })
|
||||
.unwrap();
|
||||
}));
|
||||
let builder = builder.plugin(
|
||||
tauri_plugin_single_instance::Builder::new()
|
||||
.callback(move |app, argv, cwd| {
|
||||
let _ = app
|
||||
.get_webview_window("main")
|
||||
.expect("no main window")
|
||||
.set_focus();
|
||||
let files = get_files_from_argv(argv.clone());
|
||||
if !files.is_empty() {
|
||||
allow_file_in_scopes(app, files.clone());
|
||||
}
|
||||
app.emit("single-instance", SingleInstancePayload { args: argv, cwd })
|
||||
.unwrap();
|
||||
})
|
||||
.dbus_id("com.bilingify.readest".to_owned())
|
||||
.build(),
|
||||
);
|
||||
|
||||
let builder = builder.plugin(tauri_plugin_deep_link::init());
|
||||
|
||||
@@ -221,8 +230,19 @@ pub fn run() {
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
let builder = builder.plugin(tauri_plugin_haptics::init());
|
||||
|
||||
#[cfg(feature = "webdriver")]
|
||||
let builder = builder.plugin(tauri_plugin_webdriver::init());
|
||||
|
||||
builder
|
||||
.setup(|#[allow(unused_variables)] app| {
|
||||
// When running with the webdriver feature (E2E/integration tests),
|
||||
// grant all default permissions to remote URLs (http://127.0.0.1:*)
|
||||
// so that Vitest browser-mode tests can call plugin commands.
|
||||
#[cfg(feature = "webdriver")]
|
||||
{
|
||||
use tauri::Manager;
|
||||
app.add_capability(include_str!("../capabilities-extra/webdriver.json"))?;
|
||||
}
|
||||
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
createBookFilter,
|
||||
getBreadcrumbs,
|
||||
getBookSortValue,
|
||||
compareSortValues,
|
||||
createBookSorter,
|
||||
} from '@/app/library/utils/libraryUtils';
|
||||
import { Book } from '@/types/book';
|
||||
import { LibrarySortByType } from '@/types/settings';
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
|
||||
/**
|
||||
* Tests for functions NOT covered by the existing library-utils.test.ts:
|
||||
* - createBookFilter
|
||||
* - getBreadcrumbs
|
||||
* - getBookSortValue
|
||||
* - compareSortValues
|
||||
* - createBookSorter (additional sort-by cases: Author, Format, Series, Published, default)
|
||||
*/
|
||||
|
||||
const createMockBook = (
|
||||
overrides: Partial<Omit<Book, 'metadata'> & { metadata?: Partial<BookMetadata> }> = {},
|
||||
): Book => ({
|
||||
hash: `hash-${Math.random().toString(36).substr(2, 9)}`,
|
||||
format: 'EPUB',
|
||||
title: 'Test Book',
|
||||
author: 'Test Author',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
metadata: { ...overrides.metadata } as BookMetadata,
|
||||
});
|
||||
|
||||
describe('createBookFilter', () => {
|
||||
it('should return true for all books when queryTerm is null', () => {
|
||||
const filter = createBookFilter(null);
|
||||
const book = createMockBook({ title: 'Anything' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for all non-deleted books when queryTerm is empty string', () => {
|
||||
const filter = createBookFilter('');
|
||||
const book = createMockBook({ title: 'Anything' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match by title', () => {
|
||||
const filter = createBookFilter('Moby');
|
||||
const book = createMockBook({ title: 'Moby Dick' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match by author', () => {
|
||||
const filter = createBookFilter('Twain');
|
||||
const book = createMockBook({ title: 'Adventures', author: 'Mark Twain' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match by format', () => {
|
||||
const filter = createBookFilter('PDF');
|
||||
const book = createMockBook({ format: 'PDF', title: 'Some Book' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match by groupName', () => {
|
||||
const filter = createBookFilter('fiction');
|
||||
const book = createMockBook({ title: 'A Book', groupName: 'Science Fiction' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match by description in metadata', () => {
|
||||
const filter = createBookFilter('adventure');
|
||||
const book = createMockBook({
|
||||
title: 'A Book',
|
||||
metadata: { description: 'An adventure tale' },
|
||||
});
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
const filter = createBookFilter('moby');
|
||||
const book = createMockBook({ title: 'MOBY DICK' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match non-matching books', () => {
|
||||
const filter = createBookFilter('nonexistent');
|
||||
const book = createMockBook({ title: 'Real Book', author: 'Real Author' });
|
||||
expect(filter(book)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false for deleted books', () => {
|
||||
const filter = createBookFilter('test');
|
||||
const book = createMockBook({ title: 'test book', deletedAt: Date.now() });
|
||||
expect(filter(book)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle invalid regex gracefully by doing substring match', () => {
|
||||
// The string "[invalid" is an invalid regex pattern
|
||||
const filter = createBookFilter('[invalid');
|
||||
const book = createMockBook({ title: 'A book with [invalid text' });
|
||||
expect(filter(book)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match when invalid regex query does not match', () => {
|
||||
const filter = createBookFilter('[invalid');
|
||||
const book = createMockBook({ title: 'Normal Title', author: 'Normal Author' });
|
||||
expect(filter(book)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should support regex patterns', () => {
|
||||
const filter = createBookFilter('^Moby');
|
||||
const bookMatch = createMockBook({ title: 'Moby Dick' });
|
||||
const bookNoMatch = createMockBook({ title: 'The Moby Dick' });
|
||||
expect(filter(bookMatch)).toBeTruthy();
|
||||
expect(filter(bookNoMatch)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBreadcrumbs', () => {
|
||||
it('should return empty array for empty string', () => {
|
||||
expect(getBreadcrumbs('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return single breadcrumb for single segment', () => {
|
||||
const result = getBreadcrumbs('Library');
|
||||
expect(result).toEqual([{ name: 'Library', path: 'Library' }]);
|
||||
});
|
||||
|
||||
it('should return multiple breadcrumbs for path with separators', () => {
|
||||
const result = getBreadcrumbs('Library/Fiction/Scifi');
|
||||
expect(result).toEqual([
|
||||
{ name: 'Library', path: 'Library' },
|
||||
{ name: 'Fiction', path: 'Library/Fiction' },
|
||||
{ name: 'Scifi', path: 'Library/Fiction/Scifi' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle two segments', () => {
|
||||
const result = getBreadcrumbs('A/B');
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({ name: 'A', path: 'A' });
|
||||
expect(result[1]).toEqual({ name: 'B', path: 'A/B' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBookSortValue', () => {
|
||||
it('should return formatted title for Title sort', () => {
|
||||
const book = createMockBook({ title: 'The Great Book' });
|
||||
const value = getBookSortValue(book, LibrarySortByType.Title);
|
||||
expect(typeof value).toBe('string');
|
||||
expect(value).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return formatted author for Author sort', () => {
|
||||
const book = createMockBook({ author: 'Jane Austen' });
|
||||
const value = getBookSortValue(book, LibrarySortByType.Author);
|
||||
expect(typeof value).toBe('string');
|
||||
expect(value).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return updatedAt for Updated sort', () => {
|
||||
const book = createMockBook({ updatedAt: 12345 });
|
||||
expect(getBookSortValue(book, LibrarySortByType.Updated)).toBe(12345);
|
||||
});
|
||||
|
||||
it('should return createdAt for Created sort', () => {
|
||||
const book = createMockBook({ createdAt: 67890 });
|
||||
expect(getBookSortValue(book, LibrarySortByType.Created)).toBe(67890);
|
||||
});
|
||||
|
||||
it('should return format string for Format sort', () => {
|
||||
const book = createMockBook({ format: 'PDF' });
|
||||
expect(getBookSortValue(book, LibrarySortByType.Format)).toBe('PDF');
|
||||
});
|
||||
|
||||
it('should return published date timestamp for Published sort', () => {
|
||||
const book = createMockBook({ metadata: { published: '2023-01-15' } });
|
||||
const value = getBookSortValue(book, LibrarySortByType.Published);
|
||||
expect(value).toBe(new Date('2023-01-15').getTime());
|
||||
});
|
||||
|
||||
it('should return 0 for Published sort when no published date', () => {
|
||||
const book = createMockBook({ metadata: {} });
|
||||
expect(getBookSortValue(book, LibrarySortByType.Published)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for Published sort when date is invalid', () => {
|
||||
const book = createMockBook({ metadata: { published: 'not-a-date' } });
|
||||
expect(getBookSortValue(book, LibrarySortByType.Published)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return updatedAt for unknown sort type', () => {
|
||||
const book = createMockBook({ updatedAt: 99999 });
|
||||
expect(getBookSortValue(book, 'unknown' as LibrarySortByType)).toBe(99999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareSortValues', () => {
|
||||
it('should compare two strings using locale comparison', () => {
|
||||
const result = compareSortValues('Apple', 'Banana', 'en');
|
||||
expect(result).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('should compare two numbers', () => {
|
||||
expect(compareSortValues(10, 20, 'en')).toBeLessThan(0);
|
||||
expect(compareSortValues(20, 10, 'en')).toBeGreaterThan(0);
|
||||
expect(compareSortValues(10, 10, 'en')).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 when types are mismatched', () => {
|
||||
expect(compareSortValues('hello', 42, 'en')).toBe(0);
|
||||
expect(compareSortValues(42, 'hello', 'en')).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle equal strings', () => {
|
||||
expect(compareSortValues('same', 'same', 'en')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBookSorter - additional cases', () => {
|
||||
it('should sort by author with last-name-first formatting', () => {
|
||||
const books = [
|
||||
createMockBook({ author: 'Jane Austen' }),
|
||||
createMockBook({ author: 'Charles Dickens' }),
|
||||
];
|
||||
const sorter = createBookSorter(LibrarySortByType.Author, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
// Austen < Dickens (by last name)
|
||||
expect(sorted[0]!.author).toBe('Jane Austen');
|
||||
expect(sorted[1]!.author).toBe('Charles Dickens');
|
||||
});
|
||||
|
||||
it('should sort by createdAt', () => {
|
||||
const books = [
|
||||
createMockBook({ title: 'Newer', createdAt: 2000 }),
|
||||
createMockBook({ title: 'Older', createdAt: 1000 }),
|
||||
];
|
||||
const sorter = createBookSorter(LibrarySortByType.Created, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
expect(sorted[0]!.title).toBe('Older');
|
||||
expect(sorted[1]!.title).toBe('Newer');
|
||||
});
|
||||
|
||||
it('should sort by format alphabetically', () => {
|
||||
const books = [createMockBook({ format: 'PDF' }), createMockBook({ format: 'EPUB' })];
|
||||
const sorter = createBookSorter(LibrarySortByType.Format, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
expect(sorted[0]!.format).toBe('EPUB');
|
||||
expect(sorted[1]!.format).toBe('PDF');
|
||||
});
|
||||
|
||||
it('should sort by seriesIndex', () => {
|
||||
const books = [
|
||||
createMockBook({ metadata: { seriesIndex: 3 } }),
|
||||
createMockBook({ metadata: { seriesIndex: 1 } }),
|
||||
createMockBook({ metadata: { seriesIndex: 2 } }),
|
||||
];
|
||||
const sorter = createBookSorter(LibrarySortByType.Series, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
expect(sorted[0]!.metadata?.seriesIndex).toBe(1);
|
||||
expect(sorted[1]!.metadata?.seriesIndex).toBe(2);
|
||||
expect(sorted[2]!.metadata?.seriesIndex).toBe(3);
|
||||
});
|
||||
|
||||
it('should sort by published date', () => {
|
||||
const books = [
|
||||
createMockBook({ metadata: { published: '2023-06-15' } }),
|
||||
createMockBook({ metadata: { published: '2020-01-01' } }),
|
||||
createMockBook({ metadata: { published: '2021-12-31' } }),
|
||||
];
|
||||
const sorter = createBookSorter(LibrarySortByType.Published, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
expect(sorted[0]!.metadata?.published).toBe('2020-01-01');
|
||||
expect(sorted[1]!.metadata?.published).toBe('2021-12-31');
|
||||
expect(sorted[2]!.metadata?.published).toBe('2023-06-15');
|
||||
});
|
||||
|
||||
it('should handle missing published dates in sort (books without date use fallback 0001-01-01)', () => {
|
||||
const books = [
|
||||
createMockBook({ metadata: {} }),
|
||||
createMockBook({ metadata: { published: '2020-01-01' } }),
|
||||
];
|
||||
const sorter = createBookSorter(LibrarySortByType.Published, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
// Book without published date defaults to '0001-01-01', which is earlier
|
||||
expect(sorted[0]!.metadata?.published).toBeUndefined();
|
||||
expect(sorted[1]!.metadata?.published).toBe('2020-01-01');
|
||||
});
|
||||
|
||||
it('should handle invalid published dates', () => {
|
||||
const books = [
|
||||
createMockBook({ metadata: { published: 'not-a-date' } }),
|
||||
createMockBook({ metadata: { published: '2020-01-01' } }),
|
||||
];
|
||||
const sorter = createBookSorter(LibrarySortByType.Published, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
// Invalid date treated like missing, should sort after valid dates
|
||||
expect(sorted[0]!.metadata?.published).toBe('2020-01-01');
|
||||
});
|
||||
|
||||
it('should use updatedAt as default sort for unknown sort type', () => {
|
||||
const books = [
|
||||
createMockBook({ title: 'Newer', updatedAt: 2000 }),
|
||||
createMockBook({ title: 'Older', updatedAt: 1000 }),
|
||||
];
|
||||
const sorter = createBookSorter('unknown', 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
expect(sorted[0]!.title).toBe('Older');
|
||||
expect(sorted[1]!.title).toBe('Newer');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,575 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock dependencies before importing the module under test
|
||||
vi.mock('foliate-js/opds.js', () => ({
|
||||
isOPDSCatalog: vi.fn((type: string) => {
|
||||
return (
|
||||
type.includes('application/atom+xml') &&
|
||||
(type.includes('opds-catalog') || type.includes('opds'))
|
||||
);
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/libs/document', () => ({
|
||||
EXTS: {
|
||||
EPUB: 'epub',
|
||||
PDF: 'pdf',
|
||||
MOBI: 'mobi',
|
||||
AZW: 'azw',
|
||||
AZW3: 'azw3',
|
||||
CBZ: 'cbz',
|
||||
FB2: 'fb2',
|
||||
FBZ: 'fbz',
|
||||
TXT: 'txt',
|
||||
MD: 'md',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/services/environment', () => ({
|
||||
isWebAppPlatform: vi.fn(() => true),
|
||||
isTauriAppPlatform: vi.fn(() => false),
|
||||
getAPIBaseUrl: () => '/api',
|
||||
getNodeAPIBaseUrl: () => '/node-api',
|
||||
getBaseUrl: () => 'https://web.readest.com',
|
||||
getNodeBaseUrl: () => 'https://node.readest.com',
|
||||
isWebDevMode: () => true,
|
||||
}));
|
||||
|
||||
vi.mock('@tauri-apps/plugin-http', () => ({
|
||||
fetch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/opds/utils/opdsReq', () => ({
|
||||
fetchWithAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
groupByArray,
|
||||
parseMediaType,
|
||||
isSearchLink,
|
||||
resolveURL,
|
||||
getFileExtFromPath,
|
||||
MIME,
|
||||
validateOPDSURL,
|
||||
} from '@/app/opds/utils/opdsUtils';
|
||||
import type { OPDSLink } from '@/types/opds';
|
||||
import { fetchWithAuth } from '@/app/opds/utils/opdsReq';
|
||||
|
||||
const mockFetchWithAuth = vi.mocked(fetchWithAuth);
|
||||
|
||||
describe('opdsUtils', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('groupByArray', () => {
|
||||
it('should group items by a single key', () => {
|
||||
const items = [
|
||||
{ name: 'Alice', role: 'admin' },
|
||||
{ name: 'Bob', role: 'user' },
|
||||
{ name: 'Charlie', role: 'admin' },
|
||||
];
|
||||
const grouped = groupByArray(items, (item) => item.role);
|
||||
|
||||
expect(grouped.get('admin')).toHaveLength(2);
|
||||
expect(grouped.get('user')).toHaveLength(1);
|
||||
expect(grouped.get('admin')![0]!.name).toBe('Alice');
|
||||
expect(grouped.get('admin')![1]!.name).toBe('Charlie');
|
||||
});
|
||||
|
||||
it('should group items by multiple keys (array return)', () => {
|
||||
const items = [
|
||||
{ name: 'Alice', tags: ['a', 'b'] },
|
||||
{ name: 'Bob', tags: ['b', 'c'] },
|
||||
];
|
||||
const grouped = groupByArray(items, (item) => item.tags);
|
||||
|
||||
expect(grouped.get('a')).toHaveLength(1);
|
||||
expect(grouped.get('b')).toHaveLength(2);
|
||||
expect(grouped.get('c')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return an empty map for undefined input', () => {
|
||||
const grouped = groupByArray(undefined, (item: string) => item);
|
||||
expect(grouped.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should return an empty map for empty array', () => {
|
||||
const grouped = groupByArray([], (item: string) => item);
|
||||
expect(grouped.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle single-element arrays', () => {
|
||||
const items = [{ id: 1 }];
|
||||
const grouped = groupByArray(items, (item) => item.id);
|
||||
expect(grouped.get(1)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MIME constants', () => {
|
||||
it('should have correct XML MIME type', () => {
|
||||
expect(MIME.XML).toBe('application/xml');
|
||||
});
|
||||
|
||||
it('should have correct ATOM MIME type', () => {
|
||||
expect(MIME.ATOM).toBe('application/atom+xml');
|
||||
});
|
||||
|
||||
it('should have correct XHTML MIME type', () => {
|
||||
expect(MIME.XHTML).toBe('application/xhtml+xml');
|
||||
});
|
||||
|
||||
it('should have correct HTML MIME type', () => {
|
||||
expect(MIME.HTML).toBe('text/html');
|
||||
});
|
||||
|
||||
it('should have correct EPUB MIME type', () => {
|
||||
expect(MIME.EPUB).toBe('application/epub+zip');
|
||||
});
|
||||
|
||||
it('should have correct PDF MIME type', () => {
|
||||
expect(MIME.PDF).toBe('application/pdf');
|
||||
});
|
||||
|
||||
it('should have correct OPENSEARCH MIME type', () => {
|
||||
expect(MIME.OPENSEARCH).toBe('application/opensearchdescription+xml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMediaType', () => {
|
||||
it('should parse a simple media type', () => {
|
||||
const result = parseMediaType('application/atom+xml');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.mediaType).toBe('application/atom+xml');
|
||||
expect(Object.keys(result!.parameters)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should parse media type with parameters', () => {
|
||||
const result = parseMediaType('application/atom+xml; charset=utf-8');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.mediaType).toBe('application/atom+xml');
|
||||
expect(result!.parameters['charset']).toBe('utf-8');
|
||||
});
|
||||
|
||||
it('should parse media type with multiple parameters', () => {
|
||||
const result = parseMediaType('text/html; charset=utf-8; boundary=something');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.mediaType).toBe('text/html');
|
||||
expect(result!.parameters['charset']).toBe('utf-8');
|
||||
expect(result!.parameters['boundary']).toBe('something');
|
||||
});
|
||||
|
||||
it('should parse media type with quoted parameter values', () => {
|
||||
const result = parseMediaType('application/atom+xml; profile="opds-catalog"');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.parameters['profile']).toBe('opds-catalog');
|
||||
});
|
||||
|
||||
it('should lowercase the media type', () => {
|
||||
const result = parseMediaType('Application/Atom+XML');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.mediaType).toBe('application/atom+xml');
|
||||
});
|
||||
|
||||
it('should lowercase parameter names', () => {
|
||||
const result = parseMediaType('text/html; Charset=utf-8');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.parameters['charset']).toBe('utf-8');
|
||||
});
|
||||
|
||||
it('should return null for empty string', () => {
|
||||
expect(parseMediaType('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for undefined input', () => {
|
||||
expect(parseMediaType(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle extra spaces around semicolons', () => {
|
||||
const result = parseMediaType('text/html ; charset=utf-8');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.mediaType).toBe('text/html');
|
||||
expect(result!.parameters['charset']).toBe('utf-8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSearchLink', () => {
|
||||
it('should return true for a search link with OPENSEARCH type', () => {
|
||||
const link: OPDSLink = {
|
||||
rel: 'search',
|
||||
href: '/search',
|
||||
type: MIME.OPENSEARCH,
|
||||
properties: {},
|
||||
};
|
||||
expect(isSearchLink(link)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for a search link with ATOM type', () => {
|
||||
const link: OPDSLink = {
|
||||
rel: 'search',
|
||||
href: '/search',
|
||||
type: MIME.ATOM,
|
||||
properties: {},
|
||||
};
|
||||
expect(isSearchLink(link)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when rel is an array containing "search"', () => {
|
||||
const link: OPDSLink = {
|
||||
rel: ['self', 'search'],
|
||||
href: '/search',
|
||||
type: MIME.OPENSEARCH,
|
||||
properties: {},
|
||||
};
|
||||
expect(isSearchLink(link)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when rel does not include "search"', () => {
|
||||
const link: OPDSLink = {
|
||||
rel: 'self',
|
||||
href: '/search',
|
||||
type: MIME.OPENSEARCH,
|
||||
properties: {},
|
||||
};
|
||||
expect(isSearchLink(link)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when type is not OPENSEARCH or ATOM', () => {
|
||||
const link: OPDSLink = {
|
||||
rel: 'search',
|
||||
href: '/search',
|
||||
type: 'text/html',
|
||||
properties: {},
|
||||
};
|
||||
expect(isSearchLink(link)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when rel is undefined', () => {
|
||||
const link: OPDSLink = {
|
||||
href: '/search',
|
||||
type: MIME.OPENSEARCH,
|
||||
properties: {},
|
||||
};
|
||||
expect(isSearchLink(link)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when rel is an empty array', () => {
|
||||
const link: OPDSLink = {
|
||||
rel: [],
|
||||
href: '/search',
|
||||
type: MIME.ATOM,
|
||||
properties: {},
|
||||
};
|
||||
expect(isSearchLink(link)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveURL', () => {
|
||||
it('should resolve an absolute URL relative to a base URL', () => {
|
||||
const result = resolveURL('/feed/new', 'https://example.com/opds');
|
||||
expect(result).toBe('https://example.com/feed/new');
|
||||
});
|
||||
|
||||
it('should resolve a relative URL relative to a base URL', () => {
|
||||
const result = resolveURL('new', 'https://example.com/opds/feed');
|
||||
expect(result).toBe('https://example.com/opds/new');
|
||||
});
|
||||
|
||||
it('should return empty string for empty url', () => {
|
||||
expect(resolveURL('', 'https://example.com')).toBe('');
|
||||
});
|
||||
|
||||
it('should resolve through proxy URL', () => {
|
||||
const proxyBase = '/api/opds/proxy?url=https%3A%2F%2Fexample.com%2Fopds';
|
||||
const result = resolveURL('/feed/new', proxyBase);
|
||||
expect(result).toBe('https://example.com/feed/new');
|
||||
});
|
||||
|
||||
it('should handle non-scheme relativeTo (path-only)', () => {
|
||||
const result = resolveURL('subdir/file.xml', '/opds/catalog/');
|
||||
// Should use the invalid.invalid root and strip it
|
||||
expect(result).toContain('subdir/file.xml');
|
||||
expect(result).not.toContain('invalid.invalid');
|
||||
});
|
||||
|
||||
it('should return the URL itself when resolution fails', () => {
|
||||
// Suppress console.warn for this test
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const result = resolveURL('://broken', '://also-broken');
|
||||
// Should fall through to the catch and return url
|
||||
expect(result).toBe('://broken');
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle full absolute URLs as the url parameter', () => {
|
||||
const result = resolveURL('https://other.com/feed', 'https://example.com/opds');
|
||||
expect(result).toBe('https://other.com/feed');
|
||||
});
|
||||
|
||||
it('should strip query parameters for non-scheme relativeTo', () => {
|
||||
const result = resolveURL('feed.xml?page=2', '/opds/catalog/');
|
||||
// The function strips search params for non-scheme relativeTo
|
||||
expect(result).not.toContain('page=2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileExtFromPath', () => {
|
||||
it('should find epub extension in path', () => {
|
||||
expect(getFileExtFromPath('/books/epub/my-book')).toBe('epub');
|
||||
});
|
||||
|
||||
it('should find pdf extension in path', () => {
|
||||
expect(getFileExtFromPath('/books/pdf/my-doc')).toBe('pdf');
|
||||
});
|
||||
|
||||
it('should find mobi extension in path', () => {
|
||||
expect(getFileExtFromPath('/books/mobi/my-book')).toBe('mobi');
|
||||
});
|
||||
|
||||
it('should find cbz extension in path', () => {
|
||||
expect(getFileExtFromPath('/comics/cbz/issue1')).toBe('cbz');
|
||||
});
|
||||
|
||||
it('should find fb2 extension in path', () => {
|
||||
expect(getFileExtFromPath('/books/fb2/russian-novel')).toBe('fb2');
|
||||
});
|
||||
|
||||
it('should find txt extension in path', () => {
|
||||
expect(getFileExtFromPath('/texts/txt/readme')).toBe('txt');
|
||||
});
|
||||
|
||||
it('should find md extension in path', () => {
|
||||
expect(getFileExtFromPath('/notes/md/guide')).toBe('md');
|
||||
});
|
||||
|
||||
it('should return empty string when no extension found', () => {
|
||||
expect(getFileExtFromPath('/books/unknown/my-book')).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string for empty path', () => {
|
||||
expect(getFileExtFromPath('')).toBe('');
|
||||
});
|
||||
|
||||
it('should use custom delimiter', () => {
|
||||
expect(getFileExtFromPath('books.epub.my-book', '.')).toBe('epub');
|
||||
});
|
||||
|
||||
it('should return the first matching extension', () => {
|
||||
// If both epub and pdf appear, returns whichever EXTS entry matches first
|
||||
const result = getFileExtFromPath('/books/epub/pdf/file');
|
||||
expect(['epub', 'pdf']).toContain(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateOPDSURL', () => {
|
||||
beforeEach(() => {
|
||||
mockFetchWithAuth.mockReset();
|
||||
// Suppress expected error noise from validation failure tests.
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('should return valid feed for XML OPDS feed', async () => {
|
||||
const xmlFeed = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Test Feed</title>
|
||||
</feed>`;
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/opds',
|
||||
text: () => Promise.resolve(xmlFeed),
|
||||
headers: new Headers({ 'Content-Type': 'application/atom+xml' }),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds');
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data?.type).toBe('feed');
|
||||
expect(result.data?.responseURL).toBe('https://example.com/opds');
|
||||
});
|
||||
|
||||
it('should return valid entry for XML entry document', async () => {
|
||||
const xmlEntry = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Single Book</title>
|
||||
</entry>`;
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/opds/entry',
|
||||
text: () => Promise.resolve(xmlEntry),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds/entry');
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data?.type).toBe('entry');
|
||||
});
|
||||
|
||||
it('should return valid opensearch for OpenSearchDescription', async () => {
|
||||
const xmlOSD = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||
<ShortName>Search</ShortName>
|
||||
</OpenSearchDescription>`;
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/search',
|
||||
text: () => Promise.resolve(xmlOSD),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/search');
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data?.type).toBe('opensearch');
|
||||
});
|
||||
|
||||
it('should return authentication error for 401 status', async () => {
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
url: 'https://example.com/opds',
|
||||
text: () => Promise.resolve(''),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain('Authentication required');
|
||||
});
|
||||
|
||||
it('should return error for non-OK HTTP response', async () => {
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
url: 'https://example.com/opds',
|
||||
text: () => Promise.resolve(''),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain('500');
|
||||
});
|
||||
|
||||
it('should return valid feed for JSON-based OPDS', async () => {
|
||||
const jsonFeed = JSON.stringify({
|
||||
metadata: { title: 'Test Feed' },
|
||||
links: [],
|
||||
publications: [],
|
||||
});
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/opds.json',
|
||||
text: () => Promise.resolve(jsonFeed),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds.json');
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data?.type).toBe('feed');
|
||||
});
|
||||
|
||||
it('should return invalid for JSON without OPDS fields', async () => {
|
||||
const jsonData = JSON.stringify({ foo: 'bar' });
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/data.json',
|
||||
text: () => Promise.resolve(jsonData),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/data.json');
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should return invalid for non-XML, non-JSON content', async () => {
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/page',
|
||||
text: () => Promise.resolve('Just some plain text'),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/page');
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle fetch errors gracefully', async () => {
|
||||
mockFetchWithAuth.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds');
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBe('Network error');
|
||||
});
|
||||
|
||||
it('should handle non-Error exceptions', async () => {
|
||||
mockFetchWithAuth.mockRejectedValue('string error');
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds');
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should return valid for JSON feed with only navigation', async () => {
|
||||
const jsonFeed = JSON.stringify({
|
||||
navigation: [{ title: 'New', href: '/new' }],
|
||||
});
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/opds.json',
|
||||
text: () => Promise.resolve(jsonFeed),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/opds.json');
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should return invalid for XML that is not a recognized OPDS document type', async () => {
|
||||
const xmlDoc = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<catalog>
|
||||
<book>Test</book>
|
||||
</catalog>`;
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/catalog',
|
||||
text: () => Promise.resolve(xmlDoc),
|
||||
headers: new Headers({ 'Content-Type': 'application/xml' }),
|
||||
} as Response);
|
||||
|
||||
const result = await validateOPDSURL('https://example.com/catalog');
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should pass auth and custom headers to fetchWithAuth', async () => {
|
||||
const xmlFeed = `<feed xmlns="http://www.w3.org/2005/Atom"><title>Auth Feed</title></feed>`;
|
||||
const customHeaders = {
|
||||
'CF-Access-Client-Id': 'client-id',
|
||||
'CF-Access-Client-Secret': 'client-secret',
|
||||
};
|
||||
|
||||
mockFetchWithAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
url: 'https://example.com/opds',
|
||||
text: () => Promise.resolve(xmlFeed),
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
|
||||
await validateOPDSURL('https://example.com/opds', 'user', 'pass', true, customHeaders);
|
||||
|
||||
expect(mockFetchWithAuth).toHaveBeenCalledWith(
|
||||
'https://example.com/opds',
|
||||
'user',
|
||||
'pass',
|
||||
true,
|
||||
expect.objectContaining({ signal: expect.anything() }),
|
||||
customHeaders,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,305 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getPlanDetails } from '@/app/user/utils/plan';
|
||||
import { AvailablePlan, UserPlan, PlanInterval, QuotaFeature } from '@/types/quota';
|
||||
import { StripeProductMetadata } from '@/types/payment';
|
||||
|
||||
// getPlanDetails expects (AvailablePlan & StripeAvailablePlan)[].
|
||||
// StripeAvailablePlan = AvailablePlan & { metadata?: StripeProductMetadata; product?: Stripe.Product }
|
||||
// We build the exact shape needed to satisfy the type.
|
||||
|
||||
type TestPlan = AvailablePlan & {
|
||||
metadata?: StripeProductMetadata;
|
||||
};
|
||||
|
||||
function makePlan(overrides: Partial<TestPlan> = {}): TestPlan {
|
||||
return {
|
||||
plan: 'free' as UserPlan,
|
||||
productId: 'prod_test',
|
||||
price: 0,
|
||||
currency: 'USD',
|
||||
interval: 'month' as PlanInterval,
|
||||
productName: 'Test Plan',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('getPlanDetails', () => {
|
||||
describe('free plan', () => {
|
||||
it('should return free plan details with no available plans', () => {
|
||||
const result = getPlanDetails('free', []);
|
||||
expect(result.name).toBe('Free Plan');
|
||||
expect(result.plan).toBe('free');
|
||||
expect(result.type).toBe('subscription');
|
||||
expect(result.price).toBe(0);
|
||||
expect(result.currency).toBe('USD');
|
||||
});
|
||||
|
||||
it('should use currency from available plans', () => {
|
||||
const plans = [makePlan({ plan: 'free', currency: 'EUR' })];
|
||||
const result = getPlanDetails('free', plans);
|
||||
expect(result.currency).toBe('EUR');
|
||||
});
|
||||
|
||||
it('should include features array', () => {
|
||||
const result = getPlanDetails('free', []);
|
||||
expect(result.features.length).toBeGreaterThan(0);
|
||||
const labels = result.features.map((f) => f.label);
|
||||
expect(labels).toContain('Cross-Platform Sync');
|
||||
expect(labels).toContain('AI Read Aloud');
|
||||
});
|
||||
|
||||
it('should include limits', () => {
|
||||
const result = getPlanDetails('free', []);
|
||||
expect(result.limits).toBeDefined();
|
||||
expect(Object.keys(result.limits!).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should use month interval label by default', () => {
|
||||
const result = getPlanDetails('free', []);
|
||||
expect(result.interval).toBe('month');
|
||||
});
|
||||
|
||||
it('should use year interval label when specified', () => {
|
||||
const result = getPlanDetails('free', [], 'year');
|
||||
expect(result.interval).toBe('year');
|
||||
});
|
||||
|
||||
it('should set productId from matching available plan', () => {
|
||||
const plans = [makePlan({ plan: 'free', productId: 'prod_free_123' })];
|
||||
const result = getPlanDetails('free', plans);
|
||||
expect(result.productId).toBe('prod_free_123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plus plan', () => {
|
||||
it('should return plus plan details', () => {
|
||||
const plans = [makePlan({ plan: 'plus', price: 499 })];
|
||||
const result = getPlanDetails('plus', plans);
|
||||
expect(result.name).toBe('Plus Plan');
|
||||
expect(result.plan).toBe('plus');
|
||||
expect(result.type).toBe('subscription');
|
||||
expect(result.price).toBe(499);
|
||||
});
|
||||
|
||||
it('should use default price when no matching plan found', () => {
|
||||
const result = getPlanDetails('plus', []);
|
||||
expect(result.price).toBe(499);
|
||||
});
|
||||
|
||||
it('should include expected features', () => {
|
||||
const result = getPlanDetails('plus', []);
|
||||
const labels = result.features.map((f) => f.label);
|
||||
expect(labels).toContain('Includes All Free Plan Benefits');
|
||||
expect(labels).toContain('Unlimited AI Read Aloud Hours');
|
||||
expect(labels).toContain('Priority Support');
|
||||
});
|
||||
|
||||
it('should include limits with storage and translation', () => {
|
||||
const result = getPlanDetails('plus', []);
|
||||
expect(result.limits).toBeDefined();
|
||||
const limitKeys = Object.keys(result.limits!);
|
||||
expect(limitKeys.some((k) => k.includes('Storage'))).toBe(true);
|
||||
expect(limitKeys.some((k) => k.includes('Translation'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should match correct plan by interval', () => {
|
||||
const plans = [
|
||||
makePlan({ plan: 'plus', price: 499, interval: 'month' }),
|
||||
makePlan({ plan: 'plus', price: 3999, interval: 'year' }),
|
||||
];
|
||||
const monthResult = getPlanDetails('plus', plans, 'month');
|
||||
expect(monthResult.price).toBe(499);
|
||||
|
||||
const yearResult = getPlanDetails('plus', plans, 'year');
|
||||
expect(yearResult.price).toBe(3999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pro plan', () => {
|
||||
it('should return pro plan details', () => {
|
||||
const plans = [makePlan({ plan: 'pro', price: 999 })];
|
||||
const result = getPlanDetails('pro', plans);
|
||||
expect(result.name).toBe('Pro Plan');
|
||||
expect(result.plan).toBe('pro');
|
||||
expect(result.type).toBe('subscription');
|
||||
expect(result.price).toBe(999);
|
||||
});
|
||||
|
||||
it('should use default price when no matching plan found', () => {
|
||||
const result = getPlanDetails('pro', []);
|
||||
expect(result.price).toBe(999);
|
||||
});
|
||||
|
||||
it('should include expected features', () => {
|
||||
const result = getPlanDetails('pro', []);
|
||||
const labels = result.features.map((f) => f.label);
|
||||
expect(labels).toContain('Includes All Plus Plan Benefits');
|
||||
expect(labels).toContain('Early Feature Access');
|
||||
expect(labels).toContain('Advanced AI Tools');
|
||||
});
|
||||
|
||||
it('should have higher storage limit than plus', () => {
|
||||
const proResult = getPlanDetails('pro', []);
|
||||
const plusResult = getPlanDetails('plus', []);
|
||||
|
||||
// Both should have limits
|
||||
expect(proResult.limits).toBeDefined();
|
||||
expect(plusResult.limits).toBeDefined();
|
||||
|
||||
// Find storage limit values
|
||||
const proStorageKey = Object.keys(proResult.limits!).find((k) => k.includes('Storage'));
|
||||
const plusStorageKey = Object.keys(plusResult.limits!).find((k) => k.includes('Storage'));
|
||||
expect(proStorageKey).toBeDefined();
|
||||
expect(plusStorageKey).toBeDefined();
|
||||
|
||||
// Pro should have 20 GB, Plus should have 5 GB
|
||||
expect(proResult.limits![proStorageKey!]).toBe('20 GB');
|
||||
expect(plusResult.limits![plusStorageKey!]).toBe('5 GB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('purchase plan', () => {
|
||||
it('should return purchase plan details', () => {
|
||||
const plans = [makePlan({ plan: 'purchase', price: 1999 })];
|
||||
const result = getPlanDetails('purchase', plans);
|
||||
expect(result.name).toBe('Lifetime Plan');
|
||||
expect(result.plan).toBe('purchase');
|
||||
expect(result.type).toBe('purchase');
|
||||
expect(result.interval).toBe('lifetime');
|
||||
});
|
||||
|
||||
it('should use default price when no matching plan found', () => {
|
||||
const result = getPlanDetails('purchase', []);
|
||||
expect(result.price).toBe(1999);
|
||||
});
|
||||
|
||||
it('should include products sorted by price', () => {
|
||||
const plans = [
|
||||
makePlan({
|
||||
plan: 'purchase',
|
||||
productId: 'prod_expensive',
|
||||
price: 5000,
|
||||
productName: 'Expensive',
|
||||
}),
|
||||
makePlan({ plan: 'purchase', productId: 'prod_cheap', price: 1000, productName: 'Cheap' }),
|
||||
makePlan({ plan: 'purchase', productId: 'prod_mid', price: 3000, productName: 'Mid' }),
|
||||
];
|
||||
const result = getPlanDetails('purchase', plans);
|
||||
expect(result.products).toBeDefined();
|
||||
expect(result.products).toHaveLength(3);
|
||||
expect(result.products![0]!.price).toBe(1000);
|
||||
expect(result.products![1]!.price).toBe(3000);
|
||||
expect(result.products![2]!.price).toBe(5000);
|
||||
});
|
||||
|
||||
it('should derive feature from productId when metadata.feature is missing', () => {
|
||||
const plans = [
|
||||
makePlan({
|
||||
plan: 'purchase',
|
||||
productId: 'prod_storage_100gb',
|
||||
price: 1000,
|
||||
productName: 'Storage',
|
||||
}),
|
||||
makePlan({
|
||||
plan: 'purchase',
|
||||
productId: 'prod_translation_pack',
|
||||
price: 500,
|
||||
productName: 'Translation',
|
||||
}),
|
||||
makePlan({
|
||||
plan: 'purchase',
|
||||
productId: 'prod_tokens_bundle',
|
||||
price: 2000,
|
||||
productName: 'Tokens',
|
||||
}),
|
||||
makePlan({
|
||||
plan: 'purchase',
|
||||
productId: 'prod_customization_pro',
|
||||
price: 1500,
|
||||
productName: 'Custom',
|
||||
}),
|
||||
];
|
||||
const result = getPlanDetails('purchase', plans);
|
||||
expect(result.products![0]!.feature).toBe('translation');
|
||||
expect(result.products![1]!.feature).toBe('storage');
|
||||
expect(result.products![2]!.feature).toBe('customization');
|
||||
expect(result.products![3]!.feature).toBe('tokens');
|
||||
});
|
||||
|
||||
it('should use metadata.feature when present', () => {
|
||||
const plans = [
|
||||
makePlan({
|
||||
plan: 'purchase',
|
||||
productId: 'prod_xyz',
|
||||
price: 1000,
|
||||
productName: 'XYZ',
|
||||
metadata: { plan: 'purchase', feature: 'storage' as QuotaFeature },
|
||||
}),
|
||||
];
|
||||
const result = getPlanDetails('purchase', plans);
|
||||
expect(result.products![0]!.feature).toBe('storage');
|
||||
});
|
||||
|
||||
it('should fall back to generic when productId does not match any feature', () => {
|
||||
const plans = [
|
||||
makePlan({
|
||||
plan: 'purchase',
|
||||
productId: 'prod_unknown_thing',
|
||||
price: 1000,
|
||||
productName: 'Unknown',
|
||||
}),
|
||||
];
|
||||
const result = getPlanDetails('purchase', plans);
|
||||
expect(result.products![0]!.feature).toBe('generic');
|
||||
});
|
||||
|
||||
it('should include expected features', () => {
|
||||
const result = getPlanDetails('purchase', []);
|
||||
const labels = result.features.map((f) => f.label);
|
||||
expect(labels).toContain('One-Time Payment');
|
||||
expect(labels).toContain('Expand Cloud Sync Storage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('default / unknown plan', () => {
|
||||
it('should fall back to free plan for unknown plan code', () => {
|
||||
const result = getPlanDetails('unknown_plan' as UserPlan, []);
|
||||
expect(result.plan).toBe('free');
|
||||
expect(result.name).toBe('Free Plan');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plan color and hintColor', () => {
|
||||
it('should assign distinct colors per plan', () => {
|
||||
const colors: Record<string, string> = {};
|
||||
const planCodes: UserPlan[] = ['free', 'plus', 'pro', 'purchase'];
|
||||
for (const code of planCodes) {
|
||||
const details = getPlanDetails(code, []);
|
||||
colors[code] = details.color;
|
||||
}
|
||||
// Each plan should have a unique color
|
||||
const uniqueColors = new Set(Object.values(colors));
|
||||
expect(uniqueColors.size).toBe(4);
|
||||
});
|
||||
|
||||
it('should provide hintColor for all plans', () => {
|
||||
const planCodes: UserPlan[] = ['free', 'plus', 'pro', 'purchase'];
|
||||
for (const code of planCodes) {
|
||||
const details = getPlanDetails(code, []);
|
||||
expect(details.hintColor).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('plan without interval match', () => {
|
||||
it('should handle plan with no interval specified', () => {
|
||||
// A plan with no interval should match any interval request
|
||||
const plans = [
|
||||
makePlan({ plan: 'purchase', price: 1999, interval: undefined as unknown as PlanInterval }),
|
||||
];
|
||||
// When interval is not set on available plan, it should still be found
|
||||
const result = getPlanDetails('purchase', plans);
|
||||
expect(result.price).toBe(1999);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup, fireEvent } from '@testing-library/react';
|
||||
|
||||
import NumberInput from '@/components/settings/NumberInput';
|
||||
|
||||
vi.mock('@/hooks/useTranslation', () => ({
|
||||
useTranslation: () => (s: string) => s,
|
||||
}));
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('NumberInput', () => {
|
||||
const defaultProps = {
|
||||
label: 'Font Size',
|
||||
value: 16,
|
||||
min: 8,
|
||||
max: 72,
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
|
||||
it('commits value on Enter key (form submit)', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<NumberInput {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const input = screen.getByDisplayValue('16');
|
||||
fireEvent.change(input, { target: { value: '24' } });
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
// Enter triggers form submit which commits the value
|
||||
fireEvent.submit(input.closest('form')!);
|
||||
expect(onChange).toHaveBeenCalledWith(24);
|
||||
});
|
||||
|
||||
it('commits value on blur', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<NumberInput {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const input = screen.getByDisplayValue('16');
|
||||
fireEvent.change(input, { target: { value: '20' } });
|
||||
fireEvent.blur(input);
|
||||
expect(onChange).toHaveBeenCalledWith(20);
|
||||
});
|
||||
|
||||
it('clamps value to min/max on commit', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<NumberInput {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const input = screen.getByDisplayValue('16');
|
||||
fireEvent.change(input, { target: { value: '100' } });
|
||||
fireEvent.submit(input.closest('form')!);
|
||||
expect(onChange).toHaveBeenCalledWith(72);
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,7 @@ describe('ProofreadPopup Component', () => {
|
||||
key: 'test-book',
|
||||
text: 'test word',
|
||||
cfi: 'epubcfi(/6/2[chapter1]!/4/1:0)',
|
||||
index: 0,
|
||||
range: {
|
||||
deleteContents: vi.fn(),
|
||||
insertNode: vi.fn(),
|
||||
@@ -49,7 +50,7 @@ describe('ProofreadPopup Component', () => {
|
||||
startOffset: 5,
|
||||
endOffset: 9,
|
||||
} as unknown as Range,
|
||||
index: 0,
|
||||
page: 1,
|
||||
},
|
||||
position: { point: { x: 100, y: 100 } },
|
||||
trianglePosition: { point: { x: 100, y: 100 } },
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import ReadingRuler from '@/app/reader/components/ReadingRuler';
|
||||
import { ViewSettings } from '@/types/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
const saveViewSettings = vi.fn();
|
||||
|
||||
vi.mock('@/context/EnvContext', () => ({
|
||||
useEnv: () => ({ envConfig: {} }),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/readerStore', () => ({
|
||||
useReaderStore: () => ({
|
||||
getProgress: () => null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/helpers/settings', () => ({
|
||||
saveViewSettings: (...args: unknown[]) => saveViewSettings(...args),
|
||||
}));
|
||||
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
unobserve() {}
|
||||
};
|
||||
|
||||
HTMLElement.prototype.setPointerCapture = vi.fn();
|
||||
HTMLElement.prototype.releasePointerCapture = vi.fn();
|
||||
|
||||
describe('ReadingRuler', () => {
|
||||
const viewSettings = {
|
||||
defaultFontSize: 16,
|
||||
lineHeight: 1.5,
|
||||
} as ViewSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => 1000,
|
||||
});
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
configurable: true,
|
||||
get: () => 800,
|
||||
});
|
||||
|
||||
HTMLElement.prototype.getBoundingClientRect = vi.fn(() => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 800,
|
||||
bottom: 1000,
|
||||
width: 800,
|
||||
height: 1000,
|
||||
toJSON: () => ({}),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('keeps the ruler body pass-through and exposes dedicated drag handles', () => {
|
||||
const { container } = render(
|
||||
<ReadingRuler
|
||||
bookKey='book-1'
|
||||
isVertical={false}
|
||||
rtl={false}
|
||||
lines={2}
|
||||
position={33}
|
||||
opacity={0.5}
|
||||
color='transparent'
|
||||
bookFormat='EPUB'
|
||||
viewSettings={viewSettings}
|
||||
gridInsets={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const ruler = container.querySelector('.ruler');
|
||||
const dragHandles = container.querySelectorAll('.cursor-row-resize');
|
||||
|
||||
expect(ruler?.className).toContain('pointer-events-none');
|
||||
expect(dragHandles).toHaveLength(2);
|
||||
dragHandles.forEach((handle) => {
|
||||
expect(handle.className).toContain('pointer-events-auto');
|
||||
});
|
||||
});
|
||||
|
||||
it('moves and persists the ruler position when a ruler-step event is dispatched', async () => {
|
||||
const { container } = render(
|
||||
<ReadingRuler
|
||||
bookKey='book-1'
|
||||
isVertical={false}
|
||||
rtl={false}
|
||||
lines={2}
|
||||
position={33}
|
||||
opacity={0.5}
|
||||
color='transparent'
|
||||
bookFormat='EPUB'
|
||||
viewSettings={viewSettings}
|
||||
gridInsets={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
/>,
|
||||
);
|
||||
|
||||
eventDispatcher.dispatchSync('reading-ruler-move', {
|
||||
bookKey: 'book-1',
|
||||
direction: 'forward',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const ruler = container.querySelector('.ruler') as HTMLDivElement;
|
||||
const overlays = container.querySelectorAll('.bg-base-100');
|
||||
|
||||
expect(ruler.style.top).toBe('37.8%');
|
||||
expect(ruler.style.transition).toContain('top 0.6s');
|
||||
expect(overlays[0]?.getAttribute('style')).toContain('transition: height 0.6s');
|
||||
expect(overlays[1]?.getAttribute('style')).toContain('transition: height 0.6s');
|
||||
expect(saveViewSettings).toHaveBeenCalledWith(
|
||||
{},
|
||||
'book-1',
|
||||
'readingRulerPosition',
|
||||
37.8,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the drag handle anchored instead of snapping the ruler center to the pointer', () => {
|
||||
const { container } = render(
|
||||
<ReadingRuler
|
||||
bookKey='book-1'
|
||||
isVertical={false}
|
||||
rtl={false}
|
||||
lines={2}
|
||||
position={33}
|
||||
opacity={0.5}
|
||||
color='transparent'
|
||||
bookFormat='EPUB'
|
||||
viewSettings={viewSettings}
|
||||
gridInsets={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const ruler = container.querySelector('.ruler') as HTMLDivElement;
|
||||
const topHandle = container.querySelector('.cursor-row-resize') as HTMLDivElement;
|
||||
|
||||
expect(ruler.style.top).toBe('33%');
|
||||
|
||||
fireEvent.pointerDown(topHandle, { pointerId: 1, clientY: 306 });
|
||||
fireEvent.pointerMove(topHandle, { pointerId: 1, clientY: 316 });
|
||||
|
||||
expect(ruler.style.top).toBe('34%');
|
||||
});
|
||||
|
||||
it('does not consume ruler movement when already at the boundary', () => {
|
||||
render(
|
||||
<ReadingRuler
|
||||
bookKey='book-1'
|
||||
isVertical={false}
|
||||
rtl={false}
|
||||
lines={2}
|
||||
position={97.6}
|
||||
opacity={0.5}
|
||||
color='transparent'
|
||||
bookFormat='EPUB'
|
||||
viewSettings={viewSettings}
|
||||
gridInsets={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const consumed = eventDispatcher.dispatchSync('reading-ruler-move', {
|
||||
bookKey: 'book-1',
|
||||
direction: 'forward',
|
||||
});
|
||||
|
||||
expect(consumed).toBe(false);
|
||||
expect(saveViewSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
|
||||
import { StaticListRow } from '@/app/reader/components/sidebar/TOCItem';
|
||||
import { TOCItem } from '@/libs/document';
|
||||
|
||||
vi.mock('@/utils/misc', () => ({
|
||||
getContentMd5: (s: string) => s,
|
||||
}));
|
||||
|
||||
const makeLeafItem = (overrides?: Partial<TOCItem>): TOCItem => ({
|
||||
id: 1,
|
||||
label: 'Chapter 1',
|
||||
href: 'chapter1.html',
|
||||
index: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeParentItem = (overrides?: Partial<TOCItem>): TOCItem => ({
|
||||
id: 1,
|
||||
label: 'Part One',
|
||||
href: 'part1.html',
|
||||
index: 0,
|
||||
subitems: [{ id: 2, label: 'Chapter 1', href: 'chapter1.html', index: 1 }],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
bookKey: 'book1',
|
||||
activeHref: null,
|
||||
onToggleExpand: vi.fn(),
|
||||
onItemClick: vi.fn(),
|
||||
};
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe('TOCItem accessibility', () => {
|
||||
it('treeitem has aria-label containing the chapter title', () => {
|
||||
const item = makeLeafItem({ label: 'Introduction' });
|
||||
render(<StaticListRow {...defaultProps} flatItem={{ item, depth: 0, index: 0 }} />);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
expect(treeitem.getAttribute('aria-label')).toContain('Introduction');
|
||||
});
|
||||
|
||||
it('treeitem aria-label includes page number when location is available', () => {
|
||||
const item = makeLeafItem({
|
||||
label: 'Chapter 2',
|
||||
location: { current: 4, total: 100 } as TOCItem['location'],
|
||||
});
|
||||
render(<StaticListRow {...defaultProps} flatItem={{ item, depth: 0, index: 0 }} />);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
// aria-label should include chapter title and page number (5 = current+1)
|
||||
expect(treeitem.getAttribute('aria-label')).toContain('Chapter 2');
|
||||
expect(treeitem.getAttribute('aria-label')).toContain('5');
|
||||
});
|
||||
|
||||
it('leaf treeitem does NOT have aria-expanded', () => {
|
||||
const item = makeLeafItem();
|
||||
render(<StaticListRow {...defaultProps} flatItem={{ item, depth: 0, index: 0 }} />);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
expect(treeitem.hasAttribute('aria-expanded')).toBe(false);
|
||||
});
|
||||
|
||||
it('parent treeitem HAS aria-expanded set to false when collapsed', () => {
|
||||
const item = makeParentItem();
|
||||
render(
|
||||
<StaticListRow
|
||||
{...defaultProps}
|
||||
flatItem={{ item, depth: 0, index: 0, isExpanded: false }}
|
||||
/>,
|
||||
);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
expect(treeitem.getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
it('parent treeitem HAS aria-expanded set to true when expanded', () => {
|
||||
const item = makeParentItem();
|
||||
render(
|
||||
<StaticListRow {...defaultProps} flatItem={{ item, depth: 0, index: 0, isExpanded: true }} />,
|
||||
);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
expect(treeitem.getAttribute('aria-expanded')).toBe('true');
|
||||
});
|
||||
|
||||
it('expand button has descriptive aria-label', () => {
|
||||
const item = makeParentItem();
|
||||
render(
|
||||
<StaticListRow
|
||||
{...defaultProps}
|
||||
flatItem={{ item, depth: 0, index: 0, isExpanded: false }}
|
||||
/>,
|
||||
);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button.getAttribute('aria-label')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('page number element is aria-hidden', () => {
|
||||
const item = makeLeafItem({ index: 2 });
|
||||
const { container } = render(
|
||||
<StaticListRow {...defaultProps} flatItem={{ item, depth: 0, index: 0 }} />,
|
||||
);
|
||||
// The page number div should be aria-hidden since it's included in aria-label
|
||||
const pageDiv = container.querySelector('[aria-hidden="true"]');
|
||||
expect(pageDiv).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('aria-current on active treeitem', () => {
|
||||
it('active treeitem has aria-current="page"', () => {
|
||||
const item = makeLeafItem({ href: 'chapter1.html' });
|
||||
render(
|
||||
<StaticListRow
|
||||
{...defaultProps}
|
||||
activeHref='chapter1.html'
|
||||
flatItem={{ item, depth: 0, index: 0 }}
|
||||
/>,
|
||||
);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
expect(treeitem.getAttribute('aria-current')).toBe('page');
|
||||
});
|
||||
|
||||
it('inactive treeitem does NOT have aria-current', () => {
|
||||
const item = makeLeafItem({ href: 'chapter1.html' });
|
||||
render(
|
||||
<StaticListRow
|
||||
{...defaultProps}
|
||||
activeHref='chapter2.html'
|
||||
flatItem={{ item, depth: 0, index: 0 }}
|
||||
/>,
|
||||
);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
expect(treeitem.hasAttribute('aria-current')).toBe(false);
|
||||
});
|
||||
|
||||
it('active treeitem is focusable (tabIndex=0)', () => {
|
||||
const item = makeLeafItem({ href: 'chapter1.html' });
|
||||
render(
|
||||
<StaticListRow
|
||||
{...defaultProps}
|
||||
activeHref='chapter1.html'
|
||||
flatItem={{ item, depth: 0, index: 0 }}
|
||||
/>,
|
||||
);
|
||||
const treeitem = screen.getByRole('treeitem');
|
||||
expect(treeitem.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TOCView tree role', () => {
|
||||
it('static list container has role="tree"', async () => {
|
||||
// This is tested indirectly via TOCView, but we verify the container role
|
||||
// by checking the structure rendered by StaticListRow's wrapper
|
||||
const item = makeLeafItem();
|
||||
render(
|
||||
<div role='tree'>
|
||||
<StaticListRow {...defaultProps} flatItem={{ item, depth: 0, index: 0 }} />
|
||||
</div>,
|
||||
);
|
||||
const tree = screen.getByRole('tree');
|
||||
expect(tree).toBeTruthy();
|
||||
expect(tree.querySelector('[role="treeitem"]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import { cleanup, render } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import useBookShortcuts from '@/app/reader/hooks/useBookShortcuts';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
const shortcutState = {
|
||||
actions: null as Record<
|
||||
string,
|
||||
((event?: KeyboardEvent | MessageEvent) => void) | undefined
|
||||
> | null,
|
||||
};
|
||||
|
||||
const mockView = {
|
||||
book: { dir: 'ltr' },
|
||||
prev: vi.fn(),
|
||||
next: vi.fn(),
|
||||
pan: vi.fn(),
|
||||
renderer: {
|
||||
scrolled: false,
|
||||
setAttribute: vi.fn(),
|
||||
},
|
||||
history: {
|
||||
back: vi.fn(),
|
||||
forward: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const currentViewSettings = {
|
||||
defaultFontSize: 16,
|
||||
lineHeight: 1.5,
|
||||
readingRulerEnabled: true,
|
||||
writingMode: 'horizontal-tb',
|
||||
vertical: false,
|
||||
rtl: false,
|
||||
paragraphMode: { enabled: false },
|
||||
};
|
||||
|
||||
vi.mock('@/store/readerStore', () => ({
|
||||
useReaderStore: () => ({
|
||||
getView: () => mockView,
|
||||
getViewState: () => ({ ttsEnabled: false }),
|
||||
getViewSettings: () => currentViewSettings,
|
||||
setViewSettings: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/sidebarStore', () => ({
|
||||
useSidebarStore: () => ({
|
||||
toggleSideBar: vi.fn(),
|
||||
setSideBarBookKey: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/settingsStore', () => ({
|
||||
useSettingsStore: () => ({
|
||||
setSettingsDialogOpen: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/bookDataStore', () => ({
|
||||
useBookDataStore: () => ({
|
||||
getBookData: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/notebookStore', () => ({
|
||||
useNotebookStore: () => ({
|
||||
toggleNotebook: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/themeStore', () => ({
|
||||
useThemeStore: () => ({
|
||||
safeAreaInsets: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/command-palette', () => ({
|
||||
useCommandPalette: () => ({
|
||||
open: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useShortcuts', () => ({
|
||||
default: (actions: typeof shortcutState.actions) => {
|
||||
shortcutState.actions = actions;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/services/environment', () => ({
|
||||
isTauriAppPlatform: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/window', () => ({
|
||||
tauriHandleClose: vi.fn(),
|
||||
tauriHandleToggleFullScreen: vi.fn(),
|
||||
tauriQuitApp: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/style', () => ({
|
||||
getStyles: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/services/constants', () => ({
|
||||
MAX_ZOOM_LEVEL: 200,
|
||||
MIN_ZOOM_LEVEL: 50,
|
||||
ZOOM_STEP: 10,
|
||||
}));
|
||||
|
||||
vi.mock('@/app/reader/hooks/useBooksManager', () => ({
|
||||
default: () => ({
|
||||
getNextBookKey: () => 'book-1',
|
||||
}),
|
||||
}));
|
||||
|
||||
const Harness = () => {
|
||||
useBookShortcuts({ sideBarBookKey: 'book-1', bookKeys: ['book-1'] });
|
||||
return null;
|
||||
};
|
||||
|
||||
describe('useBookShortcuts', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
shortcutState.actions = null;
|
||||
currentViewSettings.readingRulerEnabled = true;
|
||||
currentViewSettings.writingMode = 'horizontal-tb';
|
||||
currentViewSettings.vertical = false;
|
||||
currentViewSettings.rtl = false;
|
||||
currentViewSettings.paragraphMode.enabled = false;
|
||||
mockView.book.dir = 'ltr';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('routes page-turn shortcuts to reading ruler movement when enabled', () => {
|
||||
const dispatchSpy = vi.spyOn(eventDispatcher, 'dispatchSync').mockReturnValue(true);
|
||||
|
||||
render(<Harness />);
|
||||
shortcutState.actions?.['onGoNext']?.();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('reading-ruler-move', {
|
||||
bookKey: 'book-1',
|
||||
direction: 'forward',
|
||||
});
|
||||
expect(mockView.next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses reading order when directional shortcuts are handled in rtl books', () => {
|
||||
const dispatchSpy = vi.spyOn(eventDispatcher, 'dispatchSync').mockReturnValue(true);
|
||||
mockView.book.dir = 'rtl';
|
||||
|
||||
render(<Harness />);
|
||||
shortcutState.actions?.['onGoRight']?.();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('reading-ruler-move', {
|
||||
bookKey: 'book-1',
|
||||
direction: 'backward',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to normal page navigation when the ruler is disabled', () => {
|
||||
currentViewSettings.readingRulerEnabled = false;
|
||||
|
||||
render(<Harness />);
|
||||
shortcutState.actions?.['onGoNext']?.();
|
||||
|
||||
expect(mockView.next).toHaveBeenCalledWith(72);
|
||||
});
|
||||
|
||||
it('falls back to normal page navigation when the ruler cannot move further', () => {
|
||||
vi.spyOn(eventDispatcher, 'dispatchSync').mockReturnValue(false);
|
||||
|
||||
render(<Harness />);
|
||||
shortcutState.actions?.['onGoNext']?.();
|
||||
|
||||
expect(mockView.next).toHaveBeenCalledWith(72);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { migrate, MigrationEntry } from '@/services/database/migrate';
|
||||
import { DatabaseService, DatabaseExecResult, DatabaseRow } from '@/types/database';
|
||||
|
||||
/**
|
||||
* In-memory DatabaseService for testing the migration runner.
|
||||
* Tracks tables, rows, and PRAGMA user_version.
|
||||
*/
|
||||
function createMockDb(): DatabaseService & {
|
||||
userVersion: number;
|
||||
tables: Map<string, DatabaseRow[]>;
|
||||
} {
|
||||
const tables = new Map<string, DatabaseRow[]>();
|
||||
let userVersion = 0;
|
||||
|
||||
const db: DatabaseService & { userVersion: number; tables: Map<string, DatabaseRow[]> } = {
|
||||
userVersion: 0,
|
||||
tables,
|
||||
|
||||
async execute(sql: string, params: unknown[] = []): Promise<DatabaseExecResult> {
|
||||
const trimmed = sql.trim();
|
||||
|
||||
if (/^CREATE TABLE/i.test(trimmed)) {
|
||||
const tableName = trimmed.match(/CREATE TABLE\s+(?:IF NOT EXISTS\s+)?(\w+)/i)?.[1];
|
||||
if (tableName && !tables.has(tableName)) {
|
||||
tables.set(tableName, []);
|
||||
}
|
||||
return { rowsAffected: 0, lastInsertId: 0 };
|
||||
}
|
||||
|
||||
if (/^INSERT INTO/i.test(trimmed)) {
|
||||
const tableName = trimmed.match(/INTO\s+(\w+)/i)?.[1] ?? '_default';
|
||||
const existing = tables.get(tableName) ?? [];
|
||||
const id = existing.length + 1;
|
||||
|
||||
// Parse VALUES for the migration tracking table
|
||||
const valuesMatch = trimmed.match(/VALUES\s*\(([^)]+)\)/i);
|
||||
if (valuesMatch) {
|
||||
const rawVal =
|
||||
params.length > 0 ? params[0] : valuesMatch[1]!.trim().replace(/^'|'$/g, '');
|
||||
existing.push({ id, name: rawVal });
|
||||
} else {
|
||||
existing.push({ id });
|
||||
}
|
||||
tables.set(tableName, existing);
|
||||
return { rowsAffected: 1, lastInsertId: id };
|
||||
}
|
||||
|
||||
return { rowsAffected: 0, lastInsertId: 0 };
|
||||
},
|
||||
|
||||
async select<T extends DatabaseRow = DatabaseRow>(sql: string): Promise<T[]> {
|
||||
const trimmed = sql.trim();
|
||||
|
||||
if (/^PRAGMA user_version/i.test(trimmed)) {
|
||||
return [{ user_version: userVersion } as unknown as T];
|
||||
}
|
||||
|
||||
const tableName = trimmed.match(/FROM\s+(\w+)/i)?.[1] ?? '_default';
|
||||
return (tables.get(tableName) ?? []) as T[];
|
||||
},
|
||||
|
||||
async batch(statements: string[]): Promise<void> {
|
||||
for (const stmt of statements) {
|
||||
const trimmed = stmt.trim();
|
||||
if (/^PRAGMA user_version\s*=/i.test(trimmed)) {
|
||||
const val = parseInt(trimmed.match(/=\s*(\d+)/)?.[1] ?? '0', 10);
|
||||
userVersion = val;
|
||||
db.userVersion = val;
|
||||
} else {
|
||||
await db.execute(trimmed);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async close(): Promise<void> {
|
||||
tables.clear();
|
||||
},
|
||||
};
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('migrate()', () => {
|
||||
let db: ReturnType<typeof createMockDb>;
|
||||
|
||||
const migrations: MigrationEntry[] = [
|
||||
{
|
||||
name: '2026030601_create_books',
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS books (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT NOT NULL
|
||||
);
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '2026030602_create_annotations',
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS annotations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
book_id INTEGER NOT NULL,
|
||||
text TEXT
|
||||
);
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
db = createMockDb();
|
||||
});
|
||||
|
||||
it('applies all migrations on a fresh database', async () => {
|
||||
await migrate(db, migrations);
|
||||
|
||||
expect(db.tables.has('books')).toBe(true);
|
||||
expect(db.tables.has('annotations')).toBe(true);
|
||||
expect(db.tables.has('__migrations')).toBe(true);
|
||||
expect(db.userVersion).toBe(2);
|
||||
});
|
||||
|
||||
it('records migration names in the tracking table', async () => {
|
||||
await migrate(db, migrations);
|
||||
|
||||
const tracked = db.tables.get('__migrations') ?? [];
|
||||
const names = tracked.map((r) => r['name']);
|
||||
expect(names).toContain('2026030601_create_books');
|
||||
expect(names).toContain('2026030602_create_annotations');
|
||||
});
|
||||
|
||||
it('skips already-applied migrations (idempotent)', async () => {
|
||||
await migrate(db, migrations);
|
||||
const firstRunTracked = (db.tables.get('__migrations') ?? []).length;
|
||||
|
||||
// Run again — should be a no-op via PRAGMA user_version fast-path
|
||||
await migrate(db, migrations);
|
||||
const secondRunTracked = (db.tables.get('__migrations') ?? []).length;
|
||||
|
||||
expect(secondRunTracked).toBe(firstRunTracked);
|
||||
expect(db.userVersion).toBe(2);
|
||||
});
|
||||
|
||||
it('applies only new migrations when schema grows', async () => {
|
||||
// Apply first migration only
|
||||
await migrate(db, [migrations[0]!]);
|
||||
expect(db.userVersion).toBe(1);
|
||||
expect(db.tables.has('books')).toBe(true);
|
||||
expect(db.tables.has('annotations')).toBe(false);
|
||||
|
||||
// Now apply both — only second should run
|
||||
await migrate(db, migrations);
|
||||
expect(db.userVersion).toBe(2);
|
||||
expect(db.tables.has('annotations')).toBe(true);
|
||||
|
||||
const tracked = db.tables.get('__migrations') ?? [];
|
||||
expect(tracked).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does nothing when migrations array is empty', async () => {
|
||||
await migrate(db, []);
|
||||
expect(db.tables.size).toBe(0);
|
||||
expect(db.userVersion).toBe(0);
|
||||
});
|
||||
|
||||
it('uses custom tracking table name', async () => {
|
||||
await migrate(db, migrations, { table: '__custom_migrations' });
|
||||
|
||||
expect(db.tables.has('__custom_migrations')).toBe(true);
|
||||
expect(db.tables.has('__migrations')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles migration names with single quotes', async () => {
|
||||
const trickyMigrations: MigrationEntry[] = [
|
||||
{
|
||||
name: "2026030601_it's_tricky",
|
||||
sql: 'CREATE TABLE IF NOT EXISTS tricky (id INTEGER PRIMARY KEY);',
|
||||
},
|
||||
];
|
||||
|
||||
await migrate(db, trickyMigrations);
|
||||
expect(db.tables.has('tricky')).toBe(true);
|
||||
expect(db.userVersion).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMigrations()', () => {
|
||||
it('returns an array for defined schema types', async () => {
|
||||
const { getMigrations } = await import('@/services/database/migrations');
|
||||
const result = getMigrations('nonexistent_schema');
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,281 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { DatabaseService, DatabaseExecResult, DatabaseRow } from '@/types/database';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock: NativeDatabaseService
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('tauri-plugin-turso', () => {
|
||||
const rows = new Map<string, DatabaseRow[]>();
|
||||
|
||||
const mockDb = {
|
||||
execute: vi.fn(async (sql: string, params: unknown[] = []) => {
|
||||
const insertTable = sql.match(/INTO\s+(\w+)/i)?.[1];
|
||||
const fromTable = sql.match(/FROM\s+(\w+)/i)?.[1];
|
||||
const table = insertTable ?? fromTable ?? '_default';
|
||||
if (/^INSERT/i.test(sql.trim())) {
|
||||
const existing = rows.get(table) ?? [];
|
||||
const id = existing.length + 1;
|
||||
existing.push({ id, value: params[0] ?? null });
|
||||
rows.set(table, existing);
|
||||
return { rowsAffected: 1, lastInsertId: id };
|
||||
}
|
||||
if (/^DELETE/i.test(sql.trim())) {
|
||||
const existing = rows.get(table) ?? [];
|
||||
rows.set(table, []);
|
||||
return { rowsAffected: existing.length, lastInsertId: 0 };
|
||||
}
|
||||
return { rowsAffected: 0, lastInsertId: 0 };
|
||||
}),
|
||||
select: vi.fn(async (sql: string) => {
|
||||
const table = sql.match(/FROM\s+(\w+)/i)?.[1] ?? '_default';
|
||||
return rows.get(table) ?? [];
|
||||
}),
|
||||
batch: vi.fn(async () => {}),
|
||||
close: vi.fn(async () => {
|
||||
rows.clear();
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
Database: {
|
||||
load: vi.fn(async () => mockDb),
|
||||
},
|
||||
__mockDb: mockDb,
|
||||
__rows: rows,
|
||||
};
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock: @tursodatabase/database-wasm
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('@tursodatabase/database-wasm', () => {
|
||||
const rows = new Map<string, DatabaseRow[]>();
|
||||
|
||||
const mockDb = {
|
||||
prepare: vi.fn((sql: string) => ({
|
||||
run: vi.fn((...params: unknown[]) => {
|
||||
const table = sql.match(/INTO\s+(\w+)/i)?.[1] ?? '_default';
|
||||
if (/^INSERT/i.test(sql.trim())) {
|
||||
const existing = rows.get(table) ?? [];
|
||||
const id = existing.length + 1;
|
||||
existing.push({ id, value: params[0] ?? null });
|
||||
rows.set(table, existing);
|
||||
return { changes: 1, lastInsertRowid: id };
|
||||
}
|
||||
if (/^DELETE/i.test(sql.trim())) {
|
||||
const existing = rows.get(table) ?? [];
|
||||
rows.set(table, []);
|
||||
return { changes: existing.length, lastInsertRowid: 0 };
|
||||
}
|
||||
return { changes: 0, lastInsertRowid: 0 };
|
||||
}),
|
||||
all: vi.fn(() => {
|
||||
const table = sql.match(/FROM\s+(\w+)/i)?.[1] ?? '_default';
|
||||
return rows.get(table) ?? [];
|
||||
}),
|
||||
})),
|
||||
exec: vi.fn(),
|
||||
close: vi.fn(() => {
|
||||
rows.clear();
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
connect: vi.fn(() => mockDb),
|
||||
__mockDb: mockDb,
|
||||
__rows: rows,
|
||||
};
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests: NativeDatabaseService
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('NativeDatabaseService', () => {
|
||||
let db: DatabaseService;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const mod = await import('tauri-plugin-turso');
|
||||
(mod as unknown as { __rows: Map<string, DatabaseRow[]> }).__rows.clear();
|
||||
|
||||
const { NativeDatabaseService } = await import('@/services/database/nativeDatabaseService');
|
||||
db = await NativeDatabaseService.open('sqlite:test.db');
|
||||
});
|
||||
|
||||
it('execute() returns DatabaseExecResult for INSERT', async () => {
|
||||
const result: DatabaseExecResult = await db.execute('INSERT INTO items (value) VALUES (?)', [
|
||||
'hello',
|
||||
]);
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('execute() returns DatabaseExecResult for DELETE', async () => {
|
||||
await db.execute('INSERT INTO items (value) VALUES (?)', ['a']);
|
||||
const result = await db.execute('DELETE FROM items');
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
});
|
||||
|
||||
it('select() returns typed row arrays', async () => {
|
||||
await db.execute('INSERT INTO items (value) VALUES (?)', ['alpha']);
|
||||
await db.execute('INSERT INTO items (value) VALUES (?)', ['beta']);
|
||||
|
||||
const rows = await db.select<{ id: number; value: string }>('SELECT * FROM items');
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.id).toBe(1);
|
||||
expect(rows[0]!.value).toBe('alpha');
|
||||
expect(rows[1]!.id).toBe(2);
|
||||
expect(rows[1]!.value).toBe('beta');
|
||||
});
|
||||
|
||||
it('select() returns empty array when no rows', async () => {
|
||||
const rows = await db.select('SELECT * FROM empty_table');
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
it('batch() delegates to underlying db.batch()', async () => {
|
||||
await db.batch(['CREATE TABLE t (id INTEGER)', 'INSERT INTO t VALUES (1)']);
|
||||
const mod = await import('tauri-plugin-turso');
|
||||
const mockDb = (mod as unknown as { __mockDb: { batch: ReturnType<typeof vi.fn> } }).__mockDb;
|
||||
expect(mockDb.batch).toHaveBeenCalledWith([
|
||||
'CREATE TABLE t (id INTEGER)',
|
||||
'INSERT INTO t VALUES (1)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('close() delegates to underlying db.close()', async () => {
|
||||
await db.close();
|
||||
const mod = await import('tauri-plugin-turso');
|
||||
const mockDb = (mod as unknown as { __mockDb: { close: ReturnType<typeof vi.fn> } }).__mockDb;
|
||||
expect(mockDb.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests: WebDatabaseService
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('WebDatabaseService', () => {
|
||||
let db: DatabaseService;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const mod = await import('@tursodatabase/database-wasm');
|
||||
(mod as unknown as { __rows: Map<string, DatabaseRow[]> }).__rows.clear();
|
||||
|
||||
const { WebDatabaseService } = await import('@/services/database/webDatabaseService');
|
||||
db = await WebDatabaseService.open('test.db');
|
||||
});
|
||||
|
||||
it('execute() maps changes/lastInsertRowid to DatabaseExecResult', async () => {
|
||||
const result: DatabaseExecResult = await db.execute('INSERT INTO items (value) VALUES (?)', [
|
||||
'hello',
|
||||
]);
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('select() returns row objects from prepare().all()', async () => {
|
||||
await db.execute('INSERT INTO items (value) VALUES (?)', ['alpha']);
|
||||
await db.execute('INSERT INTO items (value) VALUES (?)', ['beta']);
|
||||
|
||||
const rows = await db.select<{ id: number; value: string }>('SELECT * FROM items');
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.id).toBe(1);
|
||||
expect(rows[0]!.value).toBe('alpha');
|
||||
});
|
||||
|
||||
it('select() returns empty array when no rows', async () => {
|
||||
const rows = await db.select('SELECT * FROM empty_table');
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
it('batch() wraps in BEGIN/COMMIT transaction', async () => {
|
||||
await db.batch(['CREATE TABLE t (id INTEGER)', 'INSERT INTO t VALUES (1)']);
|
||||
const mod = await import('@tursodatabase/database-wasm');
|
||||
const mockDb = (mod as unknown as { __mockDb: { exec: ReturnType<typeof vi.fn> } }).__mockDb;
|
||||
expect(mockDb.exec).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockDb.exec).toHaveBeenCalledWith('CREATE TABLE t (id INTEGER)');
|
||||
expect(mockDb.exec).toHaveBeenCalledWith('INSERT INTO t VALUES (1)');
|
||||
expect(mockDb.exec).toHaveBeenCalledWith('COMMIT');
|
||||
});
|
||||
|
||||
it('batch() rolls back on error', async () => {
|
||||
const mod = await import('@tursodatabase/database-wasm');
|
||||
const mockDb = (mod as unknown as { __mockDb: { exec: ReturnType<typeof vi.fn> } }).__mockDb;
|
||||
let callCount = 0;
|
||||
mockDb.exec.mockImplementation((_sql: string) => {
|
||||
callCount++;
|
||||
// Fail on the second exec (first real statement after BEGIN)
|
||||
if (callCount === 2) throw new Error('SQL error');
|
||||
});
|
||||
|
||||
await expect(db.batch(['BAD SQL', 'GOOD SQL'])).rejects.toThrow('SQL error');
|
||||
expect(mockDb.exec).toHaveBeenCalledWith('ROLLBACK');
|
||||
});
|
||||
|
||||
it('close() delegates to underlying db.close()', async () => {
|
||||
await db.close();
|
||||
const mod = await import('@tursodatabase/database-wasm');
|
||||
const mockDb = (mod as unknown as { __mockDb: { close: ReturnType<typeof vi.fn> } }).__mockDb;
|
||||
expect(mockDb.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests: DatabaseExecResult shape
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('DatabaseExecResult type contract', () => {
|
||||
it('has rowsAffected and lastInsertId properties', () => {
|
||||
const result: DatabaseExecResult = {
|
||||
rowsAffected: 5,
|
||||
lastInsertId: 42,
|
||||
};
|
||||
expect(result.rowsAffected).toBe(5);
|
||||
expect(result.lastInsertId).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests: Unified API consistency
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('API consistency between Native and Web implementations', () => {
|
||||
it('both implementations satisfy the DatabaseService interface', async () => {
|
||||
const { NativeDatabaseService } = await import('@/services/database/nativeDatabaseService');
|
||||
const nativeDb: DatabaseService = await NativeDatabaseService.open('sqlite:test.db');
|
||||
expect(nativeDb.execute).toBeDefined();
|
||||
expect(nativeDb.select).toBeDefined();
|
||||
expect(nativeDb.batch).toBeDefined();
|
||||
expect(nativeDb.close).toBeDefined();
|
||||
|
||||
const { WebDatabaseService } = await import('@/services/database/webDatabaseService');
|
||||
const webDb: DatabaseService = await WebDatabaseService.open('test.db');
|
||||
expect(webDb.execute).toBeDefined();
|
||||
expect(webDb.select).toBeDefined();
|
||||
expect(webDb.batch).toBeDefined();
|
||||
expect(webDb.close).toBeDefined();
|
||||
});
|
||||
|
||||
it('both return same shape from execute()', async () => {
|
||||
const { NativeDatabaseService } = await import('@/services/database/nativeDatabaseService');
|
||||
const nativeDb = await NativeDatabaseService.open('sqlite:test.db');
|
||||
const nativeResult = await nativeDb.execute('INSERT INTO t (value) VALUES (?)', ['x']);
|
||||
|
||||
const { WebDatabaseService } = await import('@/services/database/webDatabaseService');
|
||||
const webDb = await WebDatabaseService.open('test.db');
|
||||
const webResult = await webDb.execute('INSERT INTO t (value) VALUES (?)', ['x']);
|
||||
|
||||
expect(Object.keys(nativeResult).sort()).toEqual(['lastInsertId', 'rowsAffected']);
|
||||
expect(Object.keys(webResult).sort()).toEqual(['lastInsertId', 'rowsAffected']);
|
||||
expect(typeof nativeResult.rowsAffected).toBe('number');
|
||||
expect(typeof webResult.rowsAffected).toBe('number');
|
||||
expect(typeof nativeResult.lastInsertId).toBe('number');
|
||||
expect(typeof webResult.lastInsertId).toBe('number');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { it, expect } from 'vitest';
|
||||
import { DatabaseService } from '@/types/database';
|
||||
|
||||
/**
|
||||
* Shared base database operation tests exercised against any DatabaseService.
|
||||
* Covers CRUD, batch, data types, error handling, and result contract.
|
||||
*
|
||||
* Call inside a describe() block:
|
||||
* describe('base ops', () => { baseTests(() => db); });
|
||||
*/
|
||||
export function baseTests(getDb: () => DatabaseService) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema & basic operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('creates a table and inserts a row', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const result = await db.execute('INSERT INTO items (name) VALUES (?)', ['apple']);
|
||||
expect(result.rowsAffected).toBe(1);
|
||||
expect(result.lastInsertId).toBe(1);
|
||||
});
|
||||
|
||||
it('inserts multiple rows with auto-incrementing ids', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const r1 = await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
const r2 = await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
const r3 = await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
expect(r1.lastInsertId).toBe(1);
|
||||
expect(r2.lastInsertId).toBe(2);
|
||||
expect(r3.lastInsertId).toBe(3);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SELECT queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('select returns typed rows', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)');
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['apple', 10]);
|
||||
await db.execute('INSERT INTO items (name, qty) VALUES (?, ?)', ['banana', 20]);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string; qty: number }>(
|
||||
'SELECT * FROM items ORDER BY id',
|
||||
);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.name).toBe('apple');
|
||||
expect(rows[0]!.qty).toBe(10);
|
||||
expect(rows[1]!.name).toBe('banana');
|
||||
expect(rows[1]!.qty).toBe(20);
|
||||
});
|
||||
|
||||
it('select with WHERE and params', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['apple']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['banana']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['cherry']);
|
||||
|
||||
const rows = await db.select<{ id: number; name: string }>(
|
||||
'SELECT * FROM items WHERE name = ?',
|
||||
['banana'],
|
||||
);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.name).toBe('banana');
|
||||
});
|
||||
|
||||
it('select returns empty array for no matching rows', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
const rows = await db.select('SELECT * FROM items');
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPDATE & DELETE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('UPDATE returns rowsAffected', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['old']);
|
||||
|
||||
const result = await db.execute('UPDATE items SET name = ? WHERE name = ?', ['new', 'old']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
});
|
||||
|
||||
it('DELETE returns rowsAffected', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['a']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['b']);
|
||||
await db.execute('INSERT INTO items (name) VALUES (?)', ['c']);
|
||||
|
||||
const result = await db.execute('DELETE FROM items WHERE name IN (?, ?)', ['a', 'c']);
|
||||
expect(result.rowsAffected).toBe(2);
|
||||
|
||||
const remaining = await db.select<{ name: string }>('SELECT name FROM items');
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]!.name).toBe('b');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Batch execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('batch executes multiple statements atomically', async () => {
|
||||
const db = getDb();
|
||||
await db.batch([
|
||||
'CREATE TABLE t1 (id INTEGER PRIMARY KEY, val TEXT)',
|
||||
"INSERT INTO t1 (val) VALUES ('one')",
|
||||
"INSERT INTO t1 (val) VALUES ('two')",
|
||||
"INSERT INTO t1 (val) VALUES ('three')",
|
||||
]);
|
||||
|
||||
const rows = await db.select<{ val: string }>('SELECT val FROM t1 ORDER BY id');
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows.map((r) => r.val)).toEqual(['one', 'two', 'three']);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('handles NULL values correctly', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', [null]);
|
||||
|
||||
const rows = await db.select<{ id: number; val: string | null }>('SELECT * FROM t');
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.val).toBeNull();
|
||||
});
|
||||
|
||||
it('handles integer and real types', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE nums (i INTEGER, r REAL)');
|
||||
await db.execute('INSERT INTO nums (i, r) VALUES (?, ?)', [42, 3.14]);
|
||||
|
||||
const rows = await db.select<{ i: number; r: number }>('SELECT * FROM nums');
|
||||
expect(rows[0]!.i).toBe(42);
|
||||
expect(rows[0]!.r).toBeCloseTo(3.14);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('throws on invalid SQL', async () => {
|
||||
const db = getDb();
|
||||
await expect(db.execute('INVALID SQL STATEMENT')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws on constraint violation', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT UNIQUE)');
|
||||
await db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val']);
|
||||
await expect(db.execute('INSERT INTO t (val) VALUES (?)', ['unique_val'])).rejects.toThrow();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('execute result always has rowsAffected and lastInsertId as numbers', async () => {
|
||||
const db = getDb();
|
||||
await db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY)');
|
||||
const result = await db.execute('INSERT INTO t DEFAULT VALUES');
|
||||
expect(typeof result.rowsAffected).toBe('number');
|
||||
expect(typeof result.lastInsertId).toBe('number');
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user