forked from akai/readest
Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66d2fdf999 | |||
| d8e0ceeff1 | |||
| f3ad97b989 | |||
| 1bb49ab023 | |||
| f9a0b39586 | |||
| c533da498d | |||
| b50bc0b854 | |||
| 96c465931c | |||
| 2bd54ac236 | |||
| 6ad549d13c | |||
| 67249370c9 | |||
| fe3ab011ca | |||
| 6cb4278b98 | |||
| 51468862a2 | |||
| b3a44d066f | |||
| 68d4538d40 | |||
| 80105af839 | |||
| 3904c1e8e7 | |||
| 99be6c58e2 | |||
| 664cc772d0 | |||
| 21208adbcf | |||
| 70eb59e2d6 | |||
| 3e7b57282e | |||
| 534582b125 | |||
| 4465e6986e | |||
| a535f6419e | |||
| 65da9c1d47 | |||
| 5f71fd9e47 | |||
| dcf75e07d1 | |||
| 79ba9b3818 | |||
| 128b238bcb | |||
| e26f7e6a2c | |||
| 4c5ff59bcf | |||
| 99b2a34bd2 | |||
| 40f3268ef3 | |||
| 4dac0850c5 | |||
| 118538ba35 | |||
| 4a92cacd84 | |||
| ce53cd2b47 | |||
| b99c1bc19a | |||
| eec2c39f19 | |||
| c6ae85484e | |||
| 7a9f46e93c | |||
| b9f6578127 | |||
| e75a3d254e | |||
| ea15906acf | |||
| 15d2784725 | |||
| ae3bb9da9a | |||
| 239b32fcc2 | |||
| f64739419b | |||
| af8f036ca3 | |||
| 24a87508c6 | |||
| 548d50a882 | |||
| 1e81ab5205 | |||
| a47a480aa2 | |||
| 411f9e236d | |||
| 950bbd0821 | |||
| 2824e50b51 | |||
| 129720c916 | |||
| fd3533dba1 | |||
| 920032155b | |||
| 226bf7033e | |||
| 9444be7fcc | |||
| c9a69a922b | |||
| 9a11b05833 | |||
| 6e603ee38f | |||
| 03fd6e2e6f | |||
| 7dd11c0fb0 | |||
| 968597e52c | |||
| a534050b19 | |||
| 0be828fd66 | |||
| c6d4e2bdd6 | |||
| bf72ab86cd | |||
| fb49ddf484 | |||
| d8817a88b9 | |||
| 475becafe3 | |||
| df165576e6 | |||
| 051f2e5b13 | |||
| 83f0d135c5 | |||
| 1ed84a3256 | |||
| 9d6394fe2b | |||
| fe9603ffb8 | |||
| 681e065ac4 | |||
| f64fc5723e | |||
| a9a1dc8e70 | |||
| b1a1e35790 | |||
| 3c538c3746 | |||
| 9834bd57cf | |||
| b89171a4d8 | |||
| 92116e7455 | |||
| 8dfab2c963 | |||
| 9f261f12e9 | |||
| e74615ac1d | |||
| 3b350d6945 | |||
| 9e0e3fde7d | |||
| 52c49ddef1 | |||
| e3d52891fb | |||
| 9cd88fe839 | |||
| fca3917a12 | |||
| c90de6967b | |||
| bbbd378f9b | |||
| 9f7147f8f8 | |||
| 8a468a6d1f | |||
| c848a319ff | |||
| 5cc80db438 | |||
| 94930baa1b | |||
| 1294ef90c1 | |||
| 570598520f | |||
| 7b4fc91994 | |||
| fb387c2384 | |||
| 4ce1ebe477 | |||
| c1460f4b85 | |||
| bc94f3f790 | |||
| c8c761b017 | |||
| bdb25999c9 | |||
| c58e172a54 | |||
| 160efcd37b | |||
| d7470f4139 | |||
| 01b4e3530d | |||
| 09c62d442f | |||
| d62ad60ce8 | |||
| 8cd34c8aaa | |||
| a52d9e3a2b | |||
| 64a1ea531a | |||
| 19f9f5ea24 | |||
| ffc51e75de | |||
| 2100071991 | |||
| 563d3478ba | |||
| 2c54e9ae2f | |||
| c2eb2e2fcc | |||
| e592f40429 | |||
| 4bd7cfe57c | |||
| c31ece6742 | |||
| aba9e87fc1 | |||
| 2c4df601d8 | |||
| 8bfc90a5b0 | |||
| 8bea05478a | |||
| 42fee90a27 | |||
| 45e57c3943 | |||
| 51b0b8483f | |||
| cbf7a501b7 | |||
| edfcb75ba5 | |||
| ce76843ccc | |||
| 5bbc5ceccc | |||
| 224acd68b1 | |||
| 6c0f1b8b5f | |||
| 481d8198e9 | |||
| b1153ba051 | |||
| aad532bfdd | |||
| a71347c897 | |||
| 034f41ca10 | |||
| 539ad8dea2 | |||
| 48920a87bf | |||
| d1d0d2d59c | |||
| ea811c90c6 | |||
| 5cd5b65e83 | |||
| ea24b5a97a | |||
| 06d620db83 |
@@ -0,0 +1,31 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Rust build artifacts
|
||||
target
|
||||
**/target
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Build outputs
|
||||
.next
|
||||
**/.next
|
||||
out
|
||||
**/out
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
@@ -8,6 +8,6 @@ updates:
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*" # Group all Actions updates into a single larger pull request
|
||||
- '*' # Group all Actions updates into a single larger pull request
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
@@ -45,8 +45,6 @@ jobs:
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.14.0
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6
|
||||
@@ -68,6 +66,10 @@ jobs:
|
||||
run: |
|
||||
pnpm install && pnpm setup-vendors
|
||||
|
||||
- name: run format check
|
||||
run: |
|
||||
pnpm format:check || (pnpm format && git diff && exit 1)
|
||||
|
||||
- name: run tests
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
|
||||
@@ -155,8 +155,6 @@ jobs:
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.14.0
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6
|
||||
@@ -321,6 +319,8 @@ jobs:
|
||||
if: matrix.config.os == 'windows-latest'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
shell: bash
|
||||
@@ -354,7 +354,9 @@ jobs:
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file --clobber
|
||||
|
||||
echo "Signing portable binary"
|
||||
pnpm tauri signer sign $bin_file
|
||||
pushd apps/readest-app/
|
||||
pnpm tauri signer sign "../../$bin_file"
|
||||
popd
|
||||
echo "Uploading signature to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file.sig --clobber
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
docker/.env
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
@@ -45,3 +46,4 @@ fastlane/report.xml
|
||||
|
||||
# nix
|
||||
result*
|
||||
.playwright-mcp/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pnpm exec lint-staged
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
pnpm -C apps/readest-app lint
|
||||
pnpm -C apps/readest-app test
|
||||
+34
-1
@@ -1 +1,34 @@
|
||||
packages/foliate-js/
|
||||
# Dependencies
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Build Artifacts (Web & Rust)
|
||||
.next
|
||||
.open-next
|
||||
.build
|
||||
.tauri
|
||||
out
|
||||
build
|
||||
dist
|
||||
target
|
||||
fastlane
|
||||
.wrangler
|
||||
|
||||
# Autogenerated Tauri files
|
||||
gen
|
||||
**/autogenerated
|
||||
**/schemas
|
||||
|
||||
# Submodules (External Repos)
|
||||
packages
|
||||
|
||||
# Vendored Assets (Generated/External Code)
|
||||
apps/readest-app/public/*.js
|
||||
apps/readest-app/public/vendor
|
||||
|
||||
# Environment & Editor
|
||||
.env
|
||||
.env.*
|
||||
.vscode
|
||||
.idea
|
||||
*.log
|
||||
Vendored
+16
-1
@@ -13,4 +13,19 @@
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.format.enable": false,
|
||||
"typescript.format.enable": false,
|
||||
}
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"prettier.documentSelectors": ["**/*.{js,jsx,ts,tsx,css,json,md,html,yml}"]
|
||||
}
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@ git submodule update --init --recursive
|
||||
```bash
|
||||
# might need to rerun this when code is updated
|
||||
pnpm install
|
||||
# copy pdfjs-dist to Next.js public directory
|
||||
pnpm --filter @readest/readest-app setup-pdfjs
|
||||
# copy vendors dist libs to public directory
|
||||
pnpm --filter @readest/readest-app setup-vendors
|
||||
```
|
||||
|
||||
#### 3. Verify Dependencies Installation
|
||||
|
||||
+33
-31
@@ -1,38 +1,40 @@
|
||||
FROM node:22-slim
|
||||
|
||||
ENV PNPM_HOME="/root/.local/share/pnpm"
|
||||
ENV PATH="${PATH}:${PNPM_HOME}"
|
||||
|
||||
RUN npm install --global pnpm
|
||||
|
||||
# Install necessary packages
|
||||
RUN apt update -y && apt install -y --no-install-recommends \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
ca-certificates \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Rust and Cargo
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
COPY . /app
|
||||
|
||||
FROM docker.io/node:22-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@10.29.3 --activate
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY apps/readest-app/package.json ./apps/readest-app/
|
||||
COPY patches/ ./patches/
|
||||
COPY packages/ ./packages/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
RUN pnpm --filter @readest/readest-app setup-pdfjs
|
||||
FROM base AS dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm --filter @readest/readest-app setup-vendors
|
||||
|
||||
FROM dependencies AS development-stage
|
||||
COPY . .
|
||||
WORKDIR /app/apps/readest-app
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["pnpm", "dev-web", "-H", "0.0.0.0"]
|
||||
|
||||
FROM base AS build
|
||||
ARG NEXT_PUBLIC_SUPABASE_URL
|
||||
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
ARG NEXT_PUBLIC_APP_PLATFORM
|
||||
ARG NEXT_PUBLIC_API_BASE_URL
|
||||
ARG NEXT_PUBLIC_OBJECT_STORAGE_TYPE
|
||||
ARG NEXT_PUBLIC_STORAGE_FIXED_QUOTA
|
||||
ARG NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA
|
||||
COPY --from=dependencies /app/node_modules /app/node_modules
|
||||
COPY --from=dependencies /app/apps/readest-app/node_modules /app/apps/readest-app/node_modules
|
||||
COPY --from=dependencies /app/apps/readest-app/public/vendor /app/apps/readest-app/public/vendor
|
||||
COPY --from=dependencies /app/packages/foliate-js/node_modules /app/packages/foliate-js/node_modules
|
||||
COPY . .
|
||||
WORKDIR /app/apps/readest-app
|
||||
RUN pnpm build-web
|
||||
|
||||
ENTRYPOINT ["pnpm", "start-web"]
|
||||
FROM build as production-stage
|
||||
ENTRYPOINT ["pnpm", "start-web", "-H", "0.0.0.0"]
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -45,24 +45,24 @@
|
||||
|
||||
<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. | ✅ |
|
||||
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
|
||||
| **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. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **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. | ✅ |
|
||||
| **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. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
| **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. | ✅ |
|
||||
|
||||
## Planned Features
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| [**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. | 🔄 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🔄 |
|
||||
| **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. 😊
|
||||
@@ -176,7 +176,9 @@ For Android:
|
||||
|
||||
```bash
|
||||
# Initialize the Android environment (run once)
|
||||
rm apps/readest-app/src-tauri/gen/android
|
||||
pnpm tauri android init
|
||||
git checkout apps/readest-app/src-tauri/gen/android
|
||||
|
||||
pnpm tauri android dev
|
||||
# or if you want to dev on a real device
|
||||
@@ -202,6 +204,9 @@ pnpm tauri android build
|
||||
pnpm tauri ios build
|
||||
```
|
||||
|
||||
Please refer to our release script if you experience any issues:
|
||||
https://github.com/readest/readest/blob/main/.github/workflows/release.yml
|
||||
|
||||
### 6. Setup dev environment with Nix
|
||||
|
||||
If you have Nix installed, you can leverage flake to enter a development shell
|
||||
@@ -250,6 +255,32 @@ Please check the [wiki][link-gh-wiki] of this project for more information on de
|
||||
|
||||
- See Issue [readest/readest#358](https://github.com/readest/readest/issues/358) for further details, or head over to our [Discord][link-discord] server and open a support discussion with detailed logs of your environment and the steps you’ve taken.
|
||||
|
||||
### 2. AppImage Launches but Only Shows a Taskbar Icon
|
||||
|
||||
On some Arch Linux systems—especially those using Wayland—the Readest AppImage may briefly show an icon in the taskbar and then exit without opening a window.
|
||||
|
||||
You might see logs such as:
|
||||
|
||||
```
|
||||
Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...
|
||||
```
|
||||
|
||||
This behavior is usually caused by compatibility issues between the bundled AppImage libraries and the system’s EGL / Wayland environment.
|
||||
|
||||
**Workaround 1: Launch with LD_PRELOAD (recommended)**
|
||||
|
||||
You can preload the system Wayland client library before launching the AppImage:
|
||||
|
||||
```
|
||||
LD_PRELOAD=/usr/lib/libwayland-client.so /path/to/Readest.AppImage
|
||||
```
|
||||
|
||||
This workaround has been confirmed to resolve the issue on affected systems.
|
||||
|
||||
**Workaround 2: Use the Flatpak Version**
|
||||
|
||||
If you prefer a more reliable out-of-the-box experience on Arch Linux, consider using the [Flatpak build on Flathub][link-flathub] instead. The Flatpak runtime helps avoid system library mismatches and tends to behave more consistently across different Wayland and X11 setups.
|
||||
|
||||
## Contributors
|
||||
|
||||
Readest is open-source, and contributions are welcome! Feel free to open issues, suggest features, or submit pull requests. Please **review our [contributing guidelines](CONTRIBUTING.md) before you start**. We also welcome you to join our [Discord][link-discord] community for either support or contributing guidance.
|
||||
@@ -262,12 +293,12 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
|
||||
|
||||
## Support
|
||||
|
||||
If Readest has been useful to you, consider supporting its development. You can [become a sponsor on GitHub](https://github.com/sponsors/readest) or just [donate with crypto](https://donate.readest.com). Your contribution helps us squash bugs faster, improve performance, and keep building great features.
|
||||
If Readest has been useful to you, consider supporting its development. You can [become a sponsor on GitHub](https://github.com/sponsors/readest), [donate via Stripe](https://donate.stripe.com/4gMcN5aZdcE52kW3TFgjC01), or [donate with crypto](https://donate.readest.com). Your contribution helps us squash bugs faster, improve performance, and keep building great features.
|
||||
|
||||
### Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a title="Browser testing via TestMu AI" href="https://www.testmu.ai?utm_source=readest&utm_medium=sponsor" target="_blank">
|
||||
<a title="Browser testing via TestMu AI" href="https://www.testmuai.com/?utm_medium=sponsor&utm_source=readest" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/readest/readest/refs/heads/main/data/sponsors/testmu-ai-logo.png" style="vertical-align: middle;" width="250" />
|
||||
</a>
|
||||
</p>
|
||||
@@ -291,7 +322,9 @@ The following libraries and frameworks are used in this software:
|
||||
|
||||
The following fonts are utilized in this software, either bundled within the application or provided through web fonts:
|
||||
|
||||
[Bitter](https://fonts.google.com/?query=Bitter), [Fira Code](https://fonts.google.com/?query=Fira+Code), [Literata](https://fonts.google.com/?query=Literata), [Merriweather](https://fonts.google.com/?query=Merriweather), [Noto Sans](https://fonts.google.com/?query=Noto+Sans), [Roboto](https://fonts.google.com/?query=Roboto), [LXGW WenKai](https://github.com/lxgw/LxgwWenKai), [MiSans](https://hyperos.mi.com/font/en/), [Source Han](https://github.com/adobe-fonts/source-han-sans/), [WenQuanYi Micro Hei](http://wenq.org/wqy2/)
|
||||
[Bitter](https://fonts.google.com/specimen/Bitter), [Fira Code](https://fonts.google.com/specimen/Fira+Code), [Inter](https://fonts.google.com/specimen/Inter), [Literata](https://fonts.google.com/specimen/Literata), [Merriweather](https://fonts.google.com/specimen/Merriweather), [Noto Sans](https://fonts.google.com/specimen/Noto+Sans), [Roboto](https://fonts.google.com/specimen/Roboto), [LXGW WenKai](https://github.com/lxgw/LxgwWenKai), [MiSans](https://hyperos.mi.com/font/en/), [Source Han](https://github.com/adobe-fonts/source-han-sans/), [WenQuanYi Micro Hei](http://wenq.org/wqy2/)
|
||||
|
||||
We would also like to thank the [Web Chinese Fonts Plan](https://chinese-font.netlify.app) for offering open-source tools that enable the use of Chinese fonts on the web.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_APP_PLATFORM=tauri
|
||||
AI_GATEWAY_API_KEY=your_key_here
|
||||
@@ -0,0 +1,3 @@
|
||||
NEXT_PUBLIC_APP_PLATFORM=web
|
||||
AI_GATEWAY_API_KEY=your_key_here
|
||||
NEXT_PUBLIC_AI_GATEWAY_API_KEY=your_key_here
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/libs",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@@ -12,28 +12,31 @@ This crate provides Windows Explorer thumbnail support for eBook files when Read
|
||||
|
||||
## Supported Formats
|
||||
|
||||
| Format | Extension | Cover Source |
|
||||
|--------|-----------|--------------|
|
||||
| EPUB | `.epub` | OPF manifest cover reference |
|
||||
| MOBI/AZW | `.mobi`, `.azw`, `.prc` | EXTH cover offset |
|
||||
| AZW3/KF8 | `.azw3`, `.kf8` | KF8 format cover |
|
||||
| FB2 | `.fb2` | `<binary>` coverpage element |
|
||||
| Comic Book | `.cbz`, `.cbr` | First image in archive |
|
||||
| Plain Text | `.txt` | Generated placeholder |
|
||||
| Format | Extension | Cover Source |
|
||||
| ---------- | ----------------------- | ---------------------------- |
|
||||
| EPUB | `.epub` | OPF manifest cover reference |
|
||||
| MOBI/AZW | `.mobi`, `.azw`, `.prc` | EXTH cover offset |
|
||||
| AZW3/KF8 | `.azw3`, `.kf8` | KF8 format cover |
|
||||
| FB2 | `.fb2` | `<binary>` coverpage element |
|
||||
| Comic Book | `.cbz`, `.cbr` | First image in archive |
|
||||
| Plain Text | `.txt` | Generated placeholder |
|
||||
|
||||
## Building
|
||||
|
||||
### Library Only
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### COM DLL (for Windows Explorer integration)
|
||||
|
||||
```bash
|
||||
cargo build --release --features com
|
||||
```
|
||||
|
||||
### CLI Tool
|
||||
|
||||
```bash
|
||||
cargo build --release --features cli
|
||||
```
|
||||
|
||||
@@ -28,6 +28,7 @@ module.exports = {
|
||||
'id',
|
||||
'vi',
|
||||
'ms',
|
||||
'he',
|
||||
'ar',
|
||||
'th',
|
||||
'bo',
|
||||
|
||||
@@ -27,18 +27,38 @@ const nextConfig = {
|
||||
assetPrefix: '',
|
||||
reactStrictMode: true,
|
||||
serverExternalPackages: ['isows'],
|
||||
turbopack: {},
|
||||
transpilePackages: !isDev
|
||||
? [
|
||||
'i18next-browser-languagedetector',
|
||||
'react-i18next',
|
||||
'i18next',
|
||||
'@tauri-apps',
|
||||
'highlight.js',
|
||||
'foliate-js',
|
||||
'marked',
|
||||
]
|
||||
: [],
|
||||
webpack: (config) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
nunjucks: 'nunjucks/browser/nunjucks.js',
|
||||
};
|
||||
return config;
|
||||
},
|
||||
turbopack: {
|
||||
resolveAlias: {
|
||||
nunjucks: 'nunjucks/browser/nunjucks.js',
|
||||
},
|
||||
},
|
||||
transpilePackages: [
|
||||
'ai',
|
||||
'ai-sdk-ollama',
|
||||
'@ai-sdk/react',
|
||||
'@assistant-ui/react',
|
||||
'@assistant-ui/react-ai-sdk',
|
||||
'@assistant-ui/react-markdown',
|
||||
'streamdown',
|
||||
...(isDev
|
||||
? []
|
||||
: [
|
||||
'i18next-browser-languagedetector',
|
||||
'react-i18next',
|
||||
'i18next',
|
||||
'@tauri-apps',
|
||||
'highlight.js',
|
||||
'foliate-js',
|
||||
'marked',
|
||||
]),
|
||||
],
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -55,7 +75,9 @@ const nextConfig = {
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'public, max-age=31536000, immutable',
|
||||
value: isDev
|
||||
? 'public, max-age=0, must-revalidate'
|
||||
: 'public, max-age=31536000, immutable',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.98",
|
||||
"version": "0.9.101",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -15,6 +15,8 @@
|
||||
"test": "dotenv -e .env -e .env.test.local vitest",
|
||||
"tauri": "tauri",
|
||||
"clippy": "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",
|
||||
"copy-pdfjs-js": "cpx \"../../packages/foliate-js/node_modules/pdfjs-dist/legacy/build/{pdf.worker.min.mjs,pdf.min.mjs,pdf.d.mts}\" ./public/vendor/pdfjs",
|
||||
"copy-pdfjs-wasm": "cpx \"../../packages/foliate-js/node_modules/pdfjs-dist/wasm/{openjpeg.wasm,qcms_bg.wasm}\" ./public/vendor/pdfjs",
|
||||
@@ -53,11 +55,25 @@
|
||||
"build-check": "pnpm build && pnpm build-web && pnpm check:all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "^3.0.49",
|
||||
"@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",
|
||||
"@choochmeque/tauri-plugin-sharekit-api": "^0.3.0",
|
||||
"@fabianlars/tauri-plugin-oauth": "2",
|
||||
"@opennextjs/cloudflare": "^1.14.7",
|
||||
"@opennextjs/cloudflare": "^1.15.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@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",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
@@ -78,16 +94,22 @@
|
||||
"@tauri-apps/plugin-shell": "~2.3.4",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-websocket": "~2.4.2",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"@zip.js/zip.js": "^2.8.16",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"ai": "^6.0.47",
|
||||
"ai-sdk-ollama": "^3.2.0",
|
||||
"app-store-server-api": "^0.17.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"buffer": "^6.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.0",
|
||||
"foliate-js": "workspace:*",
|
||||
"franc-min": "^6.2.0",
|
||||
"fzf": "^0.5.2",
|
||||
"google-auth-library": "^10.5.0",
|
||||
"googleapis": "^164.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
@@ -99,8 +121,13 @@
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"js-md5": "^0.8.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^15.0.12",
|
||||
"next": "16.0.10",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "16.1.6",
|
||||
"next-view-transitions": "^0.3.5",
|
||||
"nunjucks": "^3.2.4",
|
||||
"overlayscrollbars": "^2.11.4",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"posthog-js": "^1.246.0",
|
||||
@@ -112,14 +139,17 @@
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-virtuoso": "^4.17.0",
|
||||
"react-window": "^1.8.11",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"semver": "^7.7.1",
|
||||
"streamdown": "^1.6.10",
|
||||
"stripe": "^18.2.1",
|
||||
"styled-jsx": "^5.1.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^4.0.8",
|
||||
"zustand": "5.0.6"
|
||||
"zustand": "5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^15.4.2",
|
||||
@@ -129,10 +159,12 @@
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/lunr": "^2.3.7",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/nunjucks": "^3.2.6",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
@@ -162,6 +194,6 @@
|
||||
"typescript": "^5.7.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.15",
|
||||
"wrangler": "^4.56.0"
|
||||
"wrangler": "^4.60.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -346,12 +346,12 @@
|
||||
"Fit": "ملاءمة",
|
||||
"Reset {{settings}}": "إعادة تعيين {{settings}}",
|
||||
"Reset Settings": "إعادة تعيين الإعدادات",
|
||||
"{{count}} pages left in chapter_zero": "لم يتبق أي صفحات في هذا الفصل",
|
||||
"{{count}} pages left in chapter_one": "تبقّت صفحة واحدة في هذا الفصل",
|
||||
"{{count}} pages left in chapter_two": "تبقّت صفحتان في هذا الفصل",
|
||||
"{{count}} pages left in chapter_few": "تبقّت {{count}} صفحات في هذا الفصل",
|
||||
"{{count}} pages left in chapter_many": "تبقّت {{count}} صفحة في هذا الفصل",
|
||||
"{{count}} pages left in chapter_other": "تبقّى {{count}} صفحة في هذا الفصل",
|
||||
"{{count}} pages left in chapter_zero": "<1>لم يتبق أي صفحات في هذا الفصل</1>",
|
||||
"{{count}} pages left in chapter_one": "<1>تبقّت صفحة واحدة في هذا الفصل</1>",
|
||||
"{{count}} pages left in chapter_two": "<1>تبقّت صفحتان في هذا الفصل</1>",
|
||||
"{{count}} pages left in chapter_few": "<1>تبقّت </1><0>{{count}}</0><1> صفحات في هذا الفصل</1>",
|
||||
"{{count}} pages left in chapter_many": "<1>تبقّت </1><0>{{count}}</0><1> صفحة في هذا الفصل</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>تبقّى </1><0>{{count}}</0><1> صفحة في هذا الفصل</1>",
|
||||
"Show Remaining Pages": "عرض الصفحات المتبقية",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -615,11 +615,10 @@
|
||||
"Size": "حجم",
|
||||
"Cover": "غطاء",
|
||||
"Contain": "احتواء",
|
||||
"{{number}} pages left in chapter": "تبقّت {{number}} صفحات في هذا الفصل",
|
||||
"{{number}} pages left in chapter": "<1>تبقّت </1><0>{{number}}</0><1> صفحات في هذا الفصل</1>",
|
||||
"Device": "الجهاز",
|
||||
"E-Ink Mode": "وضع الحبر الإلكتروني",
|
||||
"Highlight Colors": "ألوان التمييز",
|
||||
"Auto Screen Brightness": "سطوع الشاشة التلقائي",
|
||||
"Pagination": "التقسيم إلى صفحات",
|
||||
"Disable Double Tap": "تعطيل النقر المزدوج",
|
||||
"Tap to Paginate": "اضغط للتقسيم إلى صفحات",
|
||||
@@ -872,7 +871,6 @@
|
||||
"Show Results": "عرض النتائج",
|
||||
"Clear search": "مسح البحث",
|
||||
"Clear search history": "مسح سجل البحث",
|
||||
"Quick action disabled": "الإجراء السريع معطل",
|
||||
"Tap to Toggle Footer": "انقر لتبديل التذييل",
|
||||
"Exported successfully": "تم التصدير بنجاح",
|
||||
"Book exported successfully.": "تم تصدير الكتاب بنجاح.",
|
||||
@@ -915,7 +913,6 @@
|
||||
"Array of annotations": "مصفوفة التعليقات",
|
||||
"Highlighted text": "النص المميز",
|
||||
"Annotation note": "ملاحظة التعليق",
|
||||
"Update time": "وقت التحديث",
|
||||
"Date Format Tokens:": "رموز تنسيق التاريخ:",
|
||||
"Year (4 digits)": "السنة (4 أرقام)",
|
||||
"Month (01-12)": "الشهر (01-12)",
|
||||
@@ -930,5 +927,185 @@
|
||||
"Select Voice": "اختر الصوت",
|
||||
"Toggle Sticky Bottom TTS Bar": "تبديل شريط TTS الثابت",
|
||||
"Display what I'm reading on Discord": "عرض ما أقرأه على Discord",
|
||||
"Show on Discord": "عرض على Discord"
|
||||
"Show on Discord": "عرض على Discord",
|
||||
"Instant {{action}}": "{{action}} فوري",
|
||||
"Instant {{action}} Disabled": "تم تعطيل {{action}} فوري",
|
||||
"Annotation": "التعليق",
|
||||
"Reset Template": "إعادة تعيين القالب",
|
||||
"Annotation style": "نمط التعليق",
|
||||
"Annotation color": "لون التعليق",
|
||||
"Annotation time": "وقت التعليق",
|
||||
"AI": "الذكاء الاصطناعي",
|
||||
"Are you sure you want to re-index this book?": "هل أنت متأكد من إعادة فهرسة هذا الكتاب؟",
|
||||
"Enable AI in Settings": "تفعيل الذكاء الاصطناعي في الإعدادات",
|
||||
"Index This Book": "فهرسة هذا الكتاب",
|
||||
"Enable AI search and chat for this book": "تفعيل البحث والدردشة بالذكاء الاصطناعي لهذا الكتاب",
|
||||
"Start Indexing": "بدء الفهرسة",
|
||||
"Indexing book...": "جاري فهرسة الكتاب...",
|
||||
"Preparing...": "جاري التحضير...",
|
||||
"Delete this conversation?": "حذف هذه المحادثة؟",
|
||||
"No conversations yet": "لا توجد محادثات بعد",
|
||||
"Start a new chat to ask questions about this book": "ابدأ محادثة جديدة لطرح أسئلة حول هذا الكتاب",
|
||||
"Rename": "إعادة التسمية",
|
||||
"New Chat": "محادثة جديدة",
|
||||
"Chat": "محادثة",
|
||||
"Please enter a model ID": "الرجاء إدخال معرف النموذج",
|
||||
"Model not available or invalid": "النموذج غير متاح أو غير صالح",
|
||||
"Failed to validate model": "فشل التحقق من النموذج",
|
||||
"Couldn't connect to Ollama. Is it running?": "تعذر الاتصال بـ Ollama. هل يعمل؟",
|
||||
"Invalid API key or connection failed": "مفتاح API غير صالح أو فشل الاتصال",
|
||||
"Connection failed": "فشل الاتصال",
|
||||
"AI Assistant": "مساعد الذكاء الاصطناعي",
|
||||
"Enable AI Assistant": "تفعيل مساعد الذكاء الاصطناعي",
|
||||
"Provider": "المزود",
|
||||
"Ollama (Local)": "Ollama (محلي)",
|
||||
"AI Gateway (Cloud)": "بوابة الذكاء الاصطناعي (سحابي)",
|
||||
"Ollama Configuration": "إعدادات Ollama",
|
||||
"Refresh Models": "تحديث النماذج",
|
||||
"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.": "اختر من مجموعة من نماذج الذكاء الاصطناعي عالية الجودة والاقتصادية. يمكنك أيضًا استخدام نموذجك الخاص بتحديد \"نموذج مخصص\" أدناه.",
|
||||
"API Key": "مفتاح API",
|
||||
"Get Key": "احصل على المفتاح",
|
||||
"Model": "النموذج",
|
||||
"Custom Model...": "نموذج مخصص...",
|
||||
"Custom Model ID": "معرف النموذج المخصص",
|
||||
"Validate": "تحقق",
|
||||
"Model available": "النموذج متاح",
|
||||
"Connection": "الاتصال",
|
||||
"Test Connection": "اختبار الاتصال",
|
||||
"Connected": "متصل",
|
||||
"Custom Colors": "ألوان مخصصة",
|
||||
"Color E-Ink Mode": "وضع الحبر الإلكتروني الملون",
|
||||
"Reading Ruler": "مسطرة القراءة",
|
||||
"Enable Reading Ruler": "تفعيل مسطرة القراءة",
|
||||
"Lines to Highlight": "الأسطر المراد تمييزها",
|
||||
"Ruler Color": "لون المسطرة",
|
||||
"Command Palette": "لوحة الأوامر",
|
||||
"Search settings and actions...": "البحث في الإعدادات والإجراءات...",
|
||||
"No results found for": "لم يتم العثور على نتائج لـ",
|
||||
"Type to search settings and actions": "اكتب للبحث في الإعدادات والإجراءات",
|
||||
"Recent": "الأخير",
|
||||
"navigate": "التنقل",
|
||||
"select": "اختيار",
|
||||
"close": "إغلاق",
|
||||
"Search Settings": "البحث في الإعدادات",
|
||||
"Page Margins": "هوامش الصفحة",
|
||||
"AI Provider": "مزود الذكاء الاصطناعي",
|
||||
"Ollama URL": "رابط Ollama",
|
||||
"Ollama Model": "نموذج Ollama",
|
||||
"AI Gateway Model": "نموذج بوابة الذكاء الاصطناعي",
|
||||
"Actions": "الإجراءات",
|
||||
"Navigation": "التنقل",
|
||||
"Set status for {{count}} book(s)_one": "تعيين الحالة لـ {{count}} كتاب",
|
||||
"Set status for {{count}} book(s)_other": "تعيين الحالة لـ {{count}} كتب",
|
||||
"Mark as Unread": "تحديد كغير مقروء",
|
||||
"Mark as Finished": "تحديد كمنتهى",
|
||||
"Finished": "منتهى",
|
||||
"Unread": "غير مقروء",
|
||||
"Clear Status": "مسح الحالة",
|
||||
"Status": "الحالة",
|
||||
"Set status for {{count}} book(s)_zero": "تعيين الحالة للكتب",
|
||||
"Set status for {{count}} book(s)_two": "تعيين الحالة للكتابين",
|
||||
"Set status for {{count}} book(s)_few": "تعيين الحالة لـ {{count}} كتب",
|
||||
"Set status for {{count}} book(s)_many": "تعيين الحالة لـ {{count}} كتاباً",
|
||||
"Loading": "جارٍ التحميل...",
|
||||
"Exit Paragraph Mode": "الخروج من وضع الفقرة",
|
||||
"Paragraph Mode": "وضع الفقرة",
|
||||
"Embedding Model": "نموذج التضمين",
|
||||
"{{count}} book(s) synced_zero": "لم تتم مزامنة أي كتب",
|
||||
"{{count}} book(s) synced_one": "تمت مزامنة كتاب واحد",
|
||||
"{{count}} book(s) synced_two": "تمت مزامنة كتابين",
|
||||
"{{count}} book(s) synced_few": "تمت مزامنة {{count}} كتب",
|
||||
"{{count}} book(s) synced_many": "تمت مزامنة {{count}} كتاباً",
|
||||
"{{count}} book(s) synced_other": "تمت مزامنة {{count}} كتاب",
|
||||
"Unable to start RSVP": "تعذر بدء RSVP",
|
||||
"RSVP not supported for PDF": "RSVP غير مدعوم لملفات PDF",
|
||||
"Select Chapter": "اختر الفصل",
|
||||
"Context": "السياق",
|
||||
"Ready": "جاهز",
|
||||
"Chapter Progress": "تقدم الفصل",
|
||||
"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",
|
||||
"Choose where to start reading": "اختر من أين تبدأ القراءة",
|
||||
"From Chapter Start": "من بداية الفصل",
|
||||
"Start reading from the beginning of the chapter": "بدء القراءة من بداية الفصل",
|
||||
"Resume": "استئناف",
|
||||
"Continue from where you left off": "المتابعة من حيث توقف",
|
||||
"From Current Page": "من الصفحة الحالية",
|
||||
"Start from where you are currently reading": "البدء من حيث تقرأ حالياً",
|
||||
"From Selection": "من التحديد",
|
||||
"Speed Reading Mode": "وضع القراءة السريعة",
|
||||
"Scroll left": "تمرير لليسار",
|
||||
"Scroll right": "تمرير لليمين",
|
||||
"Library Sync Progress": "تقدم تزامن المكتبة",
|
||||
"Back to library": "العودة إلى المكتبة",
|
||||
"Group by...": "تجميع حسب...",
|
||||
"Export as Plain Text": "تصدير كنص بسيط",
|
||||
"Export as Markdown": "تصدير بصيغة Markdown",
|
||||
"Show Page Navigation Buttons": "أزرار التنقل",
|
||||
"Page {{number}}": "صفحة {{number}}",
|
||||
"highlight": "تمييز",
|
||||
"underline": "تسطير",
|
||||
"squiggly": "متعرج",
|
||||
"red": "أحمر",
|
||||
"violet": "بنفسجي",
|
||||
"blue": "أزرق",
|
||||
"green": "أخضر",
|
||||
"yellow": "أصفر",
|
||||
"Select {{style}} style": "اختر نمط {{style}}",
|
||||
"Select {{color}} color": "اختر لون {{color}}",
|
||||
"Close Book": "إغلاق الكتاب",
|
||||
"Speed Reading": "القراءة السريعة",
|
||||
"Close Speed Reading": "إغلاق القراءة السريعة",
|
||||
"Authors": "المؤلفون",
|
||||
"Books": "الكتب",
|
||||
"Groups": "المجموعات",
|
||||
"Back to TTS Location": "العودة إلى موقع القراءة الآلية",
|
||||
"Metadata": "بيانات وصفية",
|
||||
"Image viewer": "عارض الصور",
|
||||
"Previous Image": "الصورة السابقة",
|
||||
"Next Image": "الصورة التالية",
|
||||
"Zoomed": "مكبّر",
|
||||
"Zoom level": "مستوى التكبير",
|
||||
"Table viewer": "عارض الجداول",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "تعذر الاتصال بـ Readwise. يرجى التحقق من اتصال الشبكة لديك.",
|
||||
"Invalid Readwise access token": "رمز وصول Readwise غير صالح",
|
||||
"Disconnected from Readwise": "تم قطع الاتصال بـ Readwise",
|
||||
"Never": "أبداً",
|
||||
"Readwise Settings": "إعدادات Readwise",
|
||||
"Connected to Readwise": "متصل بـ Readwise",
|
||||
"Last synced: {{time}}": "آخر مزامنة: {{time}}",
|
||||
"Sync Enabled": "تم تمكين المزامنة",
|
||||
"Disconnect": "قطع الاتصال",
|
||||
"Connect your Readwise account to sync highlights.": "قم بتوصيل حساب Readwise الخاص بك لمزامنة التمييزات.",
|
||||
"Get your access token at": "احصل على رمز الوصول الخاص بك من",
|
||||
"Access Token": "رمز الوصول",
|
||||
"Paste your Readwise access token": "الصق رمز وصول Readwise الخاص بك",
|
||||
"Config": "تكوين",
|
||||
"Readwise Sync": "مزامنة Readwise",
|
||||
"Push Highlights": "إرسال التمييزات",
|
||||
"Highlights synced to Readwise": "تمت مزامنة التمييزات مع Readwise",
|
||||
"Readwise sync failed: no internet connection": "فشلت مزامنة Readwise: لا يوجد اتصال بالإنترنت",
|
||||
"Readwise sync failed: {{error}}": "فشلت مزامنة Readwise: {{error}}",
|
||||
"System Screen Brightness": "سطوع شاشة النظام",
|
||||
"Page:": "صفحة:",
|
||||
"Page: {{number}}": "صفحة: {{number}}",
|
||||
"Annotation page number": "رقم صفحة التعליق"
|
||||
}
|
||||
|
||||
@@ -186,8 +186,8 @@
|
||||
"Add your notes here...": "এখানে আপনার নোট যোগ করুন...",
|
||||
"Search notes and excerpts...": "নোট ও উদ্ধৃতি খুঁজুন...",
|
||||
"{{time}} min left in chapter": "অধ্যায়ে {{time}} মিনিট বাকি",
|
||||
"{{count}} pages left in chapter_one": "অধ্যায়ে ১ পৃষ্ঠা বাকি",
|
||||
"{{count}} pages left in chapter_other": "অধ্যায়ে {{count}} পৃষ্ঠা বাকি",
|
||||
"{{count}} pages left in chapter_one": "<1>অধ্যায়ে ১ পৃষ্ঠা বাকি</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>অধ্যায়ে </1><0>{{count}}</0><1> পৃষ্ঠা বাকি</1>",
|
||||
"Theme Mode": "থিম মোড",
|
||||
"Invert Image In Dark Mode": "ডার্ক মোডে ছবি উল্টান",
|
||||
"Override Book Color": "বইয়ের রঙ পরিবর্তন",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "আকার",
|
||||
"Cover": "ঢাকা",
|
||||
"Contain": "ধারণ",
|
||||
"{{number}} pages left in chapter": "অধ্যায়ে {{number}} পৃষ্ঠা বাকি",
|
||||
"{{number}} pages left in chapter": "<1>অধ্যায়ে </1><0>{{number}}</0><1> পৃষ্ঠা বাকি</1>",
|
||||
"Device": "যন্ত্র",
|
||||
"E-Ink Mode": "ই-ইঙ্ক মোড",
|
||||
"Highlight Colors": "হাইলাইট রঙ",
|
||||
"Auto Screen Brightness": "স্বয়ংক্রিয় স্ক্রিন উজ্জ্বলতা",
|
||||
"Pagination": "পৃষ্ঠা বিন্যাস",
|
||||
"Disable Double Tap": "ডাবল ট্যাপ অক্ষম করুন",
|
||||
"Tap to Paginate": "পৃষ্ঠায় যেতে ট্যাপ করুন",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "ফলাফল দেখুন",
|
||||
"Clear search": "অনুসন্ধান সাফ করুন",
|
||||
"Clear search history": "অনুসন্ধান ইতিহাস সাফ করুন",
|
||||
"Quick action disabled": "দ্রুত কার্যক্রম অক্ষম",
|
||||
"Tap to Toggle Footer": "ফুটার টগল করতে আলতো চাপুন",
|
||||
"Exported successfully": "সফলভাবে রপ্তানি হয়েছে",
|
||||
"Book exported successfully.": "বইটি সফলভাবে রপ্তানি হয়েছে।",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "টীকাগুলির তালিকা",
|
||||
"Highlighted text": "হাইলাইট করা পাঠ্য",
|
||||
"Annotation note": "টীকা নোট",
|
||||
"Update time": "আপডেট সময়",
|
||||
"Date Format Tokens:": "তারিখ বিন্যাস টোকেন:",
|
||||
"Year (4 digits)": "বছর (4 সংখ্যা)",
|
||||
"Month (01-12)": "মাস (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "ভয়েস নির্বাচন করুন",
|
||||
"Toggle Sticky Bottom TTS Bar": "স্থির TTS বার টগল করুন",
|
||||
"Display what I'm reading on Discord": "Discord-এ পড়ছি যা দেখান",
|
||||
"Show on Discord": "Discord-এ দেখান"
|
||||
"Show on Discord": "Discord-এ দেখান",
|
||||
"Instant {{action}}": "তাৎক্ষণিক {{action}}",
|
||||
"Instant {{action}} Disabled": "তাৎক্ষণিক {{action}} নিষ্ক্রিয়",
|
||||
"Annotation": "টীকা",
|
||||
"Reset Template": "টেমপ্লেট রিসেট করুন",
|
||||
"Annotation style": "টীকা শৈলী",
|
||||
"Annotation color": "টীকা রঙ",
|
||||
"Annotation time": "টীকা সময়",
|
||||
"AI": "কৃত্রিম বুদ্ধিমত্তা (AI)",
|
||||
"Are you sure you want to re-index this book?": "আপনি কি নিশ্চিত যে আপনি এই বইটি পুনরায় ইনডেক্স করতে চান?",
|
||||
"Enable AI in Settings": "সেটিংস থেকে AI সক্রিয় করুন",
|
||||
"Index This Book": "এই বইটি ইনডেক্স করুন",
|
||||
"Enable AI search and chat for this book": "এই বইটির জন্য AI অনুসন্ধান এবং চ্যাট সক্রিয় করুন",
|
||||
"Start Indexing": "ইনডেক্সিং শুরু করুন",
|
||||
"Indexing book...": "বই ইনডেক্স করা হচ্ছে...",
|
||||
"Preparing...": "প্রস্তুত করা হচ্ছে...",
|
||||
"Delete this conversation?": "এই কথোপকথনটি মুছবেন?",
|
||||
"No conversations yet": "এখনও কোনও কথোপকথন নেই",
|
||||
"Start a new chat to ask questions about this book": "এই বইটি সম্পর্কে প্রশ্ন জিজ্ঞাসা করতে একটি নতুন চ্যাট শুরু করুন",
|
||||
"Rename": "নাম পরিবর্তন",
|
||||
"New Chat": "নতুন চ্যাট",
|
||||
"Chat": "চ্যাট",
|
||||
"Please enter a model ID": "দয়া করে একটি মডেল আইডি লিখুন",
|
||||
"Model not available or invalid": "মডেল উপলব্ধ নয় বা অবৈধ",
|
||||
"Failed to validate model": "মডেল যাচাই করতে ব্যর্থ হয়েছে",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama এর সাথে সংযোগ করা যায়নি। এটি কি চলছে?",
|
||||
"Invalid API key or connection failed": "অবৈধ API কী বা সংযোগ ব্যর্থ হয়েছে",
|
||||
"Connection failed": "সংযোগ ব্যর্থ হয়েছে",
|
||||
"AI Assistant": "AI সহকারী",
|
||||
"Enable AI Assistant": "AI সহকারী সক্রিয় করুন",
|
||||
"Provider": "প্রদানকারী",
|
||||
"Ollama (Local)": "Ollama (স্থানীয়)",
|
||||
"AI Gateway (Cloud)": "AI গেটওয়ে (ক্লাউড)",
|
||||
"Ollama Configuration": "Ollama কনফিগারেশন",
|
||||
"Refresh Models": "মডেল রিফ্রেশ করুন",
|
||||
"AI Model": "AI মডেল",
|
||||
"No models detected": "কোনও মডেল শনাক্ত করা যায়নি",
|
||||
"AI Gateway Configuration": "AI গেটওয়ে কনফিগারেশন",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "উচ্চ-মানের, সাশ্রয়ী AI মডেলগুলি থেকে নির্বাচন করুন। আপনি নিচে \"কাস্টম মডেল\" নির্বাচন করে আপনার নিজস্ব মডেলও আনতে পারেন।",
|
||||
"API Key": "API কী",
|
||||
"Get Key": "কী পান",
|
||||
"Model": "মডেল",
|
||||
"Custom Model...": "কাস্টম মডেল...",
|
||||
"Custom Model ID": "কাস্টম মডেল আইডি",
|
||||
"Validate": "যাচাই করুন",
|
||||
"Model available": "মডেল উপলব্ধ",
|
||||
"Connection": "সংযোগ",
|
||||
"Test Connection": "সংযোগ পরীক্ষা করুন",
|
||||
"Connected": "সংযুক্ত",
|
||||
"Custom Colors": "কাস্টম রঙ",
|
||||
"Color E-Ink Mode": "কালার ই-ইঙ্ক মোড",
|
||||
"Reading Ruler": "রিডিং রুলার",
|
||||
"Enable Reading Ruler": "রিডিং রুলার সক্ষম করুন",
|
||||
"Lines to Highlight": "হাইলাইট করার জন্য লাইন",
|
||||
"Ruler Color": "রুলারের রঙ",
|
||||
"Command Palette": "কমান্ড প্যালেট",
|
||||
"Search settings and actions...": "সেটিংস এবং অ্যাকশন খুঁজুন...",
|
||||
"No results found for": "এর জন্য কোনো ফলাফল পাওয়া যায়নি",
|
||||
"Type to search settings and actions": "সেটিংস এবং অ্যাকশন খুঁজতে টাইপ করুন",
|
||||
"Recent": "সাম্প্রতিক",
|
||||
"navigate": "নেভিগেট",
|
||||
"select": "নির্বাচন করুন",
|
||||
"close": "বন্ধ করুন",
|
||||
"Search Settings": "সেটিংস খুঁজুন",
|
||||
"Page Margins": "পৃষ্ঠার মার্জিন",
|
||||
"AI Provider": "AI প্রদানকারী",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama মডেল",
|
||||
"AI Gateway Model": "AI গেটওয়ে মডেল",
|
||||
"Actions": "অ্যাকশন",
|
||||
"Navigation": "নেভিগেশন",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} টি বইয়ের স্ট্যাটাস সেট করুন",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} টি বইয়ের স্ট্যাটাস সেট করুন",
|
||||
"Mark as Unread": "পড়া হয়নি হিসেবে চিহ্নিত করুন",
|
||||
"Mark as Finished": "পড়া শেষ হিসেবে চিহ্নিত করুন",
|
||||
"Finished": "শেষ হয়েছে",
|
||||
"Unread": "অপঠিত",
|
||||
"Clear Status": "স্থিতি মুছুন",
|
||||
"Status": "অবস্থা",
|
||||
"Loading": "লোড হচ্ছে...",
|
||||
"Exit Paragraph Mode": "অনুচ্ছেদ মোড থেকে প্রস্থান করুন",
|
||||
"Paragraph Mode": "অনুচ্ছেদ মোড",
|
||||
"Embedding Model": "এম্বেডিং মডেল",
|
||||
"{{count}} book(s) synced_one": "{{count}}টি বই সিঙ্ক করা হয়েছে",
|
||||
"{{count}} book(s) synced_other": "{{count}}টি বই সিঙ্ক করা হয়েছে",
|
||||
"Unable to start RSVP": "RSVP शुरू করতে অসমর্থ",
|
||||
"RSVP not supported for PDF": "PDF-এর জন্য RSVP সমর্থিত নয়",
|
||||
"Select Chapter": "অধ্যায় নির্বাচন করুন",
|
||||
"Context": "প্রসঙ্গ",
|
||||
"Ready": "প্রস্তুত",
|
||||
"Chapter Progress": "অধ্যায় অগ্রগতি",
|
||||
"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 পড়া শুরু করুন",
|
||||
"Choose where to start reading": "কোথা থেকে পড়া শুরু করবেন তা চয়ন করুন",
|
||||
"From Chapter Start": "অধ্যায় শুরু থেকে",
|
||||
"Start reading from the beginning of the chapter": "অধ্যায়ের শুরু থেকে পড়া শুরু করুন",
|
||||
"Resume": "পুনরায় শুরু করুন",
|
||||
"Continue from where you left off": "যেখানে আপনি ছেড়েছিলেন সেখান থেকে চালিয়ে যান",
|
||||
"From Current Page": "বর্তমান পৃষ্ঠা থেকে",
|
||||
"Start from where you are currently reading": "আপনি বর্তমানে যেখানে পড়ছেন সেখান থেকে শুরু করুন",
|
||||
"From Selection": "নির্বাচন থেকে",
|
||||
"Speed Reading Mode": "দ্রুত পাঠ্য মোড",
|
||||
"Scroll left": "বামে স্ক্রোল করুন",
|
||||
"Scroll right": "ডানে স্ক্রোল করুন",
|
||||
"Library Sync Progress": "লাইব্রেরি সিঙ্ক অগ্রগতি",
|
||||
"Back to library": "লাইব্রেরিতে ফিরে যান",
|
||||
"Group by...": "গ্রুপ অনুযায়ী...",
|
||||
"Export as Plain Text": "সাধারণ টেক্সট হিসেবে এক্সপোর্ট করুন",
|
||||
"Export as Markdown": "Markdown হিসেবে এক্সপোর্ট করুন",
|
||||
"Show Page Navigation Buttons": "নেভিগেশন বোতাম",
|
||||
"Page {{number}}": "পৃষ্ঠা {{number}}",
|
||||
"highlight": "হাইলাইট",
|
||||
"underline": "আন্ডারলাইন",
|
||||
"squiggly": "এঁকেবেঁকে চলা",
|
||||
"red": "লাল",
|
||||
"violet": "বেগুনী",
|
||||
"blue": "নীল",
|
||||
"green": "সবুজ",
|
||||
"yellow": "হলুদ",
|
||||
"Select {{style}} style": "{{style}} স্টাইল নির্বাচন করুন",
|
||||
"Select {{color}} color": "{{color}} রঙ নির্বাচন করুন",
|
||||
"Close Book": "বই বন্ধ করুন",
|
||||
"Speed Reading": "দ্রুত পঠন",
|
||||
"Close Speed Reading": "দ্রুত পঠন বন্ধ করুন",
|
||||
"Authors": "লেখকগণ",
|
||||
"Books": "বই",
|
||||
"Groups": "দল",
|
||||
"Back to TTS Location": "টিটিএস অবস্থানে ফিরে যান",
|
||||
"Metadata": "মেটাডেটা",
|
||||
"Image viewer": "চিত্র প্রদর্শক",
|
||||
"Previous Image": "পূর্ববর্তী চিত্র",
|
||||
"Next Image": "পরবর্তী চিত্র",
|
||||
"Zoomed": "জুম করা হয়েছে",
|
||||
"Zoom level": "জুম স্তর",
|
||||
"Table viewer": "টেবিল প্রদর্শক",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise এর সাথে সংযোগ করতে অক্ষম। আপনার নেটওয়ার্ক সংযোগটি পরীক্ষা করুন।",
|
||||
"Invalid Readwise access token": "অকার্যকর Readwise অ্যাক্সেস টোকেন",
|
||||
"Disconnected from Readwise": "Readwise থেকে সংযোগ বিচ্ছিন্ন হয়েছে",
|
||||
"Never": "কখনো না",
|
||||
"Readwise Settings": "Readwise সেটিংস",
|
||||
"Connected to Readwise": "Readwise এর সাথে সংযুক্ত",
|
||||
"Last synced: {{time}}": "শেষ সিঙ্ক করা হয়েছে: {{time}}",
|
||||
"Sync Enabled": "সিঙ্কিং সক্ষম",
|
||||
"Disconnect": "সংযোগ বিচ্ছিন্ন করুন",
|
||||
"Connect your Readwise account to sync highlights.": "হাইলাইটগুলি সিঙ্ক করার জন্য আপনার Readwise অ্যাকাউন্টটি সংযোগ করুন।",
|
||||
"Get your access token at": "আপনার অ্যাক্সেস টোকেনটি এখানে পাবেন",
|
||||
"Access Token": "অ্যাক্সেস টোকেন",
|
||||
"Paste your Readwise access token": "আপনার Readwise অ্যাক্সেস টোকেনটি পেস্ট করুন",
|
||||
"Config": "কনফিগ",
|
||||
"Readwise Sync": "Readwise সিঙ্ক",
|
||||
"Push Highlights": "হাইলাইট পাঠান",
|
||||
"Highlights synced to Readwise": "হাইলাইটগুলি Readwise এ সিঙ্ক করা হয়েছে",
|
||||
"Readwise sync failed: no internet connection": "Readwise সিঙ্ক ব্যর্থ হয়েছে: ইন্টারনেট সংযোগ নেই",
|
||||
"Readwise sync failed: {{error}}": "Readwise সিঙ্ক ব্যর্থ হয়েছে: {{error}}",
|
||||
"System Screen Brightness": "সিস্টেম স্ক্রিন উজ্জ্বলতা",
|
||||
"Page:": "পৃষ্ঠা:",
|
||||
"Page: {{number}}": "পৃষ্ঠা: {{number}}",
|
||||
"Annotation page number": "টীকা পৃষ্ঠা নম্বর"
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@
|
||||
"Fit": "མཐུན་སྒྲིག",
|
||||
"Reset {{settings}}": "{{settings}} བསྐྱར་སྒྲིག",
|
||||
"Reset Settings": "སྒྲིག་བཀོད་བསྐྱར་སྒྲིག",
|
||||
"{{count}} pages left in chapter_other": "ལེའུ་འདིར་ད་དུང་ {{count}} ཤོག་ལྷེ་ལྷག་ཡོད།",
|
||||
"{{count}} pages left in chapter_other": "<1>ལེའུ་འདིར་ད་དུང་ </1><0>{{count}}</0><1> ཤོག་ལྷེ་ལྷག་ཡོད།</1>",
|
||||
"Show Remaining Pages": "ལྷག་མའི་ཤོག་ལྷེའི་གྲངས་ཀ་མངོན་པ།",
|
||||
"Source Han Serif CN": "སི་ཡོན་སུང་ཐི།",
|
||||
"Huiwen-MinchoGBK": "ཧུའེ་ཝུན་མིང་ཁྲའོ་ཐི།",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "ཨང་",
|
||||
"Cover": "དེབ་གྱི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Contain": "འབྱོར་བ།",
|
||||
"{{number}} pages left in chapter": "དོན་ཚན་ནང་ཤོག་ཨང་ {{number}} ལོག་གི་འདུག།",
|
||||
"{{number}} pages left in chapter": "<1>དོན་ཚན་ནང་ཤོག་ཨང་ </1><0>{{number}}</0><1> ལོག་གི་འདུག།</1>",
|
||||
"Device": "རྐྱབ་སྐོར",
|
||||
"E-Ink Mode": "ཡིག་སྒྱུར་སྤོ་བ",
|
||||
"Highlight Colors": "མདོག་ཚད་ཀྱི་བསྡུར་བ།",
|
||||
"Auto Screen Brightness": "རྒྱབ་སྐོར་གྱི་འོད་ཟེར་རང་འགུལ།",
|
||||
"Pagination": "ཤོག་གདོང་།",
|
||||
"Disable Double Tap": "བརྒྱབ་གདོང་གཉིས་མ་འགྱོད།",
|
||||
"Tap to Paginate": "ཤོག་གདོང་ལ་ཐོག་འགྲོ།",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "འབྲས་བུ་སྟོན།",
|
||||
"Clear search": "འཚོལ་བཤེར་གཙང་སེལ།",
|
||||
"Clear search history": "འཚོལ་བཤེར་ལོ་རྒྱུས་གཙང་སེལ།",
|
||||
"Quick action disabled": "མྱུར་མགྱོགས་བྱ་འགུལ་བཀག་འགོག",
|
||||
"Tap to Toggle Footer": "ཞབས་མཇུག་སྒོ་འབྱེད་བྱེད་པར་གནོན།",
|
||||
"Exported successfully": "ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
|
||||
"Book exported successfully.": "དཔེ་དེབ་ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "གསལ་བཤད་ལེབ་ངོས།",
|
||||
"Highlighted text": "གཙོ་གནད་ཡིག་རྐྱང་།",
|
||||
"Annotation note": "གསལ་བཤད་ཟིན་བྲིས།",
|
||||
"Update time": "གསར་བཅོས་དུས་ཚོད།",
|
||||
"Date Format Tokens:": "ཚེས་གྲངས་རྣམ་གཞག་རྟགས།:",
|
||||
"Year (4 digits)": "ལོ། (གྲངས་ཐང་4)",
|
||||
"Month (01-12)": "ཟླ། (01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "སྐད་གདངས་འདེམས།",
|
||||
"Toggle Sticky Bottom TTS Bar": "TTS སྡོམ་ཐིག་བརྗེ་བ།",
|
||||
"Display what I'm reading on Discord": "Discord ཐོག་ཀློག་བཞིན་པའི་དཔེ་ཆ་སྟོན།",
|
||||
"Show on Discord": "Discord ཐོག་སྟོན།"
|
||||
"Show on Discord": "Discord ཐོག་སྟོན།",
|
||||
"Instant {{action}}": "འཕྲལ་མར་{{action}}",
|
||||
"Instant {{action}} Disabled": "འཕྲལ་མར་{{action}}་ལྕོགས་མིན་བཟོས།",
|
||||
"Annotation": "མཆན་འགྲེལ།",
|
||||
"Reset Template": "དཔེ་གཞི་བསྐྱར་སྒྲིག",
|
||||
"Annotation style": "མཆན་འགྲེལ་གྱི་བཟོ་ལྟ།",
|
||||
"Annotation color": "མཆན་འགྲེལ་གྱི་ཚོན་མདོག",
|
||||
"Annotation time": "མཆན་འགྲེལ་གྱི་དུས་ཚོད།",
|
||||
"AI": "རིག་ནུས་མིས་བཟོས། (AI)",
|
||||
"Are you sure you want to re-index this book?": "ཁྱེད་ཀྱིས་དཔེ་དེབ་འདི་བསྐྱར་དུ་དཀར་ཆག་བཟོ་རྒྱུ་གཏན་འཁེལ་ཡིན་ནམ།",
|
||||
"Enable AI in Settings": "སྒྲིག་བཀོད་ནང་ AI སྤྱོད་པར་བྱོས།",
|
||||
"Index This Book": "དཔེ་དེབ་འདི་དཀར་ཆག་བཟོ་བ།",
|
||||
"Enable AI search and chat for this book": "དཔེ་དེབ་འདིར་ AI འཚོལ་བཤེར་དང་ཁ་བརྡ་སྤྱོད་པར་བྱོས།",
|
||||
"Start Indexing": "དཀར་ཆག་བཟོ་འགོ་བཙུགས་པ།",
|
||||
"Indexing book...": "དཔེ་དེབ་དཀར་ཆག་བཟོ་བཞིན་པ།...",
|
||||
"Preparing...": "གྲ་སྒྲིག་བྱེད་བཞིན་པ།...",
|
||||
"Delete this conversation?": "ཁ་བརྡ་འདི་བསུབ་དགོས་སམ།",
|
||||
"No conversations yet": "ད་ལྟའི་བར་ཁ་བརྡ་མེད།",
|
||||
"Start a new chat to ask questions about this book": "དཔེ་དེབ་འདིའི་སྐོར་ལ་དྲི་བ་དྲི་བར་ཁ་བརྡ་གསར་པ་ཞིག་འགོ་བཙུགས་པ།",
|
||||
"Rename": "མིང་བརྗེ་བ།",
|
||||
"New Chat": "ཁ་བརྡ་གསར་པ།",
|
||||
"Chat": "ཁ་བརྡ།",
|
||||
"Please enter a model ID": "དཔེ་གཞིའི་ཨང་རྟགས་ནང་འཇུག་བྱོས།",
|
||||
"Model not available or invalid": "དཔེ་གཞི་མེད་པ་འམ་ནུས་མེད།",
|
||||
"Failed to validate model": "དཔེ་གཞི་བདེན་སྦྱོར་བྱེད་ཐུབ་མེད།",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama ལ་འབྲེལ་མཐུད་བྱེད་ཐུབ་མེད། དེ་འཁོར་བཞིན་ཡོད་དམ།",
|
||||
"Invalid API key or connection failed": "API ལྡེ་མིག་ནུས་མེད་དམ་འབྲེལ་མཐུད་ཕམ་པ།",
|
||||
"Connection failed": "འབྲེལ་མཐུད་ཕམ་པ།",
|
||||
"AI Assistant": "AI རོགས་པ།",
|
||||
"Enable AI Assistant": "AI རོགས་པ་སྤྱོད་པར་བྱོས།",
|
||||
"Provider": "མཁོ་འདོན་པ།",
|
||||
"Ollama (Local)": "Ollama (ས་གནས།)",
|
||||
"AI Gateway (Cloud)": "AI འཛུལ་སྒོ། (སྤྲིན་གནས།)",
|
||||
"Ollama Configuration": "Ollama སྒྲིག་བཀོད།",
|
||||
"Refresh Models": "དཔེ་གཞི་བསྐྱར་བརྗེ།",
|
||||
"AI Model": "AI དཔེ་གཞི།",
|
||||
"No models detected": "དཔེ་གཞི་མ་རྙེད།",
|
||||
"AI Gateway Configuration": "AI འཛུལ་སྒོའི་སྒྲིག་བཀོད།",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "སྤུས་ལེགས་དང་ཁེ་ཕན་ཆེ་བའི་ AI དཔེ་གཞི་འདེམས་དགོས། ཁྱེད་ཀྱིས་འོག་གི་ \"རང་བཟོའི་དཔེ་གཞི།\" བདམས་ནས་རང་གི་དཔེ་གཞི་བེད་སྤྱོད་ཀྱང་ཆོག",
|
||||
"API Key": "API ལྡེ་མིག་",
|
||||
"Get Key": "ལྡེ་མིག་ལེན་པ།",
|
||||
"Model": "དཔེ་གཞི།",
|
||||
"Custom Model...": "རང་བཟོའི་དཔེ་གཞི།...",
|
||||
"Custom Model ID": "རང་བཟོའི་དཔེ་གཞིའི་ཨང་རྟགས།",
|
||||
"Validate": "བདེན་སྦྱོར།",
|
||||
"Model available": "དཔེ་གཞི་ཡོད།",
|
||||
"Connection": "འབྲེལ་མཐུད།",
|
||||
"Test Connection": "འབྲེལ་མཐུད་ཚོད་ལྟ།",
|
||||
"Connected": "འབྲེལ་མཐུད་ཟིན།",
|
||||
"Custom Colors": "རང་བཟོས་ཚོན་མདོག",
|
||||
"Color E-Ink Mode": "ཚོན་ལྡན་གློག་རྡུལ་སྣག་ཚའི་རྣམ་པ།",
|
||||
"Reading Ruler": "ལྟ་ཀློག་ཐིག་ཤིང་།",
|
||||
"Enable Reading Ruler": "ལྟ་ཀློག་ཐིག་ཤིང་སྤྱོད་པ།",
|
||||
"Lines to Highlight": "མངོན་གསལ་དུ་གཏོང་དགོས་པའི་ཐིག་ཕྲེང་།",
|
||||
"Ruler Color": "ཐིག་ཤིང་ගི་ཚོན་མདོག",
|
||||
"Command Palette": "བཀོད་འདོམས་པང་ལེབ།",
|
||||
"Search settings and actions...": "སྒྲིག་བཀོད་དང་བྱ་འགུལ་འཚོལ་བ།...",
|
||||
"No results found for": "འབྲས་བུ་མ་རྙེད་པ།",
|
||||
"Type to search settings and actions": "ཡིག་གཟུགས་ནང་འཇུག་བྱས་ཏེ་སྒྲིག་བཀོད་དང་བྱ་འགུལ་འཚོལ་བ།",
|
||||
"Recent": "ཉེ་ཆར།",
|
||||
"navigate": "འགྲུལ་བཞུད།",
|
||||
"select": "འདེམས་པ།",
|
||||
"close": "ཁ་རྒྱག་པ།",
|
||||
"Search Settings": "སྒྲིག་བཀོད་འཚོལ་བ།",
|
||||
"Page Margins": "ཤོག་ངོས་མཐའ་འགྲམ།",
|
||||
"AI Provider": "ནུས་པའི་རིག་རྩལ་མཁོ་སྤྲོད་པ།",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama དཔེ་གཞི།",
|
||||
"AI Gateway Model": "AI Gateway དཔེ་གཞི།",
|
||||
"Actions": "བྱ་འགུལ།",
|
||||
"Navigation": "འགྲུལ་བཞུད།",
|
||||
"Set status for {{count}} book(s)_other": "དེབ་ {{count}} གི་གནས་སྟངས་གཏན་འཁེལ་བྱེད།",
|
||||
"Mark as Unread": "མ་ཀློག་པར་རྟགས་རྒྱག",
|
||||
"Mark as Finished": "ཚར་བར་རྟགས་རྒྱག",
|
||||
"Finished": "ཚར་སོང་།",
|
||||
"Unread": "མ་ཀློག་པ།",
|
||||
"Clear Status": "གནས་སྟངས་གཙང་མ།",
|
||||
"Status": "གནས་སྟངས།",
|
||||
"Loading": "ལེན་བཞིན་པ།...",
|
||||
"Exit Paragraph Mode": "དུམ་མཚམས་རྣམ་པ་ནས་ཕྱིར་ཐོན།",
|
||||
"Paragraph Mode": "དུམ་མཚམས་རྣམ་པ།",
|
||||
"Embedding Model": "གནས་སྒྲིག་དཔེ་གཞི།",
|
||||
"{{count}} book(s) synced_other": "དེབ་ {{count}} མཉམ་འགྲིག་བྱས་ཟིན།",
|
||||
"Unable to start RSVP": "RSVP འགོ་འཛུགས་མ་ཐུབ།",
|
||||
"RSVP not supported for PDF": "PDF ལ་ RSVP རྒྱབ་སྐྱོར་མེད།",
|
||||
"Select Chapter": "ལེའུ་འདེམས་པ།",
|
||||
"Context": "བརྗོད་དོན།",
|
||||
"Ready": "གྲ་སྒྲིག་ཡོད།",
|
||||
"Chapter Progress": "ལེའུའི་འཕེལ་རིམ།",
|
||||
"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 ཀློག་འགོ་འཛུགས་པ།",
|
||||
"Choose where to start reading": "གང་ནས་ཀློག་འགོ་འཛུགས་མིན་འདེམས་པ།",
|
||||
"From Chapter Start": "ལེའུའི་འགོ་ནས།",
|
||||
"Start reading from the beginning of the chapter": "ལེའུ་འདིའི་འགོ་ནས་ཀློག་པ།",
|
||||
"Resume": "མུ་མཐུད་པ།",
|
||||
"Continue from where you left off": "མཚམས་བཞག་སའི་གནས་ནས་མུ་མཐུད་པ།",
|
||||
"From Current Page": "ད་ལྟའི་ཤོག་ལྷེ་ནས།",
|
||||
"Start from where you are currently reading": "ད་ལྟ་ཀློག་བཞིན་པའི་གནས་ནས་འགོ་འཛུགས་པ།",
|
||||
"From Selection": "བདམས་པའི་ནང་དོན་ནས།",
|
||||
"Speed Reading Mode": "མྱུར་ཀློག་རྣམ་པ།",
|
||||
"Scroll left": "གཡོན་ལ་བཤུད་དགོས།",
|
||||
"Scroll right": "གཡས་ལ་བཤུད་དགོས།",
|
||||
"Library Sync Progress": "དཔེ་མཛོད་མཉམ་འབྱུང་གི་རིམ་པ།",
|
||||
"Back to library": "དཔེ་མཛོད་ལ་ལོག་པ།",
|
||||
"Group by...": "དབྱེ་བ་འབྱེད་སྟངས།...",
|
||||
"Export as Plain Text": "ཡིག་རྐྱང་དཀྱུས་མར་ཕྱིར་འདྲེན།",
|
||||
"Export as Markdown": "Markdown དུ་ཕྱིར་འདྲེན།",
|
||||
"Show Page Navigation Buttons": "ཤོག་ངོས་མཐེབ་གནོན།",
|
||||
"Page {{number}}": "ཤོག་ལྷེ། {{number}}",
|
||||
"highlight": "འོད་རྟགས།",
|
||||
"underline": "ཞབས་ཐིག",
|
||||
"squiggly": "ཀྱག་ཀྱག་ཐིག",
|
||||
"red": "དམར་པོ།",
|
||||
"violet": "སྨུག་པོ།",
|
||||
"blue": "སྔོན་པོ།",
|
||||
"green": "ལྗང་ཁུ།",
|
||||
"yellow": "སེར་པོ།",
|
||||
"Select {{style}} style": "{{style}} བཟོ་ལྟ་བདམ་པ།",
|
||||
"Select {{color}} color": "{{color}} ཚོན་མདོག་བདམ་པ།",
|
||||
"Close Book": "དེབ་ཁ་རྒྱག་པ།",
|
||||
"Speed Reading": "མགྱོགས་ཀློག",
|
||||
"Close Speed Reading": "མགྱོགས་ཀློག་ཁ་རྒྱག་པ།",
|
||||
"Authors": "རྩོམ་པ་པོ།",
|
||||
"Books": "དཔེ་ཆ།",
|
||||
"Groups": "ཚོགས་པ།",
|
||||
"Back to TTS Location": "TTS གནས་སར་ཕྱིར་ལོག་པ།",
|
||||
"Metadata": "གནད་སྨིན་གོ་དོན།",
|
||||
"Image viewer": "པར་རིས་ལྟ་བྱེད།",
|
||||
"Previous Image": "སྔོན་མའི་པར་རིས།",
|
||||
"Next Image": "རྗེས་མའི་པར་རིས།",
|
||||
"Zoomed": "ཆེར་བསྐྱེད་ཟིན།",
|
||||
"Zoom level": "ཆེར་བསྐྱེད་རིམ་པ།",
|
||||
"Table viewer": "རེའུ་མིག་ལྟ་བྱེད།",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise ལ་མཐུད་ཐུབ་མ་སོང་། ཁྱེད་ཀྱི་དྲ་རྒྱའི་མཐུད་ལམ་ལ་བརྟག་དཔྱད་གནང་རོགས།",
|
||||
"Invalid Readwise access token": "Readwise འཛུལ་སྤྱོད་ལག་ཁྱེར་ནོར་འདུག",
|
||||
"Disconnected from Readwise": "Readwise ནས་མཐུད་ལམ་བཅད་ཟིན།",
|
||||
"Never": "ནམ་ཡང་མིན།",
|
||||
"Readwise Settings": "Readwise སྒྲིག་བཀོད།",
|
||||
"Connected to Readwise": "Readwise ལ་མཐུད་ཟིན།",
|
||||
"Last synced: {{time}}": "མཐའ་མའི་མཉམ་བྱུང་དུས་ཚོད། {{time}}",
|
||||
"Sync Enabled": "མཉམ་བྱུང་ནུས་པ་སྤར་ཟིན།",
|
||||
"Disconnect": "མཐུད་ལམ་གཅོད་པ།",
|
||||
"Connect your Readwise account to sync highlights.": "ཁྱེད་ཀྱི་ Readwise རྩིས་ཐོ་མཐུད་ནས་བཀོད་མཆན་མཉམ་བྱུང་གནང་རོགས།",
|
||||
"Get your access token at": "འཛུལ་སྤྱོད་ལག་ཁྱེར་འདི་ནས་ལེན་རོགས།",
|
||||
"Access Token": "འཛུལ་སྤྱོད་ལག་ཁྱེེར།",
|
||||
"Paste your Readwise access token": "Readwise འཛུལ་སྤྱོད་ལག་ཁྱེར་འདིར་སྦྱོར་རོགས།",
|
||||
"Config": "སྒྲིག་བཀོད།",
|
||||
"Readwise Sync": "Readwise མཉམ་བྱུང་།",
|
||||
"Push Highlights": "བཀོད་མཆན་སྐྱེལ་བ།",
|
||||
"Highlights synced to Readwise": "བཀོད་མཆན་ Readwise ལ་མཉམ་བྱུང་བྱས་ཟིན།",
|
||||
"Readwise sync failed: no internet connection": "Readwise མཉམ་བྱུང་མ་ཐུབ། དྲ་རྒྱའི་མཐུད་ལམ་མི་འདུག",
|
||||
"Readwise sync failed: {{error}}": "Readwise མཉམ་བྱུང་མ་ཐུབ། {{error}}",
|
||||
"System Screen Brightness": "མ་ལག་ཤེལ་སྒོའི་གསལ་ཚད།",
|
||||
"Page:": "ཤོག་ལྷེ།:",
|
||||
"Page: {{number}}": "ཤོག་ལྷེ།: {{number}}",
|
||||
"Annotation page number": "མཆན་འགྲེལ་ཤོག་ཨང་།"
|
||||
}
|
||||
|
||||
@@ -334,8 +334,8 @@
|
||||
"Fit": "Anpassen",
|
||||
"Reset {{settings}}": "{{settings}} zurücksetzen",
|
||||
"Reset Settings": "Einstellungen zurücksetzen",
|
||||
"{{count}} pages left in chapter_one": "{{count}} Seite verbleibend im Kapitel",
|
||||
"{{count}} pages left in chapter_other": "{{count}} Seiten verbleibend im Kapitel",
|
||||
"{{count}} pages left in chapter_one": "<0>{{count}}</0><1> Seite verbleibend im Kapitel</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> Seiten verbleibend im Kapitel</1>",
|
||||
"Show Remaining Pages": "Verbleibende Seiten anzeigen",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "Größe",
|
||||
"Cover": "Cover",
|
||||
"Contain": "Inhalt",
|
||||
"{{number}} pages left in chapter": "{{number}} Seiten verbleibend im Kapitel",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> Seiten verbleibend im Kapitel</1>",
|
||||
"Device": "Gerät",
|
||||
"E-Ink Mode": "E-Ink-Modus",
|
||||
"Highlight Colors": "Hervorhebungsfarben",
|
||||
"Auto Screen Brightness": "Automatische Bildschirmhelligkeit",
|
||||
"Pagination": "Seitenumbruch",
|
||||
"Disable Double Tap": "Doppeltippen deaktivieren",
|
||||
"Tap to Paginate": "Tippen zum Blättern",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "Ergebnisse anzeigen",
|
||||
"Clear search": "Suche löschen",
|
||||
"Clear search history": "Suchverlauf löschen",
|
||||
"Quick action disabled": "Schnellaktion deaktiviert",
|
||||
"Tap to Toggle Footer": "Tippen, um Fußzeile umzuschalten",
|
||||
"Exported successfully": "Erfolgreich exportiert",
|
||||
"Book exported successfully.": "Buch erfolgreich exportiert.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "Array von Anmerkungen",
|
||||
"Highlighted text": "Markierter Text",
|
||||
"Annotation note": "Anmerkungsnotiz",
|
||||
"Update time": "Aktualisierungszeit",
|
||||
"Date Format Tokens:": "Datumsformat-Token:",
|
||||
"Year (4 digits)": "Jahr (4 Ziffern)",
|
||||
"Month (01-12)": "Monat (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "Stimme auswählen",
|
||||
"Toggle Sticky Bottom TTS Bar": "Fixierte TTS-Leiste umschalten",
|
||||
"Display what I'm reading on Discord": "Zeige was ich auf Discord lese",
|
||||
"Show on Discord": "Auf Discord zeigen"
|
||||
"Show on Discord": "Auf Discord zeigen",
|
||||
"Instant {{action}}": "Sofort {{action}}",
|
||||
"Instant {{action}} Disabled": "Sofort {{action}} deaktiviert",
|
||||
"Annotation": "Anmerkung",
|
||||
"Reset Template": "Vorlage zurücksetzen",
|
||||
"Annotation style": "Anmerkungsstil",
|
||||
"Annotation color": "Anmerkungsfarbe",
|
||||
"Annotation time": "Anmerkungszeit",
|
||||
"AI": "KI",
|
||||
"Are you sure you want to re-index this book?": "Möchten Sie dieses Buch wirklich neu indizieren?",
|
||||
"Enable AI in Settings": "KI in den Einstellungen aktivieren",
|
||||
"Index This Book": "Dieses Buch indizieren",
|
||||
"Enable AI search and chat for this book": "KI-Suche und Chat für dieses Buch aktivieren",
|
||||
"Start Indexing": "Indizierung starten",
|
||||
"Indexing book...": "Buch wird indiziert...",
|
||||
"Preparing...": "Vorbereitung...",
|
||||
"Delete this conversation?": "Diese Unterhaltung löschen?",
|
||||
"No conversations yet": "Noch keine Unterhaltungen",
|
||||
"Start a new chat to ask questions about this book": "Starten Sie einen neuen Chat, um Fragen zu diesem Buch zu stellen",
|
||||
"Rename": "Umbenennen",
|
||||
"New Chat": "Neuer Chat",
|
||||
"Chat": "Chat",
|
||||
"Please enter a model ID": "Bitte geben Sie eine Modell-ID ein",
|
||||
"Model not available or invalid": "Modell nicht verfügbar oder ungültig",
|
||||
"Failed to validate model": "Modellvalidierung fehlgeschlagen",
|
||||
"Couldn't connect to Ollama. Is it running?": "Verbindung zu Ollama fehlgeschlagen. Läuft es?",
|
||||
"Invalid API key or connection failed": "Ungültiger API-Schlüssel oder Verbindung fehlgeschlagen",
|
||||
"Connection failed": "Verbindung fehlgeschlagen",
|
||||
"AI Assistant": "KI-Assistent",
|
||||
"Enable AI Assistant": "KI-Assistent aktivieren",
|
||||
"Provider": "Anbieter",
|
||||
"Ollama (Local)": "Ollama (Lokal)",
|
||||
"AI Gateway (Cloud)": "KI-Gateway (Cloud)",
|
||||
"Ollama Configuration": "Ollama-Konfiguration",
|
||||
"Refresh Models": "Modelle aktualisieren",
|
||||
"AI Model": "KI-Modell",
|
||||
"No models detected": "Keine Modelle erkannt",
|
||||
"AI Gateway Configuration": "KI-Gateway-Konfiguration",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Wählen Sie aus einer Auswahl hochwertiger, wirtschaftlicher KI-Modelle. Sie können auch Ihr eigenes Modell verwenden, indem Sie unten \"Benutzerdefiniertes Modell\" auswählen.",
|
||||
"API Key": "API-Schlüssel",
|
||||
"Get Key": "Schlüssel erhalten",
|
||||
"Model": "Modell",
|
||||
"Custom Model...": "Benutzerdefiniertes Modell...",
|
||||
"Custom Model ID": "Benutzerdefinierte Modell-ID",
|
||||
"Validate": "Validieren",
|
||||
"Model available": "Modell verfügbar",
|
||||
"Connection": "Verbindung",
|
||||
"Test Connection": "Verbindung testen",
|
||||
"Connected": "Verbunden",
|
||||
"Custom Colors": "Benutzerdefinierte Farben",
|
||||
"Color E-Ink Mode": "Farb-E-Ink-Modus",
|
||||
"Reading Ruler": "Lese-Lineal",
|
||||
"Enable Reading Ruler": "Lese-Lineal aktivieren",
|
||||
"Lines to Highlight": "Hervorzuhebende Zeilen",
|
||||
"Ruler Color": "Linealfarbe",
|
||||
"Command Palette": "Befehlspalette",
|
||||
"Search settings and actions...": "Einstellungen und Aktionen suchen...",
|
||||
"No results found for": "Keine Ergebnisse gefunden für",
|
||||
"Type to search settings and actions": "Tippen, um Einstellungen und Aktionen zu suchen",
|
||||
"Recent": "Zuletzt verwendet",
|
||||
"navigate": "navigieren",
|
||||
"select": "auswählen",
|
||||
"close": "schließen",
|
||||
"Search Settings": "Einstellungen suchen",
|
||||
"Page Margins": "Seitenränder",
|
||||
"AI Provider": "KI-Anbieter",
|
||||
"Ollama URL": "Ollama-URL",
|
||||
"Ollama Model": "Ollama-Modell",
|
||||
"AI Gateway Model": "AI Gateway-Modell",
|
||||
"Actions": "Aktionen",
|
||||
"Navigation": "Navigation",
|
||||
"Set status for {{count}} book(s)_one": "Status für {{count}} Buch festlegen",
|
||||
"Set status for {{count}} book(s)_other": "Status für {{count}} Bücher festlegen",
|
||||
"Mark as Unread": "Als ungelesen markieren",
|
||||
"Mark as Finished": "Als beendet markieren",
|
||||
"Finished": "Beendet",
|
||||
"Unread": "Ungelesen",
|
||||
"Clear Status": "Status löschen",
|
||||
"Status": "Status",
|
||||
"Loading": "Laden...",
|
||||
"Exit Paragraph Mode": "Absatzmodus verlassen",
|
||||
"Paragraph Mode": "Absatzmodus",
|
||||
"Embedding Model": "Einbettungsmodell",
|
||||
"{{count}} book(s) synced_one": "{{count}} Buch synchronisiert",
|
||||
"{{count}} book(s) synced_other": "{{count}} Bücher synchronisiert",
|
||||
"Unable to start RSVP": "RSVP kann nicht gestartet werden",
|
||||
"RSVP not supported for PDF": "RSVP wird für PDF nicht unterstützt",
|
||||
"Select Chapter": "Kapitel auswählen",
|
||||
"Context": "Kontext",
|
||||
"Ready": "Bereit",
|
||||
"Chapter Progress": "Kapitelfortschritt",
|
||||
"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",
|
||||
"Choose where to start reading": "Wählen Sie, wo das Lesen beginnen soll",
|
||||
"From Chapter Start": "Vom Kapitelanfang",
|
||||
"Start reading from the beginning of the chapter": "Ab dem Anfang des Kapitels lesen",
|
||||
"Resume": "Fortsetzen",
|
||||
"Continue from where you left off": "Dort fortfahren, wo Sie aufgehört haben",
|
||||
"From Current Page": "Von der aktuellen Seite",
|
||||
"Start from where you are currently reading": "Dort beginnen, wo Sie gerade lesen",
|
||||
"From Selection": "Von der Auswahl",
|
||||
"Speed Reading Mode": "Schnelllesemodus",
|
||||
"Scroll left": "Nach links scrollen",
|
||||
"Scroll right": "Nach rechts scrollen",
|
||||
"Library Sync Progress": "Bibliotheks-Synchronisierungsfortschritt",
|
||||
"Back to library": "Zurück zur Bibliothek",
|
||||
"Group by...": "Gruppieren nach...",
|
||||
"Export as Plain Text": "Als reinen Text exportieren",
|
||||
"Export as Markdown": "Als Markdown exportieren",
|
||||
"Show Page Navigation Buttons": "Navigationsschaltflächen",
|
||||
"Page {{number}}": "Seite {{number}}",
|
||||
"highlight": "Hervorhebung",
|
||||
"underline": "Unterstreichung",
|
||||
"squiggly": "geschlängelt",
|
||||
"red": "rot",
|
||||
"violet": "violett",
|
||||
"blue": "blau",
|
||||
"green": "grün",
|
||||
"yellow": "gelb",
|
||||
"Select {{style}} style": "Stil {{style}} auswählen",
|
||||
"Select {{color}} color": "Farbe {{color}} auswählen",
|
||||
"Close Book": "Buch schließen",
|
||||
"Speed Reading": "Schnelllesen",
|
||||
"Close Speed Reading": "Schnelllesen schließen",
|
||||
"Authors": "Autoren",
|
||||
"Books": "Bücher",
|
||||
"Groups": "Gruppen",
|
||||
"Back to TTS Location": "Zurück zur TTS-Position",
|
||||
"Metadata": "Metadaten",
|
||||
"Image viewer": "Bildbetrachter",
|
||||
"Previous Image": "Vorheriges Bild",
|
||||
"Next Image": "Nächstes Bild",
|
||||
"Zoomed": "Gezoomt",
|
||||
"Zoom level": "Zoomstufe",
|
||||
"Table viewer": "Tabellenbetrachter",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Verbindung zu Readwise nicht möglich. Bitte überprüfen Sie Ihre Netzwerkverbindung.",
|
||||
"Invalid Readwise access token": "Ungültiges Readwise-Zugriffstoken",
|
||||
"Disconnected from Readwise": "Von Readwise getrennt",
|
||||
"Never": "Nie",
|
||||
"Readwise Settings": "Readwise-Einstellungen",
|
||||
"Connected to Readwise": "Mit Readwise verbunden",
|
||||
"Last synced: {{time}}": "Zuletzt synchronisiert: {{time}}",
|
||||
"Sync Enabled": "Synchronisierung aktiviert",
|
||||
"Disconnect": "Trennen",
|
||||
"Connect your Readwise account to sync highlights.": "Verbinden Sie Ihr Readwise-Konto, um Highlights zu synchronisieren.",
|
||||
"Get your access token at": "Holen Sie sich Ihr Zugriffstoken unter",
|
||||
"Access Token": "Zugriffstoken",
|
||||
"Paste your Readwise access token": "Fügen Sie Ihr Readwise-Zugriffstoken ein",
|
||||
"Config": "Konfiguration",
|
||||
"Readwise Sync": "Readwise-Synchronisierung",
|
||||
"Push Highlights": "Highlights übertragen",
|
||||
"Highlights synced to Readwise": "Highlights mit Readwise synchronisiert",
|
||||
"Readwise sync failed: no internet connection": "Readwise-Synchronisierung fehlgeschlagen: Keine Internetverbindung",
|
||||
"Readwise sync failed: {{error}}": "Readwise-Synchronisierung fehlgeschlagen: {{error}}",
|
||||
"System Screen Brightness": "System-Bildschirmhelligkeit",
|
||||
"Page:": "Seite:",
|
||||
"Page: {{number}}": "Seite: {{number}}",
|
||||
"Annotation page number": "Seitenzahl der Anmerkung"
|
||||
}
|
||||
|
||||
@@ -335,8 +335,8 @@
|
||||
"Fit": "Προσαρμογή",
|
||||
"Reset {{settings}}": "Επαναφορά {{settings}}",
|
||||
"Reset Settings": "Επαναφορά ρυθμίσεων",
|
||||
"{{count}} pages left in chapter_one": "Μένει {{count}} σελίδα",
|
||||
"{{count}} pages left in chapter_other": "Μένουν {{count}} σελίδες",
|
||||
"{{count}} pages left in chapter_one": "<1>Μένει </1><0>{{count}}</0><1> σελίδα</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Μένουν </1><0>{{count}}</0><1> σελίδες</1>",
|
||||
"Show Remaining Pages": "Εμφάνιση υπολοίπων",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "Μέγεθος",
|
||||
"Cover": "Εξώφυλλο",
|
||||
"Contain": "Περιέχει",
|
||||
"{{number}} pages left in chapter": "Μένουν {{number}} σελίδες στο κεφάλαιο",
|
||||
"{{number}} pages left in chapter": "<1>Μένουν </1><0>{{number}}</0><1> σελίδες στο κεφάλαιο</1>",
|
||||
"Device": "Συσκευή",
|
||||
"E-Ink Mode": "Λειτουργία E-Ink",
|
||||
"Highlight Colors": "Χρώματα επισήμανσης",
|
||||
"Auto Screen Brightness": "Αυτόματη φωτεινότητα οθόνης",
|
||||
"Pagination": "Σελιδοποίηση",
|
||||
"Disable Double Tap": "Απενεργοποίηση διπλού ταπ",
|
||||
"Tap to Paginate": "Ταπ για σελιδοποίηση",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "Εμφάνιση αποτελεσμάτων",
|
||||
"Clear search": "Εκκαθάριση αναζήτησης",
|
||||
"Clear search history": "Εκκαθάριση ιστορικού αναζήτησης",
|
||||
"Quick action disabled": "Η γρήγορη ενέργεια απενεργοποιήθηκε",
|
||||
"Tap to Toggle Footer": "Πατήστε για εναλλαγή υποσέλιδου",
|
||||
"Exported successfully": "Εξαγωγή επιτυχής",
|
||||
"Book exported successfully.": "Το βιβλίο εξήχθη επιτυχώς.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "Πίνακας σχολιασμών",
|
||||
"Highlighted text": "Επισημασμένο κείμενο",
|
||||
"Annotation note": "Σημείωση σχολιασμού",
|
||||
"Update time": "Ώρα ενημέρωσης",
|
||||
"Date Format Tokens:": "Σύμβολα μορφοποίησης ημερομηνίας:",
|
||||
"Year (4 digits)": "Έτος (4 ψηφία)",
|
||||
"Month (01-12)": "Μήνας (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "Επιλογή φωνής",
|
||||
"Toggle Sticky Bottom TTS Bar": "Εναλλαγή καρφιτσωμένης μπάρας TTS",
|
||||
"Display what I'm reading on Discord": "Εμφάνιση του βιβλίου που διαβάζω στο Discord",
|
||||
"Show on Discord": "Εμφάνιση στο Discord"
|
||||
"Show on Discord": "Εμφάνιση στο Discord",
|
||||
"Instant {{action}}": "Άμεση {{action}}",
|
||||
"Instant {{action}} Disabled": "Άμεση {{action}} απενεργοποιημένη",
|
||||
"Annotation": "Σημείωση",
|
||||
"Reset Template": "Επαναφορά προτύπου",
|
||||
"Annotation style": "Στυλ σχολίου",
|
||||
"Annotation color": "Χρώμα σχολίου",
|
||||
"Annotation time": "Ώρα σχολίου",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Είστε σίγουροι ότι θέλετε να επαναδημιουργήσετε το ευρετήριο αυτού του βιβλίου;",
|
||||
"Enable AI in Settings": "Ενεργοποίηση AI στις ρυθμίσεις",
|
||||
"Index This Book": "Δημιουργία ευρετηρίου για αυτό το βιβλίο",
|
||||
"Enable AI search and chat for this book": "Ενεργοποίηση αναζήτησης AI και συνομιλίας για αυτό το βιβλίο",
|
||||
"Start Indexing": "Έναρξη δημιουργίας ευρετηρίου",
|
||||
"Indexing book...": "Δημιουργία ευρετηρίου βιβλίου...",
|
||||
"Preparing...": "Προετοιμασία...",
|
||||
"Delete this conversation?": "Διαγραφή αυτής της συνομιλίας;",
|
||||
"No conversations yet": "Δεν υπάρχουν συνομιλίες ακόμα",
|
||||
"Start a new chat to ask questions about this book": "Ξεκινήστε μια νέα συνομιλία για να κάνετε ερωτήσεις σχετικά με αυτό το βιβλίο",
|
||||
"Rename": "Μετονομασία",
|
||||
"New Chat": "Νέα συνομιλία",
|
||||
"Chat": "Συνομιλία",
|
||||
"Please enter a model ID": "Εισάγετε ένα αναγνωριστικό μοντέλου",
|
||||
"Model not available or invalid": "Το μοντέλο δεν είναι διαθέσιμο ή δεν είναι έγκυρο",
|
||||
"Failed to validate model": "Αποτυχία επικύρωσης μοντέλου",
|
||||
"Couldn't connect to Ollama. Is it running?": "Δεν ήταν δυνατή η σύνδεση με το Ollama. Εκτελείται;",
|
||||
"Invalid API key or connection failed": "Μη έγκυρο κλειδί API ή αποτυχία σύνδεσης",
|
||||
"Connection failed": "Αποτυχία σύνδεσης",
|
||||
"AI Assistant": "Βοηθός AI",
|
||||
"Enable AI Assistant": "Ενεργοποίηση βοηθού AI",
|
||||
"Provider": "Πάροχος",
|
||||
"Ollama (Local)": "Ollama (Τοπικό)",
|
||||
"AI Gateway (Cloud)": "Πύλη AI (Cloud)",
|
||||
"Ollama Configuration": "Διαμόρφωση Ollama",
|
||||
"Refresh Models": "Ανανέωση μοντέλων",
|
||||
"AI Model": "Μοντέλο AI",
|
||||
"No models detected": "Δεν εντοπίστηκαν μοντέλα",
|
||||
"AI Gateway Configuration": "Διαμόρφωση πύλης AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Επιλέξτε από μια συλλογή υψηλής ποιότητας, οικονομικών μοντέλων AI. Μπορείτε επίσης να χρησιμοποιήσετε το δικό σας μοντέλο επιλέγοντας \"Προσαρμοσμένο μοντέλο\" παρακάτω.",
|
||||
"API Key": "Κλειδί API",
|
||||
"Get Key": "Λήψη κλειδιού",
|
||||
"Model": "Μοντέλο",
|
||||
"Custom Model...": "Προσαρμοσμένο μοντέλο...",
|
||||
"Custom Model ID": "Αναγνωριστικό προσαρμοσμένου μοντέλου",
|
||||
"Validate": "Επικύρωση",
|
||||
"Model available": "Το μοντέλο είναι διαθέσιμο",
|
||||
"Connection": "Σύνδεση",
|
||||
"Test Connection": "Δοκιμή σύνδεσης",
|
||||
"Connected": "Συνδεδεμένο",
|
||||
"Custom Colors": "Προσαρμοσμένα χρώματα",
|
||||
"Color E-Ink Mode": "Λειτουργία έγχρωμου E-Ink",
|
||||
"Reading Ruler": "Χάρακας ανάγνωσης",
|
||||
"Enable Reading Ruler": "Ενεργοποίηση χάρακα ανάγνωσης",
|
||||
"Lines to Highlight": "Γραμμές για επισήμανση",
|
||||
"Ruler Color": "Χρώμα χάρακα",
|
||||
"Command Palette": "Παλέτα εντολών",
|
||||
"Search settings and actions...": "Αναζήτηση ρυθμίσεων και ενεργειών...",
|
||||
"No results found for": "Δεν βρέθηκαν αποτελέσματα για",
|
||||
"Type to search settings and actions": "Πληκτρολογήστε για αναζήτηση ρυθμίσεων και ενεργειών",
|
||||
"Recent": "Πρόσφατα",
|
||||
"navigate": "πλοήγηση",
|
||||
"select": "επιλογή",
|
||||
"close": "κλείσιμο",
|
||||
"Search Settings": "Αναζήτηση ρυθμίσεων",
|
||||
"Page Margins": "Περιθώρια σελίδας",
|
||||
"AI Provider": "Πάροχος AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Μοντέλο Ollama",
|
||||
"AI Gateway Model": "Μοντέλο AI Gateway",
|
||||
"Actions": "Ενέργειες",
|
||||
"Navigation": "Πλοήγηση",
|
||||
"Set status for {{count}} book(s)_one": "Ορισμός κατάστασης για {{count}} βιβλίο",
|
||||
"Set status for {{count}} book(s)_other": "Ορισμός κατάστασης για {{count}} βιβλία",
|
||||
"Mark as Unread": "Σήμανση ως μη αναγνωσμένο",
|
||||
"Mark as Finished": "Σήμανση ως ολοκληρωμένο",
|
||||
"Finished": "Ολοκληρώθηκε",
|
||||
"Unread": "Μη αναγνωσμένο",
|
||||
"Clear Status": "Εκκαθάριση κατάστασης",
|
||||
"Status": "Κατάσταση",
|
||||
"Loading": "Φόρτωση...",
|
||||
"Exit Paragraph Mode": "Έξοδος από τη λειτουργία παραγράφου",
|
||||
"Paragraph Mode": "Λειτουργία παραγράφου",
|
||||
"Embedding Model": "Μοντέλο ενσωμάτωσης",
|
||||
"{{count}} book(s) synced_one": "{{count}} βιβλίο συγχρονίστηκε",
|
||||
"{{count}} book(s) synced_other": "{{count}} βιβλία συγχρονίστηκαν",
|
||||
"Unable to start RSVP": "Αδυναμία έναρξης RSVP",
|
||||
"RSVP not supported for PDF": "Το RSVP δεν υποστηρίζεται για PDF",
|
||||
"Select Chapter": "Επιλογή Κεφαλαίου",
|
||||
"Context": "Πλαίσιο",
|
||||
"Ready": "Έτοιμο",
|
||||
"Chapter Progress": "Πρόοδος Κεφαλαίου",
|
||||
"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",
|
||||
"Choose where to start reading": "Επιλέξτε από πού θα ξεκινήσετε την ανάγνωση",
|
||||
"From Chapter Start": "Από την αρχή του κεφαλαίου",
|
||||
"Start reading from the beginning of the chapter": "Ξεκινήστε την ανάγνωση από την αρχή του κεφαλαίου",
|
||||
"Resume": "Συνέχεια",
|
||||
"Continue from where you left off": "Συνεχίστε από εκεί που σταματήσατε",
|
||||
"From Current Page": "Από την τρέχουσα σελίδα",
|
||||
"Start from where you are currently reading": "Ξεκινήστε από εκεί που διαβάζετε αυτή τη στιγμή",
|
||||
"From Selection": "Από την επιλογή",
|
||||
"Speed Reading Mode": "Λειτουργία ταχείας ανάγνωσης",
|
||||
"Scroll left": "Κύλιση αριστερά",
|
||||
"Scroll right": "Κύλιση δεξιά",
|
||||
"Library Sync Progress": "Πρόοδος συγχρονισμού βιβλιοθήκης",
|
||||
"Back to library": "Επιστροφή στη βιβλιοθήκη",
|
||||
"Group by...": "Ομαδοποίηση κατά...",
|
||||
"Export as Plain Text": "Αποστολή ως απλό κείμενο",
|
||||
"Export as Markdown": "Αποστολή ως Markdown",
|
||||
"Show Page Navigation Buttons": "Κουμπιά πλοήγησης",
|
||||
"Page {{number}}": "Σελίδα {{number}}",
|
||||
"highlight": "επισήμανση",
|
||||
"underline": "υπογράμμιση",
|
||||
"squiggly": "κυματιστή γραμμή",
|
||||
"red": "κόκκινο",
|
||||
"violet": "βιολετί",
|
||||
"blue": "μπλε",
|
||||
"green": "πράσινο",
|
||||
"yellow": "κίτρινο",
|
||||
"Select {{style}} style": "Επιλέξτε στυλ {{style}}",
|
||||
"Select {{color}} color": "Επιλέξτε χρώμα {{color}}",
|
||||
"Close Book": "Κλείσιμο βιβλίου",
|
||||
"Speed Reading": "Γρήγορη Ανάγνωση",
|
||||
"Close Speed Reading": "Κλείσιμο Γρήγορης Ανάγνωσης",
|
||||
"Authors": "Συγγραφείς",
|
||||
"Books": "Βιβλία",
|
||||
"Groups": "Ομάδες",
|
||||
"Back to TTS Location": "Επιστροφή στην τοποθεσία TTS",
|
||||
"Metadata": "Μεταδεδομένα",
|
||||
"Image viewer": "Πρόγραμμα προβολής εικόνων",
|
||||
"Previous Image": "Προηγούμενη εικόνα",
|
||||
"Next Image": "Επόμενη εικόνα",
|
||||
"Zoomed": "Ζουμαρισμένο",
|
||||
"Zoom level": "Επίπεδο ζουμ",
|
||||
"Table viewer": "Πρόγραμμα προβολής πινάκων",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Αδυναμία σύνδεσης στο Readwise. Ελέγξτε τη σύνδεση του δικτύου σας.",
|
||||
"Invalid Readwise access token": "Μη έγκυρο διακριτικό πρόσβασης Readwise",
|
||||
"Disconnected from Readwise": "Αποσυνδέθηκε από το Readwise",
|
||||
"Never": "Ποτέ",
|
||||
"Readwise Settings": "Ρυθμίσεις Readwise",
|
||||
"Connected to Readwise": "Συνδέθηκε στο Readwise",
|
||||
"Last synced: {{time}}": "Τελευταίος συγχρονισμός: {{time}}",
|
||||
"Sync Enabled": "Ο συγχρονισμός ενεργοποιήθηκε",
|
||||
"Disconnect": "Αποσύνδεση",
|
||||
"Connect your Readwise account to sync highlights.": "Συνδέστε τον λογαριασμό σας Readwise για να συγχρονίσετε τις επισημάνσεις.",
|
||||
"Get your access token at": "Αποκτήστε το διακριτικό πρόσβασής σας στο",
|
||||
"Access Token": "Διακριτικό πρόσβασης",
|
||||
"Paste your Readwise access token": "Επικολλήστε το διακριτικό πρόσβασης Readwise",
|
||||
"Config": "Ρύθμιση παραμέτρων",
|
||||
"Readwise Sync": "Συγχρονισμός Readwise",
|
||||
"Push Highlights": "Προώθηση επισημάνσεων",
|
||||
"Highlights synced to Readwise": "Οι επισημάνσεις συγχρονίστηκαν στο Readwise",
|
||||
"Readwise sync failed: no internet connection": "Ο συγχρονισμός Readwise απέτυχε: δεν υπάρχει σύνδεση στο διαδίκτυο",
|
||||
"Readwise sync failed: {{error}}": "Ο συγχρονισμός Readwise απέτυχε: {{error}}",
|
||||
"System Screen Brightness": "Φωτεινότητα οθόνης συστήματος",
|
||||
"Page:": "Σελίδα:",
|
||||
"Page: {{number}}": "Σελίδα: {{number}}",
|
||||
"Annotation page number": "Αριθμός σελίδας σχολίου"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Are you sure to delete {{count}} selected books?",
|
||||
"Search in {{count}} Book(s)..._one": "Search in {{count}} book...",
|
||||
"Search in {{count}} Book(s)..._other": "Search in {{count}} books...",
|
||||
"{{count}} pages left in chapter_one": "{{count}} page left in chapter",
|
||||
"{{count}} pages left in chapter_other": "{{count}} pages left in chapter",
|
||||
"{{count}} pages left in chapter_one": "<0>{{count}}</0><1> page left in chapter</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> pages left in chapter</1>",
|
||||
"Deleted {{count}} file(s)_one": "Deleted {{count}} file",
|
||||
"Deleted {{count}} file(s)_other": "Deleted {{count}} files",
|
||||
"Failed to delete {{count}} file(s)_one": "Failed to delete {{count}} file",
|
||||
@@ -19,5 +19,9 @@
|
||||
"Are you sure to delete {{count}} selected file(s)?_one": "Are you sure to delete {{count}} selected file?",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "Are you sure to delete {{count}} selected files?",
|
||||
"Successfully imported {{count}} book(s)_one": "Successfully imported {{count}} book",
|
||||
"Successfully imported {{count}} book(s)_other": "Successfully imported {{count}} books"
|
||||
"Successfully imported {{count}} book(s)_other": "Successfully imported {{count}} books",
|
||||
"Set status for {{count}} book(s)_one": "Set status for {{count}} book",
|
||||
"Set status for {{count}} book(s)_other": "Set status for {{count}} books",
|
||||
"{{count}} book(s) synced_one": "{{count}} book synced",
|
||||
"{{count}} book(s) synced_other": "{{count}} books synced"
|
||||
}
|
||||
|
||||
@@ -365,9 +365,9 @@
|
||||
"Fit": "Encajar",
|
||||
"Reset {{settings}}": "Restablecer {{settings}}",
|
||||
"Reset Settings": "Restablecer configuración",
|
||||
"{{count}} pages left in chapter_one": "{{count}} página restante en el capítulo",
|
||||
"{{count}} pages left in chapter_many": "{{count}} páginas restantes en el capítulo",
|
||||
"{{count}} pages left in chapter_other": "{{count}} páginas restantes en el capítulo",
|
||||
"{{count}} pages left in chapter_one": "<0>{{count}}</0><1> página restante en el capítulo</1>",
|
||||
"{{count}} pages left in chapter_many": "<0>{{count}}</0><1> páginas restantes en el capítulo</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> páginas restantes en el capítulo</1>",
|
||||
"Show Remaining Pages": "Mostrar resto",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -603,11 +603,10 @@
|
||||
"Size": "Tamaño",
|
||||
"Cover": "Cubierta",
|
||||
"Contain": "Contener",
|
||||
"{{number}} pages left in chapter": "{{number}} páginas restantes en el capítulo",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> páginas restantes en el capítulo</1>",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modo E-Ink",
|
||||
"Highlight Colors": "Colores de resaltado",
|
||||
"Auto Screen Brightness": "Brillo automático de pantalla",
|
||||
"Pagination": "Paginación",
|
||||
"Disable Double Tap": "Deshabilitar Doble Toque",
|
||||
"Tap to Paginate": "Tocar para Paginación",
|
||||
@@ -842,7 +841,6 @@
|
||||
"Show Results": "Mostrar resultados",
|
||||
"Clear search": "Borrar búsqueda",
|
||||
"Clear search history": "Borrar historial de búsqueda",
|
||||
"Quick action disabled": "Acción rápida desactivada",
|
||||
"Tap to Toggle Footer": "Toca para alternar pie de página",
|
||||
"Exported successfully": "Exportado con éxito",
|
||||
"Book exported successfully.": "Libro exportado con éxito.",
|
||||
@@ -885,7 +883,6 @@
|
||||
"Array of annotations": "Lista de anotaciones",
|
||||
"Highlighted text": "Texto resaltado",
|
||||
"Annotation note": "Nota de anotación",
|
||||
"Update time": "Hora de actualización",
|
||||
"Date Format Tokens:": "Tokens de formato de fecha:",
|
||||
"Year (4 digits)": "Año (4 dígitos)",
|
||||
"Month (01-12)": "Mes (01-12)",
|
||||
@@ -900,5 +897,179 @@
|
||||
"Select Voice": "Seleccionar voz",
|
||||
"Toggle Sticky Bottom TTS Bar": "Alternar barra TTS fija",
|
||||
"Display what I'm reading on Discord": "Mostrar lo que estoy leyendo en Discord",
|
||||
"Show on Discord": "Mostrar en Discord"
|
||||
"Show on Discord": "Mostrar en Discord",
|
||||
"Instant {{action}}": "{{action}} instantáneo",
|
||||
"Instant {{action}} Disabled": "{{action}} instantáneo desactivado",
|
||||
"Annotation": "Anotación",
|
||||
"Reset Template": "Restablecer plantilla",
|
||||
"Annotation style": "Estilo de anotación",
|
||||
"Annotation color": "Color de anotación",
|
||||
"Annotation time": "Hora de anotación",
|
||||
"AI": "IA",
|
||||
"Are you sure you want to re-index this book?": "¿Está seguro de que desea reindexar este libro?",
|
||||
"Enable AI in Settings": "Habilitar IA en Configuración",
|
||||
"Index This Book": "Indexar este libro",
|
||||
"Enable AI search and chat for this book": "Habilitar búsqueda y chat con IA para este libro",
|
||||
"Start Indexing": "Iniciar indexación",
|
||||
"Indexing book...": "Indexando libro...",
|
||||
"Preparing...": "Preparando...",
|
||||
"Delete this conversation?": "¿Eliminar esta conversación?",
|
||||
"No conversations yet": "Aún no hay conversaciones",
|
||||
"Start a new chat to ask questions about this book": "Inicie un nuevo chat para hacer preguntas sobre este libro",
|
||||
"Rename": "Renombrar",
|
||||
"New Chat": "Nuevo chat",
|
||||
"Chat": "Chat",
|
||||
"Please enter a model ID": "Por favor, ingrese un ID de modelo",
|
||||
"Model not available or invalid": "Modelo no disponible o inválido",
|
||||
"Failed to validate model": "Error al validar el modelo",
|
||||
"Couldn't connect to Ollama. Is it running?": "No se pudo conectar a Ollama. ¿Está ejecutándose?",
|
||||
"Invalid API key or connection failed": "Clave API inválida o conexión fallida",
|
||||
"Connection failed": "Conexión fallida",
|
||||
"AI Assistant": "Asistente de IA",
|
||||
"Enable AI Assistant": "Habilitar asistente de IA",
|
||||
"Provider": "Proveedor",
|
||||
"Ollama (Local)": "Ollama (Local)",
|
||||
"AI Gateway (Cloud)": "Pasarela de IA (Nube)",
|
||||
"Ollama Configuration": "Configuración de Ollama",
|
||||
"Refresh Models": "Actualizar modelos",
|
||||
"AI Model": "Modelo de IA",
|
||||
"No models detected": "No se detectaron modelos",
|
||||
"AI Gateway Configuration": "Configuración de pasarela de IA",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Elija entre una selección de modelos de IA de alta calidad y económicos. También puede usar su propio modelo seleccionando \"Modelo personalizado\" a continuación.",
|
||||
"API Key": "Clave API",
|
||||
"Get Key": "Obtener clave",
|
||||
"Model": "Modelo",
|
||||
"Custom Model...": "Modelo personalizado...",
|
||||
"Custom Model ID": "ID de modelo personalizado",
|
||||
"Validate": "Validar",
|
||||
"Model available": "Modelo disponible",
|
||||
"Connection": "Conexión",
|
||||
"Test Connection": "Probar conexión",
|
||||
"Connected": "Conectado",
|
||||
"Custom Colors": "Colores personalizados",
|
||||
"Color E-Ink Mode": "Modo E-Ink a color",
|
||||
"Reading Ruler": "Regla de lectura",
|
||||
"Enable Reading Ruler": "Habilitar regla de lectura",
|
||||
"Lines to Highlight": "Líneas a resaltar",
|
||||
"Ruler Color": "Color de la regla",
|
||||
"Command Palette": "Paleta de comandos",
|
||||
"Search settings and actions...": "Buscar ajustes y acciones...",
|
||||
"No results found for": "No se encontraron resultados para",
|
||||
"Type to search settings and actions": "Escribe para buscar ajustes y acciones",
|
||||
"Recent": "Reciente",
|
||||
"navigate": "navegar",
|
||||
"select": "seleccionar",
|
||||
"close": "cerrar",
|
||||
"Search Settings": "Buscar ajustes",
|
||||
"Page Margins": "Márgenes de página",
|
||||
"AI Provider": "Proveedor de IA",
|
||||
"Ollama URL": "URL de Ollama",
|
||||
"Ollama Model": "Modelo de Ollama",
|
||||
"AI Gateway Model": "Modelo de AI Gateway",
|
||||
"Actions": "Acciones",
|
||||
"Navigation": "Navegación",
|
||||
"Set status for {{count}} book(s)_one": "Establecer estado para {{count}} libro",
|
||||
"Set status for {{count}} book(s)_other": "Establecer estado para {{count}} libros",
|
||||
"Mark as Unread": "Marcar como no leído",
|
||||
"Mark as Finished": "Marcar como terminado",
|
||||
"Finished": "Terminado",
|
||||
"Unread": "Sin leer",
|
||||
"Clear Status": "Borrar estado",
|
||||
"Status": "Estado",
|
||||
"Set status for {{count}} book(s)_many": "Establecer estado para {{count}} libros",
|
||||
"Loading": "Cargando...",
|
||||
"Exit Paragraph Mode": "Salir del modo de párrafo",
|
||||
"Paragraph Mode": "Modo de párrafo",
|
||||
"Embedding Model": "Modelo de incrustación",
|
||||
"{{count}} book(s) synced_one": "{{count}} libro sincronizado",
|
||||
"{{count}} book(s) synced_many": "{{count}} libros sincronizados",
|
||||
"{{count}} book(s) synced_other": "{{count}} libros sincronizados",
|
||||
"Unable to start RSVP": "No se puede iniciar RSVP",
|
||||
"RSVP not supported for PDF": "RSVP no es compatible con PDF",
|
||||
"Select Chapter": "Seleccionar capítulo",
|
||||
"Context": "Contexto",
|
||||
"Ready": "Listo",
|
||||
"Chapter Progress": "Progreso del capítulo",
|
||||
"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",
|
||||
"Choose where to start reading": "Elegir dónde empezar a leer",
|
||||
"From Chapter Start": "Desde el inicio del capítulo",
|
||||
"Start reading from the beginning of the chapter": "Empezar a leer desde el principio del capítulo",
|
||||
"Resume": "Reanudar",
|
||||
"Continue from where you left off": "Continuar desde donde lo dejaste",
|
||||
"From Current Page": "Desde la página actual",
|
||||
"Start from where you are currently reading": "Empezar desde donde estás leyendo actualmente",
|
||||
"From Selection": "Desde la selección",
|
||||
"Speed Reading Mode": "Modo de lectura rápida",
|
||||
"Scroll left": "Desplazar a la izquierda",
|
||||
"Scroll right": "Desplazar a la derecha",
|
||||
"Library Sync Progress": "Progreso de sincronización de la biblioteca",
|
||||
"Back to library": "Volver a la biblioteca",
|
||||
"Group by...": "Agrupar por...",
|
||||
"Export as Plain Text": "Exportar como texto sin formato",
|
||||
"Export as Markdown": "Exportar como Markdown",
|
||||
"Show Page Navigation Buttons": "Botones de navegación",
|
||||
"Page {{number}}": "Página {{number}}",
|
||||
"highlight": "resaltado",
|
||||
"underline": "subrayado",
|
||||
"squiggly": "ondulado",
|
||||
"red": "rojo",
|
||||
"violet": "violeta",
|
||||
"blue": "azul",
|
||||
"green": "verde",
|
||||
"yellow": "amarillo",
|
||||
"Select {{style}} style": "Seleccionar estilo {{style}}",
|
||||
"Select {{color}} color": "Seleccionar color {{color}}",
|
||||
"Close Book": "Cerrar libro",
|
||||
"Speed Reading": "Lectura rápida",
|
||||
"Close Speed Reading": "Cerrar lectura rápida",
|
||||
"Authors": "Autores",
|
||||
"Books": "Libros",
|
||||
"Groups": "Grupos",
|
||||
"Back to TTS Location": "Volver a la ubicación de TTS",
|
||||
"Metadata": "Metadatos",
|
||||
"Image viewer": "Visor de imágenes",
|
||||
"Previous Image": "Imagen anterior",
|
||||
"Next Image": "Imagen siguiente",
|
||||
"Zoomed": "Con zoom",
|
||||
"Zoom level": "Nivel de zoom",
|
||||
"Table viewer": "Visor de tablas",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "No se pudo conectar a Readwise. Por favor, comprueba tu conexión a la red.",
|
||||
"Invalid Readwise access token": "Token de acceso a Readwise no válido",
|
||||
"Disconnected from Readwise": "Desconectado de Readwise",
|
||||
"Never": "Nunca",
|
||||
"Readwise Settings": "Ajustes de Readwise",
|
||||
"Connected to Readwise": "Conectado a Readwise",
|
||||
"Last synced: {{time}}": "Última sincronización: {{time}}",
|
||||
"Sync Enabled": "Sincronización habilitada",
|
||||
"Disconnect": "Desconectar",
|
||||
"Connect your Readwise account to sync highlights.": "Conecta tu cuenta de Readwise para sincronizar los resaltados.",
|
||||
"Get your access token at": "Obtén tu token de acceso en",
|
||||
"Access Token": "Token de acceso",
|
||||
"Paste your Readwise access token": "Pega tu token de acceso de Readwise",
|
||||
"Config": "Configuración",
|
||||
"Readwise Sync": "Sincronización con Readwise",
|
||||
"Push Highlights": "Enviar resaltados",
|
||||
"Highlights synced to Readwise": "Resaltados sincronizados con Readwise",
|
||||
"Readwise sync failed: no internet connection": "Error en la sincronización con Readwise: sin conexión a internet",
|
||||
"Readwise sync failed: {{error}}": "Error en la sincronización con Readwise: {{error}}",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -334,8 +334,8 @@
|
||||
"Fit": "متناسب",
|
||||
"Reset {{settings}}": "بازنشانی {{settings}}",
|
||||
"Reset Settings": "بازنشانی تنظیمات",
|
||||
"{{count}} pages left in chapter_one": "{{count}} صفحه تا انتهای فصل",
|
||||
"{{count}} pages left in chapter_other": "{{count}} صفحه تا انتهای فصل",
|
||||
"{{count}} pages left in chapter_one": "<0>{{count}}</0><1> صفحه تا انتهای فصل</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> صفحه تا انتهای فصل</1>",
|
||||
"Show Remaining Pages": "نمایش صفحات باقیمانده",
|
||||
"Source Han Serif CN": "Source Han Serif CN",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "اندازه",
|
||||
"Cover": "جلد",
|
||||
"Contain": "شامل",
|
||||
"{{number}} pages left in chapter": "{{number}} صفحه تا انتهای فصل",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> صفحه تا انتهای فصل</1>",
|
||||
"Device": "دستگاه",
|
||||
"E-Ink Mode": "حالت E-Ink",
|
||||
"Highlight Colors": "رنگهای هایلایت",
|
||||
"Auto Screen Brightness": "روشنایی خودکار صفحه",
|
||||
"Pagination": "صفحهبندی",
|
||||
"Disable Double Tap": "غیرفعال کردن کلیک دوبل",
|
||||
"Tap to Paginate": "ضربه برای صفحهبندی",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "نمایش نتایج",
|
||||
"Clear search": "پاک کردن جستجو",
|
||||
"Clear search history": "پاک کردن تاریخچه جستجو",
|
||||
"Quick action disabled": "عملکرد سریع غیرفعال است",
|
||||
"Tap to Toggle Footer": "برای تغییر پاورقی ضربه بزنید",
|
||||
"Exported successfully": "صادرات موفق",
|
||||
"Book exported successfully.": "کتاب با موفقیت صادر شد.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "آرایه یادداشتها",
|
||||
"Highlighted text": "متن برجستهشده",
|
||||
"Annotation note": "یادداشت حاشیه",
|
||||
"Update time": "زمان بهروزرسانی",
|
||||
"Date Format Tokens:": "نشانههای قالب تاریخ:",
|
||||
"Year (4 digits)": "سال (۴ رقم)",
|
||||
"Month (01-12)": "ماه (۰۱-۱۲)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "انتخاب صدا",
|
||||
"Toggle Sticky Bottom TTS Bar": "تغییر نوار TTS ثابت",
|
||||
"Display what I'm reading on Discord": "نمایش کتاب در حال خواندن در Discord",
|
||||
"Show on Discord": "نمایش در Discord"
|
||||
"Show on Discord": "نمایش در Discord",
|
||||
"Instant {{action}}": "{{action}} فوری",
|
||||
"Instant {{action}} Disabled": "{{action}} فوری غیرفعال شد",
|
||||
"Annotation": "یادداشت",
|
||||
"Reset Template": "بازنشانی قالب",
|
||||
"Annotation style": "سبک یادداشت",
|
||||
"Annotation color": "رنگ یادداشت",
|
||||
"Annotation time": "زمان یادداشت",
|
||||
"AI": "هوش مصنوعی",
|
||||
"Are you sure you want to re-index this book?": "آیا مطمئن هستید که میخواهید این کتاب را دوباره فهرست کنید؟",
|
||||
"Enable AI in Settings": "فعال کردن هوش مصنوعی در تنظیمات",
|
||||
"Index This Book": "فهرست کردن این کتاب",
|
||||
"Enable AI search and chat for this book": "فعال کردن جستجو و گفتگوی هوش مصنوعی برای این کتاب",
|
||||
"Start Indexing": "شروع فهرستبندی",
|
||||
"Indexing book...": "در حال فهرستبندی کتاب...",
|
||||
"Preparing...": "در حال آمادهسازی...",
|
||||
"Delete this conversation?": "این مکالمه حذف شود؟",
|
||||
"No conversations yet": "هنوز مکالمهای وجود ندارد",
|
||||
"Start a new chat to ask questions about this book": "یک گفتگوی جدید برای پرسیدن سؤال درباره این کتاب شروع کنید",
|
||||
"Rename": "تغییر نام",
|
||||
"New Chat": "گفتگوی جدید",
|
||||
"Chat": "گفتگو",
|
||||
"Please enter a model ID": "لطفاً شناسه مدل را وارد کنید",
|
||||
"Model not available or invalid": "مدل در دسترس نیست یا نامعتبر است",
|
||||
"Failed to validate model": "اعتبارسنجی مدل ناموفق بود",
|
||||
"Couldn't connect to Ollama. Is it running?": "اتصال به Ollama ممکن نشد. آیا در حال اجرا است؟",
|
||||
"Invalid API key or connection failed": "کلید API نامعتبر است یا اتصال ناموفق بود",
|
||||
"Connection failed": "اتصال ناموفق بود",
|
||||
"AI Assistant": "دستیار هوش مصنوعی",
|
||||
"Enable AI Assistant": "فعال کردن دستیار هوش مصنوعی",
|
||||
"Provider": "ارائهدهنده",
|
||||
"Ollama (Local)": "Ollama (محلی)",
|
||||
"AI Gateway (Cloud)": "دروازه هوش مصنوعی (ابری)",
|
||||
"Ollama Configuration": "پیکربندی Ollama",
|
||||
"Refresh Models": "بازخوانی مدلها",
|
||||
"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.": "از میان مجموعهای از مدلهای هوش مصنوعی با کیفیت و اقتصادی انتخاب کنید. همچنین میتوانید با انتخاب \"مدل سفارشی\" در زیر، مدل خود را استفاده کنید.",
|
||||
"API Key": "کلید API",
|
||||
"Get Key": "دریافت کلید",
|
||||
"Model": "مدل",
|
||||
"Custom Model...": "مدل سفارشی...",
|
||||
"Custom Model ID": "شناسه مدل سفارشی",
|
||||
"Validate": "اعتبارسنجی",
|
||||
"Model available": "مدل در دسترس است",
|
||||
"Connection": "اتصال",
|
||||
"Test Connection": "تست اتصال",
|
||||
"Connected": "متصل",
|
||||
"Custom Colors": "رنگهای سفارشی",
|
||||
"Color E-Ink Mode": "حالت جوهر الکترونیک رنگی",
|
||||
"Reading Ruler": "خطکش مطالعه",
|
||||
"Enable Reading Ruler": "فعالسازی خطکش مطالعه",
|
||||
"Lines to Highlight": "خطوط برای تمایز",
|
||||
"Ruler Color": "رنگ خطکش",
|
||||
"Command Palette": "پالت فرمان",
|
||||
"Search settings and actions...": "جستجوی تنظیمات و عملیات...",
|
||||
"No results found for": "نتیجهای برای این مورد یافت نشد",
|
||||
"Type to search settings and actions": "برای جستجوی تنظیمات و عملیات تایپ کنید",
|
||||
"Recent": "اخیر",
|
||||
"navigate": "ناوبری",
|
||||
"select": "انتخاب",
|
||||
"close": "بستن",
|
||||
"Search Settings": "جستجوی تنظیمات",
|
||||
"Page Margins": "حاشیههای صفحه",
|
||||
"AI Provider": "ارائه دهنده هوش مصنوعی",
|
||||
"Ollama URL": "آدرس Ollama",
|
||||
"Ollama Model": "مدل Ollama",
|
||||
"AI Gateway Model": "مدل AI Gateway",
|
||||
"Actions": "عملیات",
|
||||
"Navigation": "ناوبری",
|
||||
"Set status for {{count}} book(s)_one": "تعیین وضعیت برای {{count}} کتاب",
|
||||
"Set status for {{count}} book(s)_other": "تعیین وضعیت برای {{count}} کتاب",
|
||||
"Mark as Unread": "علامتگذاری بهعنوان خواندهنشده",
|
||||
"Mark as Finished": "علامتگذاری بهعنوان تمامشده",
|
||||
"Finished": "تمامشده",
|
||||
"Unread": "خواندهنشده",
|
||||
"Clear Status": "پاک کردن وضعیت",
|
||||
"Status": "وضعیت",
|
||||
"Loading": "در حال بارگذاری...",
|
||||
"Exit Paragraph Mode": "خروج از حالت بند",
|
||||
"Paragraph Mode": "حالت بند",
|
||||
"Embedding Model": "مدل جاسازی",
|
||||
"{{count}} book(s) synced_one": "{{count}} کتاب همگام شد",
|
||||
"{{count}} book(s) synced_other": "{{count}} کتاب همگام شدند",
|
||||
"Unable to start RSVP": "قادر به شروع RSVP نیست",
|
||||
"RSVP not supported for PDF": "RSVP برای PDF پشتیبانی نمیشود",
|
||||
"Select Chapter": "انتخاب فصل",
|
||||
"Context": "زمینه",
|
||||
"Ready": "آماده",
|
||||
"Chapter Progress": "پیشرفت فصل",
|
||||
"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",
|
||||
"Choose where to start reading": "انتخاب کنید از کجا مطالعه شروع شود",
|
||||
"From Chapter Start": "از ابتدای فصل",
|
||||
"Start reading from the beginning of the chapter": "شروع مطالعه از ابتدای فصل",
|
||||
"Resume": "ادامه",
|
||||
"Continue from where you left off": "ادامه از جایی که متوقف شدید",
|
||||
"From Current Page": "از صفحه فعلی",
|
||||
"Start from where you are currently reading": "شروع از جایی که در حال مطالعه هستید",
|
||||
"From Selection": "از انتخاب",
|
||||
"Speed Reading Mode": "حالت مطالعه سریع",
|
||||
"Scroll left": "اسکرول به چپ",
|
||||
"Scroll right": "اسکرول به راست",
|
||||
"Library Sync Progress": "پیشرفت همگامسازی کتابخانه",
|
||||
"Back to library": "بازگشت به کتابخانه",
|
||||
"Group by...": "گروهبندی بر اساس...",
|
||||
"Export as Plain Text": "خروجی به صورت متن ساده",
|
||||
"Export as Markdown": "خروجی به صورت Markdown",
|
||||
"Show Page Navigation Buttons": "دکمههای ناوبری",
|
||||
"Page {{number}}": "صفحه {{number}}",
|
||||
"highlight": "هایلایت",
|
||||
"underline": "زیرخط",
|
||||
"squiggly": "موجدار",
|
||||
"red": "قرمز",
|
||||
"violet": "بنفش",
|
||||
"blue": "آبی",
|
||||
"green": "سبز",
|
||||
"yellow": "زرد",
|
||||
"Select {{style}} style": "انتخاب سبک {{style}}",
|
||||
"Select {{color}} color": "انتخاب رنگ {{color}}",
|
||||
"Close Book": "بستن کتاب",
|
||||
"Speed Reading": "تندخوانی",
|
||||
"Close Speed Reading": "بستن تندخوانی",
|
||||
"Authors": "نویسندگان",
|
||||
"Books": "کتابها",
|
||||
"Groups": "گروهها",
|
||||
"Back to TTS Location": "بازگشت به مکان TTS",
|
||||
"Metadata": "فراداده",
|
||||
"Image viewer": "نمایشگر تصویر",
|
||||
"Previous Image": "تصویر قبلی",
|
||||
"Next Image": "تصویر بعدی",
|
||||
"Zoomed": "بزرگنمایی شده",
|
||||
"Zoom level": "سطح بزرگنمایی",
|
||||
"Table viewer": "نمایشگر جدول",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "اتصال به Readwise امکانپذیر نیست. لطفاً اتصال شبکه خود را بررسی کنید.",
|
||||
"Invalid Readwise access token": "توکن دسترسی Readwise نامعتبر است",
|
||||
"Disconnected from Readwise": "اتصال از Readwise قطع شد",
|
||||
"Never": "هرگز",
|
||||
"Readwise Settings": "تنظیمات Readwise",
|
||||
"Connected to Readwise": "به Readwise متصل شد",
|
||||
"Last synced: {{time}}": "آخرین همگامسازی: {{time}}",
|
||||
"Sync Enabled": "همگامسازی فعال شد",
|
||||
"Disconnect": "قطع اتصال",
|
||||
"Connect your Readwise account to sync highlights.": "برای همگامسازی هایلایتها، حساب Readwise خود را متصل کنید.",
|
||||
"Get your access token at": "توکن دسترسی خود را از اینجا دریافت کنید:",
|
||||
"Access Token": "توکن دسترسی",
|
||||
"Paste your Readwise access token": "توکن دسترسی Readwise خود را جایگذاری کنید",
|
||||
"Config": "پیکربندی",
|
||||
"Readwise Sync": "همگامسازی Readwise",
|
||||
"Push Highlights": "ارسال هایلایتها",
|
||||
"Highlights synced to Readwise": "هایلایتها با Readwise همگامسازی شدند",
|
||||
"Readwise sync failed: no internet connection": "همگامسازی Readwise ناموفق بود: اتصال اینترنت برقرار نیست",
|
||||
"Readwise sync failed: {{error}}": "همگامسازی Readwise ناموفق بود: {{error}}",
|
||||
"System Screen Brightness": "روشنایی صفحه نمایش سیستم",
|
||||
"Page:": "صفحه:",
|
||||
"Page: {{number}}": "صفحه: {{number}}",
|
||||
"Annotation page number": "شماره صفحه یادداشت"
|
||||
}
|
||||
|
||||
@@ -337,9 +337,9 @@
|
||||
"Fit": "Adapter",
|
||||
"Reset {{settings}}": "Réinitialiser {{settings}}",
|
||||
"Reset Settings": "Réinitialiser les paramètres",
|
||||
"{{count}} pages left in chapter_one": "{{count}} page restant dans le chapitre",
|
||||
"{{count}} pages left in chapter_many": "{{count}} pages restantes dans le chapitre",
|
||||
"{{count}} pages left in chapter_other": "{{count}} pages restantes dans le chapitre",
|
||||
"{{count}} pages left in chapter_one": "<0>{{count}}</0><1> page restant dans le chapitre</1>",
|
||||
"{{count}} pages left in chapter_many": "<0>{{count}}</0><1> pages restantes dans le chapitre</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> pages restantes dans le chapitre</1>",
|
||||
"Show Remaining Pages": "Voir restantes",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -603,11 +603,10 @@
|
||||
"Size": "Taille",
|
||||
"Cover": "Couverture",
|
||||
"Contain": "Contenir",
|
||||
"{{number}} pages left in chapter": "{{number}} pages restantes dans le chapitre",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> pages restantes dans le chapitre</1>",
|
||||
"Device": "Appareil",
|
||||
"E-Ink Mode": "Mode E-Ink",
|
||||
"Highlight Colors": "Couleurs de Surlignage",
|
||||
"Auto Screen Brightness": "Luminosité Automatique de l'Écran",
|
||||
"Pagination": "Pagination",
|
||||
"Disable Double Tap": "Désactiver le Double Tap",
|
||||
"Tap to Paginate": "Tapoter pour Paginater",
|
||||
@@ -842,7 +841,6 @@
|
||||
"Show Results": "Afficher les résultats",
|
||||
"Clear search": "Effacer la recherche",
|
||||
"Clear search history": "Effacer l'historique de recherche",
|
||||
"Quick action disabled": "Action rapide désactivée",
|
||||
"Tap to Toggle Footer": "Appuyez pour afficher/masquer le pied de page",
|
||||
"Exported successfully": "Exporté avec succès",
|
||||
"Book exported successfully.": "Livre exporté avec succès.",
|
||||
@@ -885,7 +883,6 @@
|
||||
"Array of annotations": "Liste d'annotations",
|
||||
"Highlighted text": "Texte surligné",
|
||||
"Annotation note": "Note d'annotation",
|
||||
"Update time": "Heure de mise à jour",
|
||||
"Date Format Tokens:": "Jetons de format de date :",
|
||||
"Year (4 digits)": "Année (4 chiffres)",
|
||||
"Month (01-12)": "Mois (01-12)",
|
||||
@@ -900,5 +897,179 @@
|
||||
"Select Voice": "Sélectionner la voix",
|
||||
"Toggle Sticky Bottom TTS Bar": "Basculer la barre TTS fixée",
|
||||
"Display what I'm reading on Discord": "Afficher ce que je lis sur Discord",
|
||||
"Show on Discord": "Afficher sur Discord"
|
||||
"Show on Discord": "Afficher sur Discord",
|
||||
"Instant {{action}}": "{{action}} instantané",
|
||||
"Instant {{action}} Disabled": "{{action}} instantané désactivé",
|
||||
"Annotation": "Annotation",
|
||||
"Reset Template": "Réinitialiser le modèle",
|
||||
"Annotation style": "Style d'annotation",
|
||||
"Annotation color": "Couleur d'annotation",
|
||||
"Annotation time": "Heure d'annotation",
|
||||
"AI": "IA",
|
||||
"Are you sure you want to re-index this book?": "Voulez-vous vraiment réindexer ce livre ?",
|
||||
"Enable AI in Settings": "Activer l'IA dans les paramètres",
|
||||
"Index This Book": "Indexer ce livre",
|
||||
"Enable AI search and chat for this book": "Activer la recherche IA et le chat pour ce livre",
|
||||
"Start Indexing": "Démarrer l'indexation",
|
||||
"Indexing book...": "Indexation du livre...",
|
||||
"Preparing...": "Préparation...",
|
||||
"Delete this conversation?": "Supprimer cette conversation ?",
|
||||
"No conversations yet": "Pas encore de conversations",
|
||||
"Start a new chat to ask questions about this book": "Démarrez une nouvelle conversation pour poser des questions sur ce livre",
|
||||
"Rename": "Renommer",
|
||||
"New Chat": "Nouvelle conversation",
|
||||
"Chat": "Chat",
|
||||
"Please enter a model ID": "Veuillez entrer un ID de modèle",
|
||||
"Model not available or invalid": "Modèle non disponible ou invalide",
|
||||
"Failed to validate model": "Échec de la validation du modèle",
|
||||
"Couldn't connect to Ollama. Is it running?": "Impossible de se connecter à Ollama. Est-il en cours d'exécution ?",
|
||||
"Invalid API key or connection failed": "Clé API invalide ou échec de connexion",
|
||||
"Connection failed": "Échec de connexion",
|
||||
"AI Assistant": "Assistant IA",
|
||||
"Enable AI Assistant": "Activer l'assistant IA",
|
||||
"Provider": "Fournisseur",
|
||||
"Ollama (Local)": "Ollama (Local)",
|
||||
"AI Gateway (Cloud)": "Passerelle IA (Cloud)",
|
||||
"Ollama Configuration": "Configuration Ollama",
|
||||
"Refresh Models": "Actualiser les modèles",
|
||||
"AI Model": "Modèle IA",
|
||||
"No models detected": "Aucun modèle détecté",
|
||||
"AI Gateway Configuration": "Configuration de la passerelle IA",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Choisissez parmi une sélection de modèles IA de haute qualité et économiques. Vous pouvez également utiliser votre propre modèle en sélectionnant \"Modèle personnalisé\" ci-dessous.",
|
||||
"API Key": "Clé API",
|
||||
"Get Key": "Obtenir la clé",
|
||||
"Model": "Modèle",
|
||||
"Custom Model...": "Modèle personnalisé...",
|
||||
"Custom Model ID": "ID du modèle personnalisé",
|
||||
"Validate": "Valider",
|
||||
"Model available": "Modèle disponible",
|
||||
"Connection": "Connexion",
|
||||
"Test Connection": "Tester la connexion",
|
||||
"Connected": "Connecté",
|
||||
"Custom Colors": "Couleurs personnalisées",
|
||||
"Color E-Ink Mode": "Mode E-Ink couleur",
|
||||
"Reading Ruler": "Règle de lecture",
|
||||
"Enable Reading Ruler": "Activer la règle de lecture",
|
||||
"Lines to Highlight": "Lignes à mettre en évidence",
|
||||
"Ruler Color": "Couleur de la règle",
|
||||
"Command Palette": "Palette de commandes",
|
||||
"Search settings and actions...": "Rechercher des paramètres et des actions...",
|
||||
"No results found for": "Aucun résultat trouvé pour",
|
||||
"Type to search settings and actions": "Saisissez pour rechercher des paramètres et des actions",
|
||||
"Recent": "Récent",
|
||||
"navigate": "naviguer",
|
||||
"select": "sélectionner",
|
||||
"close": "fermer",
|
||||
"Search Settings": "Rechercher dans les paramètres",
|
||||
"Page Margins": "Marges de page",
|
||||
"AI Provider": "Fournisseur d'IA",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Modèle Ollama",
|
||||
"AI Gateway Model": "Modèle AI Gateway",
|
||||
"Actions": "Actions",
|
||||
"Navigation": "Navigation",
|
||||
"Set status for {{count}} book(s)_one": "Définir le statut pour {{count}} livre",
|
||||
"Set status for {{count}} book(s)_other": "Définir le statut pour {{count}} livres",
|
||||
"Mark as Unread": "Marquer comme non lu",
|
||||
"Mark as Finished": "Marquer comme terminé",
|
||||
"Finished": "Terminé",
|
||||
"Unread": "Non lu",
|
||||
"Clear Status": "Effacer le statut",
|
||||
"Status": "Statut",
|
||||
"Set status for {{count}} book(s)_many": "Définir le statut pour {{count}} livres",
|
||||
"Loading": "Chargement...",
|
||||
"Exit Paragraph Mode": "Quitter le mode paragraphe",
|
||||
"Paragraph Mode": "Mode paragraphe",
|
||||
"Embedding Model": "Modèle d'incorporation",
|
||||
"{{count}} book(s) synced_one": "{{count}} livre synchronisé",
|
||||
"{{count}} book(s) synced_many": "{{count}} livres synchronisés",
|
||||
"{{count}} book(s) synced_other": "{{count}} livres synchronisés",
|
||||
"Unable to start RSVP": "Impossible de démarrer RSVP",
|
||||
"RSVP not supported for PDF": "RSVP non pris en charge pour PDF",
|
||||
"Select Chapter": "Sélectionner le chapitre",
|
||||
"Context": "Contexte",
|
||||
"Ready": "Prêt",
|
||||
"Chapter Progress": "Progression du chapitre",
|
||||
"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",
|
||||
"Choose where to start reading": "Choisir où commencer la lecture",
|
||||
"From Chapter Start": "Depuis le début du chapitre",
|
||||
"Start reading from the beginning of the chapter": "Commencer la lecture au début du chapitre",
|
||||
"Resume": "Reprendre",
|
||||
"Continue from where you left off": "Continuer là où vous vous êtes arrêté",
|
||||
"From Current Page": "Depuis la page actuelle",
|
||||
"Start from where you are currently reading": "Commencer là où vous lisez actuellement",
|
||||
"From Selection": "Depuis la sélection",
|
||||
"Speed Reading Mode": "Mode lecture rapide",
|
||||
"Scroll left": "Défiler vers la gauche",
|
||||
"Scroll right": "Défiler vers la droite",
|
||||
"Library Sync Progress": "Progression de la synchronisation de la bibliothèque",
|
||||
"Back to library": "Retour à la bibliothèque",
|
||||
"Group by...": "Grouper par...",
|
||||
"Export as Plain Text": "Exporter en texte brut",
|
||||
"Export as Markdown": "Exporter en Markdown",
|
||||
"Show Page Navigation Buttons": "Boutons de navigation",
|
||||
"Page {{number}}": "Page {{number}}",
|
||||
"highlight": "surlignage",
|
||||
"underline": "soulignage",
|
||||
"squiggly": "ondulé",
|
||||
"red": "rouge",
|
||||
"violet": "violet",
|
||||
"blue": "bleu",
|
||||
"green": "vert",
|
||||
"yellow": "jaune",
|
||||
"Select {{style}} style": "Sélectionner le style {{style}}",
|
||||
"Select {{color}} color": "Sélectionner la couleur {{color}}",
|
||||
"Close Book": "Fermer le livre",
|
||||
"Speed Reading": "Lecture rapide",
|
||||
"Close Speed Reading": "Fermer la lecture rapide",
|
||||
"Authors": "Auteurs",
|
||||
"Books": "Livres",
|
||||
"Groups": "Groupes",
|
||||
"Back to TTS Location": "Retour à l'emplacement TTS",
|
||||
"Metadata": "Métadonnées",
|
||||
"Image viewer": "Visionneuse d'images",
|
||||
"Previous Image": "Image précédente",
|
||||
"Next Image": "Image suivante",
|
||||
"Zoomed": "Zoomé",
|
||||
"Zoom level": "Niveau de zoom",
|
||||
"Table viewer": "Visionneuse de tableaux",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Impossible de se connecter à Readwise. Veuillez vérifier votre connexion réseau.",
|
||||
"Invalid Readwise access token": "Jeton d'accès Readwise invalide",
|
||||
"Disconnected from Readwise": "Déconnecté de Readwise",
|
||||
"Never": "Jamais",
|
||||
"Readwise Settings": "Paramètres Readwise",
|
||||
"Connected to Readwise": "Connecté à Readwise",
|
||||
"Last synced: {{time}}": "Dernière synchronisation : {{time}}",
|
||||
"Sync Enabled": "Synchronisation activée",
|
||||
"Disconnect": "Déconnecter",
|
||||
"Connect your Readwise account to sync highlights.": "Connectez votre compte Readwise pour synchroniser les surlignages.",
|
||||
"Get your access token at": "Obtenez votre jeton d'accès sur",
|
||||
"Access Token": "Jeton d'accès",
|
||||
"Paste your Readwise access token": "Collez votre jeton d'accès Readwise",
|
||||
"Config": "Configuration",
|
||||
"Readwise Sync": "Synchronisation Readwise",
|
||||
"Push Highlights": "Pousser les surlignages",
|
||||
"Highlights synced to Readwise": "Surlignages synchronisés avec Readwise",
|
||||
"Readwise sync failed: no internet connection": "Échec de la synchronisation Readwise : pas de connexion internet",
|
||||
"Readwise sync failed: {{error}}": "Échec de la synchronisation Readwise : {{error}}",
|
||||
"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"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -335,8 +335,8 @@
|
||||
"Fit": "फिट",
|
||||
"Reset {{settings}}": "रीसेट {{settings}}",
|
||||
"Reset Settings": "सेटिंग्स रीसेट करें",
|
||||
"{{count}} pages left in chapter_one": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
|
||||
"{{count}} pages left in chapter_other": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
|
||||
"{{count}} pages left in chapter_one": "<1>इस अध्याय में </1><0>{{count}}</0><1> पृष्ठ शेष हैं</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>इस अध्याय में </1><0>{{count}}</0><1> पृष्ठ शेष हैं</1>",
|
||||
"Show Remaining Pages": "शेष पृष्ठ दिखाएँ",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "आकार",
|
||||
"Cover": "कवर",
|
||||
"Contain": "समाहित करें",
|
||||
"{{number}} pages left in chapter": "इस अध्याय में {{number}} पृष्ठ शेष हैं",
|
||||
"{{number}} pages left in chapter": "<1>इस अध्याय में </1><0>{{number}}</0><1> पृष्ठ शेष हैं</1>",
|
||||
"Device": "डिवाइस",
|
||||
"E-Ink Mode": "ई-इंक मोड",
|
||||
"Highlight Colors": "हाइलाइट रंग",
|
||||
"Auto Screen Brightness": "स्वचालित स्क्रीन ब्राइटनेस",
|
||||
"Pagination": "पृष्ठांकन",
|
||||
"Disable Double Tap": "डबल टैप अक्षम करें",
|
||||
"Tap to Paginate": "पृष्ठांकन के लिए टैप करें",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "परिणाम दिखाएं",
|
||||
"Clear search": "खोज साफ़ करें",
|
||||
"Clear search history": "खोज इतिहास साफ़ करें",
|
||||
"Quick action disabled": "त्वरित क्रिया अक्षम",
|
||||
"Tap to Toggle Footer": "फुटर टॉगल करने के लिए टैप करें",
|
||||
"Exported successfully": "सफलतापूर्वक निर्यात किया गया",
|
||||
"Book exported successfully.": "पुस्तक सफलतापूर्वक निर्यात की गई।",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "एनोटेशन की सूची",
|
||||
"Highlighted text": "हाइलाइट किया गया पाठ",
|
||||
"Annotation note": "एनोटेशन नोट",
|
||||
"Update time": "अपडेट समय",
|
||||
"Date Format Tokens:": "तिथि प्रारूप टोकन:",
|
||||
"Year (4 digits)": "वर्ष (4 अंक)",
|
||||
"Month (01-12)": "माह (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "आवाज़ चुनें",
|
||||
"Toggle Sticky Bottom TTS Bar": "स्थिर TTS बार टॉगल करें",
|
||||
"Display what I'm reading on Discord": "Discord पर पढ़ रही किताब दिखाएं",
|
||||
"Show on Discord": "Discord पर दिखाएं"
|
||||
"Show on Discord": "Discord पर दिखाएं",
|
||||
"Instant {{action}}": "तत्काल {{action}}",
|
||||
"Instant {{action}} Disabled": "तत्काल {{action}} अक्षम",
|
||||
"Annotation": "टिप्पणी",
|
||||
"Reset Template": "टेम्पलेट रीसेट करें",
|
||||
"Annotation style": "एनोटेशन शैली",
|
||||
"Annotation color": "एनोटेशन रंग",
|
||||
"Annotation time": "एनोटेशन समय",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "क्या आप वाकई इस पुस्तक को फिर से इंडेक्स करना चाहते हैं?",
|
||||
"Enable AI in Settings": "सेटिंग्स में AI सक्षम करें",
|
||||
"Index This Book": "इस पुस्तक को इंडेक्स करें",
|
||||
"Enable AI search and chat for this book": "इस पुस्तक के लिए AI खोज और चैट सक्षम करें",
|
||||
"Start Indexing": "इंडेक्सिंग शुरू करें",
|
||||
"Indexing book...": "पुस्तक इंडेक्स हो रही है...",
|
||||
"Preparing...": "तैयारी हो रही है...",
|
||||
"Delete this conversation?": "इस वार्तालाप को हटाएं?",
|
||||
"No conversations yet": "अभी तक कोई वार्तालाप नहीं",
|
||||
"Start a new chat to ask questions about this book": "इस पुस्तक के बारे में प्रश्न पूछने के लिए नई चैट शुरू करें",
|
||||
"Rename": "नाम बदलें",
|
||||
"New Chat": "नई चैट",
|
||||
"Chat": "चैट",
|
||||
"Please enter a model ID": "कृपया मॉडल ID दर्ज करें",
|
||||
"Model not available or invalid": "मॉडल उपलब्ध नहीं या अमान्य है",
|
||||
"Failed to validate model": "मॉडल सत्यापित करने में विफल",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama से कनेक्ट नहीं हो सका। क्या यह चल रहा है?",
|
||||
"Invalid API key or connection failed": "अमान्य API कुंजी या कनेक्शन विफल",
|
||||
"Connection failed": "कनेक्शन विफल",
|
||||
"AI Assistant": "AI सहायक",
|
||||
"Enable AI Assistant": "AI सहायक सक्षम करें",
|
||||
"Provider": "प्रदाता",
|
||||
"Ollama (Local)": "Ollama (स्थानीय)",
|
||||
"AI Gateway (Cloud)": "AI गेटवे (क्लाउड)",
|
||||
"Ollama Configuration": "Ollama कॉन्फ़िगरेशन",
|
||||
"Refresh Models": "मॉडल रीफ्रेश करें",
|
||||
"AI Model": "AI मॉडल",
|
||||
"No models detected": "कोई मॉडल नहीं मिला",
|
||||
"AI Gateway Configuration": "AI गेटवे कॉन्फ़िगरेशन",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "उच्च-गुणवत्ता, किफायती AI मॉडल के चयन में से चुनें। आप नीचे \"कस्टम मॉडल\" का चयन करके अपना मॉडल भी उपयोग कर सकते हैं।",
|
||||
"API Key": "API कुंजी",
|
||||
"Get Key": "कुंजी प्राप्त करें",
|
||||
"Model": "मॉडल",
|
||||
"Custom Model...": "कस्टम मॉडल...",
|
||||
"Custom Model ID": "कस्टम मॉडल ID",
|
||||
"Validate": "सत्यापित करें",
|
||||
"Model available": "मॉडल उपलब्ध",
|
||||
"Connection": "कनेक्शन",
|
||||
"Test Connection": "कनेक्शन परीक्षण",
|
||||
"Connected": "कनेक्टेड",
|
||||
"Custom Colors": "कस्टम रंग",
|
||||
"Color E-Ink Mode": "कलर ई-इंक मोड",
|
||||
"Reading Ruler": "पठन रूलर",
|
||||
"Enable Reading Ruler": "पठन रूलर सक्षम करें",
|
||||
"Lines to Highlight": "हाइलाइट करने के लिए पंक्तियाँ",
|
||||
"Ruler Color": "रूलर का रंग",
|
||||
"Command Palette": "कमांड पैलेट",
|
||||
"Search settings and actions...": "सेटिंग और क्रियाएं खोजें...",
|
||||
"No results found for": "के लिए कोई परिणाम नहीं मिला",
|
||||
"Type to search settings and actions": "सेटिंग और क्रियाएं खोजने के लिए टाइप करें",
|
||||
"Recent": "हालिया",
|
||||
"navigate": "नेविगेट",
|
||||
"select": "चुनें",
|
||||
"close": "बंद करें",
|
||||
"Search Settings": "सेटिंग खोजें",
|
||||
"Page Margins": "पेज मार्जिन",
|
||||
"AI Provider": "AI प्रदाता",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama मॉडल",
|
||||
"AI Gateway Model": "AI गेटवे मॉडल",
|
||||
"Actions": "क्रियाएं",
|
||||
"Navigation": "नेविगेशन",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} किताब की स्थिति सेट करें",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} किताबों की स्थिति सेट करें",
|
||||
"Mark as Unread": "अपठित के रूप में चिह्नित करें",
|
||||
"Mark as Finished": "समाप्त के रूप में चिह्नित करें",
|
||||
"Finished": "समाप्त",
|
||||
"Unread": "अपठित",
|
||||
"Clear Status": "स्थिति साफ़ करें",
|
||||
"Status": "स्थिति",
|
||||
"Loading": "लोड हो रहा है...",
|
||||
"Exit Paragraph Mode": "पैराग्राफ मोड से बाहर निकलें",
|
||||
"Paragraph Mode": "पैराग्राफ मोड",
|
||||
"Embedding Model": "एंबेडिंग मॉडल",
|
||||
"{{count}} book(s) synced_one": "{{count}} पुस्तक सिंक की गई",
|
||||
"{{count}} book(s) synced_other": "{{count}} पुस्तकें सिंक की गईं",
|
||||
"Unable to start RSVP": "RSVP शुरू करने में असमर्थ",
|
||||
"RSVP not supported for PDF": "PDF के लिए RSVP समर्थित नहीं है",
|
||||
"Select Chapter": "अध्याय चुनें",
|
||||
"Context": "संदर्भ",
|
||||
"Ready": "तैयार",
|
||||
"Chapter Progress": "अध्याय प्रगति",
|
||||
"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 पठन शुरू करें",
|
||||
"Choose where to start reading": "चुनें कि कहाँ से पढ़ना शुरू करना है",
|
||||
"From Chapter Start": "अध्याय के आरंभ से",
|
||||
"Start reading from the beginning of the chapter": "अध्याय की शुरुआत से पढ़ना शुरू करें",
|
||||
"Resume": "फिर से शुरू करें",
|
||||
"Continue from where you left off": "वहीं से जारी रखें जहाँ आपने छोड़ा था",
|
||||
"From Current Page": "वर्तमान पृष्ठ से",
|
||||
"Start from where you are currently reading": "जहाँ आप अभी पढ़ रहे हैं वहीं से शुरू करें",
|
||||
"From Selection": "चयन से",
|
||||
"Speed Reading Mode": "त्वरित पठन मोड",
|
||||
"Scroll left": "बाएं स्क्रॉल करें",
|
||||
"Scroll right": "दाएं स्क्रॉल करें",
|
||||
"Library Sync Progress": "पुस्तकालय सिंक प्रगति",
|
||||
"Back to library": "लाइब्रेरी पर वापस जाएं",
|
||||
"Group by...": "इसके द्वारा समूहबद्ध करें...",
|
||||
"Export as Plain Text": "प्लेन टेक्स्ट के रूप में एक्सपोर्ट करें",
|
||||
"Export as Markdown": "Markdown के रूप में एक्सपोर्ट करें",
|
||||
"Show Page Navigation Buttons": "नेविगेशन बटन",
|
||||
"Page {{number}}": "पृष्ठ {{number}}",
|
||||
"highlight": "हाइलाइट",
|
||||
"underline": "अंडरलाइन",
|
||||
"squiggly": "टेढ़ी-मेढ़ी रेखा",
|
||||
"red": "लाल",
|
||||
"violet": "बैंगनी",
|
||||
"blue": "नीला",
|
||||
"green": "हरा",
|
||||
"yellow": "पीला",
|
||||
"Select {{style}} style": "{{style}} शैली चुनें",
|
||||
"Select {{color}} color": "{{color}} रंग चुनें",
|
||||
"Close Book": "किताब बंद करें",
|
||||
"Speed Reading": "तीव्र पठन",
|
||||
"Close Speed Reading": "तीव्र पठन बंद करें",
|
||||
"Authors": "लेखक",
|
||||
"Books": "पुस्तकें",
|
||||
"Groups": "समूह",
|
||||
"Back to TTS Location": "टीटीएस स्थान पर वापस जाएं",
|
||||
"Metadata": "मेटाडाटा",
|
||||
"Image viewer": "छवि दर्शक",
|
||||
"Previous Image": "पिछली छवि",
|
||||
"Next Image": "अगली छवि",
|
||||
"Zoomed": "ज़ूम किया गया",
|
||||
"Zoom level": "ज़ूम स्तर",
|
||||
"Table viewer": "तालिका दर्शक",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise से कनेक्ट करने में असमर्थ। कृपया अपना नेटवर्क कनेक्शन जांचें।",
|
||||
"Invalid Readwise access token": "अमान्य Readwise एक्सेस टोकन",
|
||||
"Disconnected from Readwise": "Readwise से डिस्कनेक्ट हो गया",
|
||||
"Never": "कभी नहीं",
|
||||
"Readwise Settings": "Readwise सेटिंग्स",
|
||||
"Connected to Readwise": "Readwise से जुड़ा हुआ",
|
||||
"Last synced: {{time}}": "पिछला सिंक: {{time}}",
|
||||
"Sync Enabled": "सिंक सक्षम",
|
||||
"Disconnect": "डिस्कनेक्ट करें",
|
||||
"Connect your Readwise account to sync highlights.": "हाइलाइट्स सिंक करने के लिए अपना Readwise खाता कनेक्ट करें।",
|
||||
"Get your access token at": "अपना एक्सेस टोकन यहाँ प्राप्त करें",
|
||||
"Access Token": "एक्सेस टोकन",
|
||||
"Paste your Readwise access token": "अपना Readwise एक्सेस टोकन पेस्ट करें",
|
||||
"Config": "कॉन्फिग",
|
||||
"Readwise Sync": "Readwise सिंक",
|
||||
"Push Highlights": "हाइलाइट्स भेजें",
|
||||
"Highlights synced to Readwise": "हाइलाइट्स Readwise में सिंक हो गए",
|
||||
"Readwise sync failed: no internet connection": "Readwise सिंक विफल रहा: कोई इंटरनेट कनेक्शन नहीं",
|
||||
"Readwise sync failed: {{error}}": "Readwise सिंक विफल रहा: {{error}}",
|
||||
"System Screen Brightness": "सिस्टम स्क्रीन चमक",
|
||||
"Page:": "पृष्ठ:",
|
||||
"Page: {{number}}": "पृष्ठ: {{number}}",
|
||||
"Annotation page number": "व्याख्या पृष्ठ संख्या"
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
"Fit": "Pas",
|
||||
"Reset {{settings}}": "Atur Ulang {{settings}}",
|
||||
"Reset Settings": "Atur Ulang Pengaturan",
|
||||
"{{count}} pages left in chapter_other": "{{count}} halaman tersisa di bab ini",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> halaman tersisa di bab ini</1>",
|
||||
"Show Remaining Pages": "Tampilkan halaman tersisa",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "Ukuran",
|
||||
"Cover": "Sampul",
|
||||
"Contain": "Mengandung",
|
||||
"{{number}} pages left in chapter": "{{number}} halaman tersisa di bab",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> halaman tersisa di bab</1>",
|
||||
"Device": "Perangkat",
|
||||
"E-Ink Mode": "Mode E-Ink",
|
||||
"Highlight Colors": "Warna Sorotan",
|
||||
"Auto Screen Brightness": "Auto Kecerahan Layar",
|
||||
"Pagination": "Paginasi",
|
||||
"Disable Double Tap": "Nonaktifkan Ketuk Ganda",
|
||||
"Tap to Paginate": "Ketuk untuk Paginasi",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "Tampilkan Hasil",
|
||||
"Clear search": "Hapus pencarian",
|
||||
"Clear search history": "Hapus riwayat pencarian",
|
||||
"Quick action disabled": "Tindakan cepat dinonaktifkan",
|
||||
"Tap to Toggle Footer": "Ketuk untuk beralih footer",
|
||||
"Exported successfully": "Berhasil diekspor",
|
||||
"Book exported successfully.": "Buku berhasil diekspor.",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "Daftar anotasi",
|
||||
"Highlighted text": "Teks yang disorot",
|
||||
"Annotation note": "Catatan anotasi",
|
||||
"Update time": "Waktu pembaruan",
|
||||
"Date Format Tokens:": "Token Format Tanggal:",
|
||||
"Year (4 digits)": "Tahun (4 digit)",
|
||||
"Month (01-12)": "Bulan (01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "Pilih suara",
|
||||
"Toggle Sticky Bottom TTS Bar": "Alihkan bilah TTS tetap",
|
||||
"Display what I'm reading on Discord": "Tampilkan buku yang sedang dibaca di Discord",
|
||||
"Show on Discord": "Tampilkan di Discord"
|
||||
"Show on Discord": "Tampilkan di Discord",
|
||||
"Instant {{action}}": "{{action}} Instan",
|
||||
"Instant {{action}} Disabled": "{{action}} Instan Dinonaktifkan",
|
||||
"Annotation": "Anotasi",
|
||||
"Reset Template": "Atur Ulang Template",
|
||||
"Annotation style": "Gaya anotasi",
|
||||
"Annotation color": "Warna anotasi",
|
||||
"Annotation time": "Waktu anotasi",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Apakah Anda yakin ingin mengindeks ulang buku ini?",
|
||||
"Enable AI in Settings": "Aktifkan AI di Pengaturan",
|
||||
"Index This Book": "Indeks Buku Ini",
|
||||
"Enable AI search and chat for this book": "Aktifkan pencarian AI dan obrolan untuk buku ini",
|
||||
"Start Indexing": "Mulai Mengindeks",
|
||||
"Indexing book...": "Mengindeks buku...",
|
||||
"Preparing...": "Mempersiapkan...",
|
||||
"Delete this conversation?": "Hapus percakapan ini?",
|
||||
"No conversations yet": "Belum ada percakapan",
|
||||
"Start a new chat to ask questions about this book": "Mulai obrolan baru untuk mengajukan pertanyaan tentang buku ini",
|
||||
"Rename": "Ubah nama",
|
||||
"New Chat": "Obrolan Baru",
|
||||
"Chat": "Obrolan",
|
||||
"Please enter a model ID": "Silakan masukkan ID model",
|
||||
"Model not available or invalid": "Model tidak tersedia atau tidak valid",
|
||||
"Failed to validate model": "Gagal memvalidasi model",
|
||||
"Couldn't connect to Ollama. Is it running?": "Tidak dapat terhubung ke Ollama. Apakah sedang berjalan?",
|
||||
"Invalid API key or connection failed": "Kunci API tidak valid atau koneksi gagal",
|
||||
"Connection failed": "Koneksi gagal",
|
||||
"AI Assistant": "Asisten AI",
|
||||
"Enable AI Assistant": "Aktifkan Asisten AI",
|
||||
"Provider": "Penyedia",
|
||||
"Ollama (Local)": "Ollama (Lokal)",
|
||||
"AI Gateway (Cloud)": "Gateway AI (Cloud)",
|
||||
"Ollama Configuration": "Konfigurasi Ollama",
|
||||
"Refresh Models": "Segarkan Model",
|
||||
"AI Model": "Model AI",
|
||||
"No models detected": "Tidak ada model terdeteksi",
|
||||
"AI Gateway Configuration": "Konfigurasi Gateway AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Pilih dari pilihan model AI berkualitas tinggi dan ekonomis. Anda juga dapat menggunakan model Anda sendiri dengan memilih \"Model Kustom\" di bawah.",
|
||||
"API Key": "Kunci API",
|
||||
"Get Key": "Dapatkan Kunci",
|
||||
"Model": "Model",
|
||||
"Custom Model...": "Model Kustom...",
|
||||
"Custom Model ID": "ID Model Kustom",
|
||||
"Validate": "Validasi",
|
||||
"Model available": "Model tersedia",
|
||||
"Connection": "Koneksi",
|
||||
"Test Connection": "Uji Koneksi",
|
||||
"Connected": "Terhubung",
|
||||
"Custom Colors": "Warna Kustom",
|
||||
"Color E-Ink Mode": "Mode E-Ink Berwarna",
|
||||
"Reading Ruler": "Penggaris Membaca",
|
||||
"Enable Reading Ruler": "Aktifkan Penggaris Membaca",
|
||||
"Lines to Highlight": "Baris untuk Disorot",
|
||||
"Ruler Color": "Warna Penggaris",
|
||||
"Command Palette": "Palet Perintah",
|
||||
"Search settings and actions...": "Cari pengaturan dan tindakan...",
|
||||
"No results found for": "Tidak ada hasil ditemukan untuk",
|
||||
"Type to search settings and actions": "Ketik untuk mencari pengaturan dan tindakan",
|
||||
"Recent": "Terbaru",
|
||||
"navigate": "navigasi",
|
||||
"select": "pilih",
|
||||
"close": "tutup",
|
||||
"Search Settings": "Cari Pengaturan",
|
||||
"Page Margins": "Margin Halaman",
|
||||
"AI Provider": "Penyedia AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi",
|
||||
"Set status for {{count}} book(s)_other": "Atur status untuk {{count}} buku",
|
||||
"Mark as Unread": "Tandai sebagai Belum Dibaca",
|
||||
"Mark as Finished": "Tandai sebagai Selesai",
|
||||
"Finished": "Selesai",
|
||||
"Unread": "Belum Dibaca",
|
||||
"Clear Status": "Hapus Status",
|
||||
"Status": "Status",
|
||||
"Loading": "Memuat...",
|
||||
"Exit Paragraph Mode": "Keluar dari Mode Paragraf",
|
||||
"Paragraph Mode": "Mode Paragraf",
|
||||
"Embedding Model": "Model Penyematan",
|
||||
"{{count}} book(s) synced_other": "{{count}} buku telah disinkronkan",
|
||||
"Unable to start RSVP": "Tidak dapat memulai RSVP",
|
||||
"RSVP not supported for PDF": "RSVP tidak didukung untuk PDF",
|
||||
"Select Chapter": "Pilih Bab",
|
||||
"Context": "Konteks",
|
||||
"Ready": "Siap",
|
||||
"Chapter Progress": "Kemajuan Bab",
|
||||
"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",
|
||||
"Choose where to start reading": "Pilih tempat untuk mulai membaca",
|
||||
"From Chapter Start": "Dari Awal Bab",
|
||||
"Start reading from the beginning of the chapter": "Mulai membaca dari awal bab",
|
||||
"Resume": "Lanjutkan",
|
||||
"Continue from where you left off": "Lanjutkan dari tempat Anda berhenti",
|
||||
"From Current Page": "Dari Halaman Saat Ini",
|
||||
"Start from where you are currently reading": "Mulai dari tempat Anda sedang membaca",
|
||||
"From Selection": "Dari Pilihan",
|
||||
"Speed Reading Mode": "Mode Baca Cepat",
|
||||
"Scroll left": "Gulir ke kiri",
|
||||
"Scroll right": "Gulir ke kanan",
|
||||
"Library Sync Progress": "Kemajuan Sinkronisasi Pustaka",
|
||||
"Back to library": "Kembali ke perpustakaan",
|
||||
"Group by...": "Kelompokkan menurut...",
|
||||
"Export as Plain Text": "Ekspor sebagai Teks Biasa",
|
||||
"Export as Markdown": "Ekspor sebagai Markdown",
|
||||
"Show Page Navigation Buttons": "Tombol navigasi",
|
||||
"Page {{number}}": "Halaman {{number}}",
|
||||
"highlight": "sorotan",
|
||||
"underline": "garis bawah",
|
||||
"squiggly": "bergelombang",
|
||||
"red": "merah",
|
||||
"violet": "ungu",
|
||||
"blue": "biru",
|
||||
"green": "hijau",
|
||||
"yellow": "kuning",
|
||||
"Select {{style}} style": "Pilih gaya {{style}}",
|
||||
"Select {{color}} color": "Pilih warna {{color}}",
|
||||
"Close Book": "Tutup Buku",
|
||||
"Speed Reading": "Membaca Cepat",
|
||||
"Close Speed Reading": "Tutup Membaca Cepat",
|
||||
"Authors": "Penulis",
|
||||
"Books": "Buku",
|
||||
"Groups": "Grup",
|
||||
"Back to TTS Location": "Kembali ke Lokasi TTS",
|
||||
"Metadata": "Metadata",
|
||||
"Image viewer": "Penampil gambar",
|
||||
"Previous Image": "Gambar Sebelumnya",
|
||||
"Next Image": "Gambar Berikutnya",
|
||||
"Zoomed": "Diperbesar",
|
||||
"Zoom level": "Tingkat zoom",
|
||||
"Table viewer": "Penampil tabel",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Tidak dapat terhubung ke Readwise. Silakan periksa koneksi jaringan Anda.",
|
||||
"Invalid Readwise access token": "Token akses Readwise tidak valid",
|
||||
"Disconnected from Readwise": "Terputus dari Readwise",
|
||||
"Never": "Tidak pernah",
|
||||
"Readwise Settings": "Pengaturan Readwise",
|
||||
"Connected to Readwise": "Terhubung ke Readwise",
|
||||
"Last synced: {{time}}": "Terakhir disinkronkan: {{time}}",
|
||||
"Sync Enabled": "Sinkronisasi Diaktifkan",
|
||||
"Disconnect": "Putuskan sambungan",
|
||||
"Connect your Readwise account to sync highlights.": "Hubungkan akun Readwise Anda untuk menyinkronkan sorotan.",
|
||||
"Get your access token at": "Dapatkan token akses Anda di",
|
||||
"Access Token": "Token Akses",
|
||||
"Paste your Readwise access token": "Tempelkan token akses Readwise Anda",
|
||||
"Config": "Konfigurasi",
|
||||
"Readwise Sync": "Sinkronisasi Readwise",
|
||||
"Push Highlights": "Kirim Sorotan",
|
||||
"Highlights synced to Readwise": "Sorotan disinkronkan ke Readwise",
|
||||
"Readwise sync failed: no internet connection": "Sinkronisasi Readwise gagal: tidak ada koneksi internet",
|
||||
"Readwise sync failed: {{error}}": "Sinkronisasi Readwise gagal: {{error}}",
|
||||
"System Screen Brightness": "Kecerahan Layar Sistem",
|
||||
"Page:": "Halaman:",
|
||||
"Page: {{number}}": "Halaman: {{number}}",
|
||||
"Annotation page number": "Nomor halaman anotasi"
|
||||
}
|
||||
|
||||
@@ -338,9 +338,9 @@
|
||||
"Fit": "Adatta",
|
||||
"Reset {{settings}}": "Reimposta {{settings}}",
|
||||
"Reset Settings": "Reimposta impostazioni",
|
||||
"{{count}} pages left in chapter_one": "{{count}} pagina rimasta nel capitolo",
|
||||
"{{count}} pages left in chapter_many": "{{count}} pagine rimaste nel capitolo",
|
||||
"{{count}} pages left in chapter_other": "{{count}} pagine rimaste nel capitolo",
|
||||
"{{count}} pages left in chapter_one": "<0>{{count}}</0><1> pagina rimasta nel capitolo</1>",
|
||||
"{{count}} pages left in chapter_many": "<0>{{count}}</0><1> pagine rimaste nel capitolo</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> pagine rimaste nel capitolo</1>",
|
||||
"Show Remaining Pages": "Mostra pagine rimanenti",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -603,11 +603,10 @@
|
||||
"Size": "Dimensione",
|
||||
"Cover": "Copertura",
|
||||
"Contain": "Contenere",
|
||||
"{{number}} pages left in chapter": "{{number}} pagine rimaste nel capitolo",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> pagine rimaste nel capitolo</1>",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modalità E-Ink",
|
||||
"Highlight Colors": "Colori Evidenziazione",
|
||||
"Auto Screen Brightness": "Luminosità schermo automatica",
|
||||
"Pagination": "Impaginazione",
|
||||
"Disable Double Tap": "Disabilita Doppio Tap",
|
||||
"Tap to Paginate": "Tocca per Impaginare",
|
||||
@@ -842,7 +841,6 @@
|
||||
"Show Results": "Mostra risultati",
|
||||
"Clear search": "Cancella ricerca",
|
||||
"Clear search history": "Cancella cronologia ricerche",
|
||||
"Quick action disabled": "Azione rapida disattivata",
|
||||
"Tap to Toggle Footer": "Tocca per mostrare/nascondere il piè di pagina",
|
||||
"Exported successfully": "Esportato con successo",
|
||||
"Book exported successfully.": "Libro esportato con successo.",
|
||||
@@ -885,7 +883,6 @@
|
||||
"Array of annotations": "Elenco annotazioni",
|
||||
"Highlighted text": "Testo evidenziato",
|
||||
"Annotation note": "Nota annotazione",
|
||||
"Update time": "Ora aggiornamento",
|
||||
"Date Format Tokens:": "Token formato data:",
|
||||
"Year (4 digits)": "Anno (4 cifre)",
|
||||
"Month (01-12)": "Mese (01-12)",
|
||||
@@ -900,5 +897,179 @@
|
||||
"Select Voice": "Seleziona voce",
|
||||
"Toggle Sticky Bottom TTS Bar": "Attiva/disattiva barra TTS fissa",
|
||||
"Display what I'm reading on Discord": "Mostra cosa sto leggendo su Discord",
|
||||
"Show on Discord": "Mostra su Discord"
|
||||
"Show on Discord": "Mostra su Discord",
|
||||
"Instant {{action}}": "{{action}} istantaneo",
|
||||
"Instant {{action}} Disabled": "{{action}} istantaneo disattivato",
|
||||
"Annotation": "Annotazione",
|
||||
"Reset Template": "Reimposta modello",
|
||||
"Annotation style": "Stile annotazione",
|
||||
"Annotation color": "Colore annotazione",
|
||||
"Annotation time": "Ora annotazione",
|
||||
"AI": "IA",
|
||||
"Are you sure you want to re-index this book?": "Sei sicuro di voler reindicizzare questo libro?",
|
||||
"Enable AI in Settings": "Abilita IA nelle impostazioni",
|
||||
"Index This Book": "Indicizza questo libro",
|
||||
"Enable AI search and chat for this book": "Abilita ricerca IA e chat per questo libro",
|
||||
"Start Indexing": "Avvia indicizzazione",
|
||||
"Indexing book...": "Indicizzazione libro...",
|
||||
"Preparing...": "Preparazione...",
|
||||
"Delete this conversation?": "Eliminare questa conversazione?",
|
||||
"No conversations yet": "Ancora nessuna conversazione",
|
||||
"Start a new chat to ask questions about this book": "Avvia una nuova chat per fare domande su questo libro",
|
||||
"Rename": "Rinomina",
|
||||
"New Chat": "Nuova chat",
|
||||
"Chat": "Chat",
|
||||
"Please enter a model ID": "Inserisci un ID modello",
|
||||
"Model not available or invalid": "Modello non disponibile o non valido",
|
||||
"Failed to validate model": "Validazione modello fallita",
|
||||
"Couldn't connect to Ollama. Is it running?": "Impossibile connettersi a Ollama. È in esecuzione?",
|
||||
"Invalid API key or connection failed": "Chiave API non valida o connessione fallita",
|
||||
"Connection failed": "Connessione fallita",
|
||||
"AI Assistant": "Assistente IA",
|
||||
"Enable AI Assistant": "Abilita assistente IA",
|
||||
"Provider": "Fornitore",
|
||||
"Ollama (Local)": "Ollama (Locale)",
|
||||
"AI Gateway (Cloud)": "Gateway IA (Cloud)",
|
||||
"Ollama Configuration": "Configurazione Ollama",
|
||||
"Refresh Models": "Aggiorna modelli",
|
||||
"AI Model": "Modello IA",
|
||||
"No models detected": "Nessun modello rilevato",
|
||||
"AI Gateway Configuration": "Configurazione gateway IA",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Scegli tra una selezione di modelli IA di alta qualità ed economici. Puoi anche usare il tuo modello selezionando \"Modello personalizzato\" qui sotto.",
|
||||
"API Key": "Chiave API",
|
||||
"Get Key": "Ottieni chiave",
|
||||
"Model": "Modello",
|
||||
"Custom Model...": "Modello personalizzato...",
|
||||
"Custom Model ID": "ID modello personalizzato",
|
||||
"Validate": "Convalida",
|
||||
"Model available": "Modello disponibile",
|
||||
"Connection": "Connessione",
|
||||
"Test Connection": "Testa connessione",
|
||||
"Connected": "Connesso",
|
||||
"Custom Colors": "Colori personalizzati",
|
||||
"Color E-Ink Mode": "Modalità E-Ink a colori",
|
||||
"Reading Ruler": "Righello di lettura",
|
||||
"Enable Reading Ruler": "Abilita righello di lettura",
|
||||
"Lines to Highlight": "Righe da evidenziare",
|
||||
"Ruler Color": "Colore del righello",
|
||||
"Command Palette": "Tavolozza dei comandi",
|
||||
"Search settings and actions...": "Cerca impostazioni e azioni...",
|
||||
"No results found for": "Nessun risultato trovato per",
|
||||
"Type to search settings and actions": "Digita per cercare impostazioni e azioni",
|
||||
"Recent": "Recenti",
|
||||
"navigate": "naviga",
|
||||
"select": "seleziona",
|
||||
"close": "chiudi",
|
||||
"Search Settings": "Cerca impostazioni",
|
||||
"Page Margins": "Margini della pagina",
|
||||
"AI Provider": "Fornitore AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Modello Ollama",
|
||||
"AI Gateway Model": "Modello AI Gateway",
|
||||
"Actions": "Azioni",
|
||||
"Navigation": "Navigazione",
|
||||
"Set status for {{count}} book(s)_one": "Imposta stato per {{count}} libro",
|
||||
"Set status for {{count}} book(s)_other": "Imposta stato per {{count}} libri",
|
||||
"Mark as Unread": "Segna come non letto",
|
||||
"Mark as Finished": "Segna come finito",
|
||||
"Finished": "Finito",
|
||||
"Unread": "Non letto",
|
||||
"Clear Status": "Cancella stato",
|
||||
"Status": "Stato",
|
||||
"Set status for {{count}} book(s)_many": "Imposta stato per {{count}} libri",
|
||||
"Loading": "Caricamento...",
|
||||
"Exit Paragraph Mode": "Esci dalla modalità paragrafo",
|
||||
"Paragraph Mode": "Modalità paragrafo",
|
||||
"Embedding Model": "Modello di incorporazione",
|
||||
"{{count}} book(s) synced_one": "{{count}} libro sincronizzato",
|
||||
"{{count}} book(s) synced_many": "{{count}} libri sincronizzati",
|
||||
"{{count}} book(s) synced_other": "{{count}} libri sincronizzati",
|
||||
"Unable to start RSVP": "Impossibile avviare RSVP",
|
||||
"RSVP not supported for PDF": "RSVP non supportato per PDF",
|
||||
"Select Chapter": "Seleziona capitolo",
|
||||
"Context": "Contesto",
|
||||
"Ready": "Pronto",
|
||||
"Chapter Progress": "Progresso capitolo",
|
||||
"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",
|
||||
"Choose where to start reading": "Scegli dove iniziare a leggere",
|
||||
"From Chapter Start": "Dall'inizio del capitolo",
|
||||
"Start reading from the beginning of the chapter": "Inizia a leggere dall'inizio del capitolo",
|
||||
"Resume": "Riprendi",
|
||||
"Continue from where you left off": "Continua da dove avevi interrotto",
|
||||
"From Current Page": "Dalla pagina corrente",
|
||||
"Start from where you are currently reading": "Inizia da dove stai leggendo",
|
||||
"From Selection": "Dalla selezione",
|
||||
"Speed Reading Mode": "Modalità lettura veloce",
|
||||
"Scroll left": "Scorri a sinistra",
|
||||
"Scroll right": "Scorri a destra",
|
||||
"Library Sync Progress": "Avanzamento sincronizzazione libreria",
|
||||
"Back to library": "Torna alla libreria",
|
||||
"Group by...": "Raggruppa per...",
|
||||
"Export as Plain Text": "Esporta come testo normale",
|
||||
"Export as Markdown": "Esporta come Markdown",
|
||||
"Show Page Navigation Buttons": "Tasti navigazione",
|
||||
"Page {{number}}": "Pagina {{number}}",
|
||||
"highlight": "evidenziazione",
|
||||
"underline": "sottolineatura",
|
||||
"squiggly": "ondulato",
|
||||
"red": "rosso",
|
||||
"violet": "viola",
|
||||
"blue": "blu",
|
||||
"green": "verde",
|
||||
"yellow": "giallo",
|
||||
"Select {{style}} style": "Seleziona lo stile {{style}}",
|
||||
"Select {{color}} color": "Seleziona il colore {{color}}",
|
||||
"Close Book": "Chiudi libro",
|
||||
"Speed Reading": "Lettura veloce",
|
||||
"Close Speed Reading": "Chiudi lettura veloce",
|
||||
"Authors": "Autori",
|
||||
"Books": "Libri",
|
||||
"Groups": "Gruppi",
|
||||
"Back to TTS Location": "Torna alla posizione TTS",
|
||||
"Metadata": "Metadati",
|
||||
"Image viewer": "Visualizzatore immagini",
|
||||
"Previous Image": "Immagine precedente",
|
||||
"Next Image": "Immagine successiva",
|
||||
"Zoomed": "Zoomato",
|
||||
"Zoom level": "Livello di zoom",
|
||||
"Table viewer": "Visualizzatore tabelle",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Impossibile connettersi a Readwise. Controlla la tua connessione di rete.",
|
||||
"Invalid Readwise access token": "Token di accesso Readwise non valido",
|
||||
"Disconnected from Readwise": "Disconnesso da Readwise",
|
||||
"Never": "Mai",
|
||||
"Readwise Settings": "Impostazioni Readwise",
|
||||
"Connected to Readwise": "Connesso a Readwise",
|
||||
"Last synced: {{time}}": "Ultima sincronizzazione: {{time}}",
|
||||
"Sync Enabled": "Sincronizzazione abilitata",
|
||||
"Disconnect": "Disconnetti",
|
||||
"Connect your Readwise account to sync highlights.": "Connetti il tuo account Readwise per sincronizzare le evidenziazioni.",
|
||||
"Get your access token at": "Ottieni il tuo token di accesso su",
|
||||
"Access Token": "Token di accesso",
|
||||
"Paste your Readwise access token": "Incolla il tuo token di accesso Readwise",
|
||||
"Config": "Configurazione",
|
||||
"Readwise Sync": "Sincronizzazione Readwise",
|
||||
"Push Highlights": "Invia evidenziazioni",
|
||||
"Highlights synced to Readwise": "Evidenziazioni sincronizzate su Readwise",
|
||||
"Readwise sync failed: no internet connection": "Sincronizzazione Readwise fallita: nessuna connessione internet",
|
||||
"Readwise sync failed: {{error}}": "Sincronizzazione Readwise fallita: {{error}}",
|
||||
"System Screen Brightness": "Luminosità dello schermo del sistema",
|
||||
"Page:": "Pagina:",
|
||||
"Page: {{number}}": "Pagina: {{number}}",
|
||||
"Annotation page number": "Numero di pagina dell'annotazione"
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
"Fit": "フィット",
|
||||
"Reset {{settings}}": "{{settings}}をリセット",
|
||||
"Reset Settings": "設定をリセット",
|
||||
"{{count}} pages left in chapter_other": "{{count}} ページ残り",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> ページ残り</1>",
|
||||
"Show Remaining Pages": "残りを表示",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "サイズ",
|
||||
"Cover": "カバー",
|
||||
"Contain": "含む",
|
||||
"{{number}} pages left in chapter": "章に{{number}}ページ残り",
|
||||
"{{number}} pages left in chapter": "<1>章に</1><0>{{number}}</0><1>ページ残り</1>",
|
||||
"Device": "デバイス",
|
||||
"E-Ink Mode": "E-Inkモード",
|
||||
"Highlight Colors": "ハイライトカラー",
|
||||
"Auto Screen Brightness": "自動画面の明るさ",
|
||||
"Pagination": "ページネーション",
|
||||
"Disable Double Tap": "ダブルタップを無効にする",
|
||||
"Tap to Paginate": "タップしてページを切り替える",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "結果を表示",
|
||||
"Clear search": "検索をクリア",
|
||||
"Clear search history": "検索履歴をクリア",
|
||||
"Quick action disabled": "クイックアクション無効",
|
||||
"Tap to Toggle Footer": "タップでフッターを切り替え",
|
||||
"Exported successfully": "エクスポート成功",
|
||||
"Book exported successfully.": "書籍のエクスポートに成功しました。",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "注釈の配列",
|
||||
"Highlighted text": "ハイライトテキスト",
|
||||
"Annotation note": "注釈ノート",
|
||||
"Update time": "更新時刻",
|
||||
"Date Format Tokens:": "日付形式トークン:",
|
||||
"Year (4 digits)": "年(4桁)",
|
||||
"Month (01-12)": "月(01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "音声選択",
|
||||
"Toggle Sticky Bottom TTS Bar": "TTSバー固定切替",
|
||||
"Display what I'm reading on Discord": "読書中の本をDiscordに表示",
|
||||
"Show on Discord": "Discordに表示"
|
||||
"Show on Discord": "Discordに表示",
|
||||
"Instant {{action}}": "インスタント{{action}}",
|
||||
"Instant {{action}} Disabled": "インスタント{{action}}を無効化",
|
||||
"Annotation": "注釈",
|
||||
"Reset Template": "テンプレートをリセット",
|
||||
"Annotation style": "注釈スタイル",
|
||||
"Annotation color": "注釈の色",
|
||||
"Annotation time": "注釈時間",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "この本を再インデックスしてもよろしいですか?",
|
||||
"Enable AI in Settings": "設定でAIを有効にする",
|
||||
"Index This Book": "この本をインデックス",
|
||||
"Enable AI search and chat for this book": "この本のAI検索とチャットを有効にする",
|
||||
"Start Indexing": "インデックス開始",
|
||||
"Indexing book...": "本をインデックス中...",
|
||||
"Preparing...": "準備中...",
|
||||
"Delete this conversation?": "この会話を削除しますか?",
|
||||
"No conversations yet": "まだ会話がありません",
|
||||
"Start a new chat to ask questions about this book": "この本について質問するには新しいチャットを開始してください",
|
||||
"Rename": "名前を変更",
|
||||
"New Chat": "新しいチャット",
|
||||
"Chat": "チャット",
|
||||
"Please enter a model ID": "モデルIDを入力してください",
|
||||
"Model not available or invalid": "モデルが利用できないか無効です",
|
||||
"Failed to validate model": "モデルの検証に失敗しました",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollamaに接続できませんでした。実行中ですか?",
|
||||
"Invalid API key or connection failed": "APIキーが無効か接続に失敗しました",
|
||||
"Connection failed": "接続に失敗しました",
|
||||
"AI Assistant": "AIアシスタント",
|
||||
"Enable AI Assistant": "AIアシスタントを有効にする",
|
||||
"Provider": "プロバイダー",
|
||||
"Ollama (Local)": "Ollama(ローカル)",
|
||||
"AI Gateway (Cloud)": "AIゲートウェイ(クラウド)",
|
||||
"Ollama Configuration": "Ollama設定",
|
||||
"Refresh Models": "モデルを更新",
|
||||
"AI Model": "AIモデル",
|
||||
"No models detected": "モデルが検出されませんでした",
|
||||
"AI Gateway Configuration": "AIゲートウェイ設定",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "高品質で経済的なAIモデルからお選びください。下の「カスタムモデル」を選択して独自のモデルを使用することもできます。",
|
||||
"API Key": "APIキー",
|
||||
"Get Key": "キーを取得",
|
||||
"Model": "モデル",
|
||||
"Custom Model...": "カスタムモデル...",
|
||||
"Custom Model ID": "カスタムモデルID",
|
||||
"Validate": "検証",
|
||||
"Model available": "モデルが利用可能",
|
||||
"Connection": "接続",
|
||||
"Test Connection": "接続をテスト",
|
||||
"Connected": "接続済み",
|
||||
"Custom Colors": "カスタムカラー",
|
||||
"Color E-Ink Mode": "カラー電子ペーパーモード",
|
||||
"Reading Ruler": "リーディングルーラー",
|
||||
"Enable Reading Ruler": "リーディングルーラーを有効にする",
|
||||
"Lines to Highlight": "ハイライトする行数",
|
||||
"Ruler Color": "ルーラーの色",
|
||||
"Command Palette": "コマンドパレット",
|
||||
"Search settings and actions...": "設定とアクションを検索...",
|
||||
"No results found for": "の結果が見つかりませんでした",
|
||||
"Type to search settings and actions": "入力して設定とアクションを検索",
|
||||
"Recent": "最近使った項目",
|
||||
"navigate": "ナビゲート",
|
||||
"select": "選択",
|
||||
"close": "閉じる",
|
||||
"Search Settings": "設定を検索",
|
||||
"Page Margins": "ページ余白",
|
||||
"AI Provider": "AIプロバイダー",
|
||||
"Ollama URL": "OllamaのURL",
|
||||
"Ollama Model": "Ollamaモデル",
|
||||
"AI Gateway Model": "AI Gatewayモデル",
|
||||
"Actions": "アクション",
|
||||
"Navigation": "ナビゲーション",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} 冊の本のステータスを設定",
|
||||
"Mark as Unread": "未読にする",
|
||||
"Mark as Finished": "読了にする",
|
||||
"Finished": "読了",
|
||||
"Unread": "未読",
|
||||
"Clear Status": "ステータスをクリア",
|
||||
"Status": "ステータス",
|
||||
"Loading": "読み込み中",
|
||||
"Exit Paragraph Mode": "段落モードを終了",
|
||||
"Paragraph Mode": "段落モード",
|
||||
"Embedding Model": "埋め込みモデル",
|
||||
"{{count}} book(s) synced_other": "{{count}} 冊の本が同期されました",
|
||||
"Unable to start RSVP": "RSVPを起動できません",
|
||||
"RSVP not supported for PDF": "PDFはRSVPに対応していません",
|
||||
"Select Chapter": "章を選択",
|
||||
"Context": "コンテキスト",
|
||||
"Ready": "準備完了",
|
||||
"Chapter Progress": "章の進捗",
|
||||
"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読書を開始",
|
||||
"Choose where to start reading": "読書を開始する場所を選択",
|
||||
"From Chapter Start": "章の最初から",
|
||||
"Start reading from the beginning of the chapter": "この章の最初から読み直す",
|
||||
"Resume": "再開",
|
||||
"Continue from where you left off": "中断した場所から再開する",
|
||||
"From Current Page": "現在のページから",
|
||||
"Start from where you are currently reading": "現在読んでいる場所から開始する",
|
||||
"From Selection": "選択範囲から",
|
||||
"Speed Reading Mode": "速読モード",
|
||||
"Scroll left": "左にスクロール",
|
||||
"Scroll right": "右にスクロール",
|
||||
"Library Sync Progress": "ライブラリ同期の進捗",
|
||||
"Back to library": "ライブラリに戻る",
|
||||
"Group by...": "グループ化...",
|
||||
"Export as Plain Text": "プレーンテキストとして書き出し",
|
||||
"Export as Markdown": "Markdownとして書き出し",
|
||||
"Show Page Navigation Buttons": "ナビゲーションボタンを表示",
|
||||
"Page {{number}}": "{{number}} ページ",
|
||||
"highlight": "ハイライト",
|
||||
"underline": "下線",
|
||||
"squiggly": "波線",
|
||||
"red": "赤",
|
||||
"violet": "すみれ色",
|
||||
"blue": "青",
|
||||
"green": "緑",
|
||||
"yellow": "黄色",
|
||||
"Select {{style}} style": "スタイル {{style}} を選択",
|
||||
"Select {{color}} color": "色 {{color}} を選択",
|
||||
"Close Book": "本を閉じる",
|
||||
"Speed Reading": "速読",
|
||||
"Close Speed Reading": "速読を閉じる",
|
||||
"Authors": "著者",
|
||||
"Books": "書籍",
|
||||
"Groups": "グループ",
|
||||
"Back to TTS Location": "TTSの位置に戻る",
|
||||
"Metadata": "メタデータ",
|
||||
"Image viewer": "画像ビューア",
|
||||
"Previous Image": "前の画像",
|
||||
"Next Image": "次の画像",
|
||||
"Zoomed": "ズーム済み",
|
||||
"Zoom level": "ズームレベル",
|
||||
"Table viewer": "テーブルビューア",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise に接続できません。ネットワーク接続を確認してください。",
|
||||
"Invalid Readwise access token": "無効な Readwise アクセストークン",
|
||||
"Disconnected from Readwise": "Readwise から切断されました",
|
||||
"Never": "一度もなし",
|
||||
"Readwise Settings": "Readwise 設定",
|
||||
"Connected to Readwise": "Readwise に接続済み",
|
||||
"Last synced: {{time}}": "最終同期:{{time}}",
|
||||
"Sync Enabled": "同期有効",
|
||||
"Disconnect": "切断",
|
||||
"Connect your Readwise account to sync highlights.": "ハイライトを同期するには Readwise アカウントを接続してください。",
|
||||
"Get your access token at": "アクセストークンの取得先:",
|
||||
"Access Token": "アクセストークン",
|
||||
"Paste your Readwise access token": "Readwise アクセストークンを貼り付けてください",
|
||||
"Config": "設定",
|
||||
"Readwise Sync": "Readwise 同期",
|
||||
"Push Highlights": "ハイライトをプッシュ",
|
||||
"Highlights synced to Readwise": "ハイライトが Readwise に同期されました",
|
||||
"Readwise sync failed: no internet connection": "Readwise 同期失敗:インターネット接続がありません",
|
||||
"Readwise sync failed: {{error}}": "Readwise 同期失敗:{{error}}",
|
||||
"System Screen Brightness": "システムの画面の明るさ",
|
||||
"Page:": "ページ:",
|
||||
"Page: {{number}}": "ページ: {{number}}",
|
||||
"Annotation page number": "注釈のページ番号"
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
"Fit": "맞춤",
|
||||
"Reset {{settings}}": "{{settings}} 재설정",
|
||||
"Reset Settings": "설정 재설정",
|
||||
"{{count}} pages left in chapter_other": "남은 페이지 {{count}}장",
|
||||
"{{count}} pages left in chapter_other": "<1>남은 페이지 </1><0>{{count}}</0><1>장</1>",
|
||||
"Show Remaining Pages": "남은 페이지 보기",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "크기",
|
||||
"Cover": "표지",
|
||||
"Contain": "포함",
|
||||
"{{number}} pages left in chapter": "남은 페이지 {{number}}장",
|
||||
"{{number}} pages left in chapter": "<1>남은 페이지 </1><0>{{number}}</0><1>장</1>",
|
||||
"Device": "장치",
|
||||
"E-Ink Mode": "E-Ink 모드",
|
||||
"Highlight Colors": "하이라이트 색상",
|
||||
"Auto Screen Brightness": "자동 화면 밝기",
|
||||
"Pagination": "페이지 매김",
|
||||
"Disable Double Tap": "더블 탭 비활성화",
|
||||
"Tap to Paginate": "탭하여 페이지 매김",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "결과 표시",
|
||||
"Clear search": "검색 지우기",
|
||||
"Clear search history": "검색 기록 지우기",
|
||||
"Quick action disabled": "빠른 작업 비활성화됨",
|
||||
"Tap to Toggle Footer": "탭하여 바닥글 전환",
|
||||
"Exported successfully": "내보내기 성공",
|
||||
"Book exported successfully.": "책을 성공적으로 내보냈습니다.",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "주석 배열",
|
||||
"Highlighted text": "하이라이트된 텍스트",
|
||||
"Annotation note": "주석 노트",
|
||||
"Update time": "업데이트 시간",
|
||||
"Date Format Tokens:": "날짜 형식 토큰:",
|
||||
"Year (4 digits)": "연도 (4자리)",
|
||||
"Month (01-12)": "월 (01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "음성 선택",
|
||||
"Toggle Sticky Bottom TTS Bar": "고정 TTS 바 전환",
|
||||
"Display what I'm reading on Discord": "Discord에 읽는 책 표시",
|
||||
"Show on Discord": "Discord에 표시"
|
||||
"Show on Discord": "Discord에 표시",
|
||||
"Instant {{action}}": "즉시 {{action}}",
|
||||
"Instant {{action}} Disabled": "즉시 {{action}} 비활성화",
|
||||
"Annotation": "주석",
|
||||
"Reset Template": "템플릿 초기화",
|
||||
"Annotation style": "주석 스타일",
|
||||
"Annotation color": "주석 색상",
|
||||
"Annotation time": "주석 시간",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "이 책을 다시 인덱싱하시겠습니까?",
|
||||
"Enable AI in Settings": "설정에서 AI 활성화",
|
||||
"Index This Book": "이 책 인덱싱",
|
||||
"Enable AI search and chat for this book": "이 책에 대한 AI 검색 및 채팅 활성화",
|
||||
"Start Indexing": "인덱싱 시작",
|
||||
"Indexing book...": "책 인덱싱 중...",
|
||||
"Preparing...": "준비 중...",
|
||||
"Delete this conversation?": "이 대화를 삭제하시겠습니까?",
|
||||
"No conversations yet": "아직 대화가 없습니다",
|
||||
"Start a new chat to ask questions about this book": "이 책에 대해 질문하려면 새 채팅을 시작하세요",
|
||||
"Rename": "이름 변경",
|
||||
"New Chat": "새 채팅",
|
||||
"Chat": "채팅",
|
||||
"Please enter a model ID": "모델 ID를 입력하세요",
|
||||
"Model not available or invalid": "모델을 사용할 수 없거나 유효하지 않습니다",
|
||||
"Failed to validate model": "모델 검증 실패",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama에 연결할 수 없습니다. 실행 중인가요?",
|
||||
"Invalid API key or connection failed": "API 키가 유효하지 않거나 연결 실패",
|
||||
"Connection failed": "연결 실패",
|
||||
"AI Assistant": "AI 어시스턴트",
|
||||
"Enable AI Assistant": "AI 어시스턴트 활성화",
|
||||
"Provider": "제공자",
|
||||
"Ollama (Local)": "Ollama (로컬)",
|
||||
"AI Gateway (Cloud)": "AI 게이트웨이 (클라우드)",
|
||||
"Ollama Configuration": "Ollama 설정",
|
||||
"Refresh Models": "모델 새로고침",
|
||||
"AI Model": "AI 모델",
|
||||
"No models detected": "모델이 감지되지 않았습니다",
|
||||
"AI Gateway Configuration": "AI 게이트웨이 설정",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "고품질의 경제적인 AI 모델 중에서 선택하세요. 아래의 \"사용자 정의 모델\"을 선택하여 자신의 모델을 사용할 수도 있습니다.",
|
||||
"API Key": "API 키",
|
||||
"Get Key": "키 받기",
|
||||
"Model": "모델",
|
||||
"Custom Model...": "사용자 정의 모델...",
|
||||
"Custom Model ID": "사용자 정의 모델 ID",
|
||||
"Validate": "검증",
|
||||
"Model available": "모델 사용 가능",
|
||||
"Connection": "연결",
|
||||
"Test Connection": "연결 테스트",
|
||||
"Connected": "연결됨",
|
||||
"Custom Colors": "사용자 정의 색상",
|
||||
"Color E-Ink Mode": "컬러 E-Ink 모드",
|
||||
"Reading Ruler": "읽기 자",
|
||||
"Enable Reading Ruler": "읽기 자 활성화",
|
||||
"Lines to Highlight": "강조할 줄 수",
|
||||
"Ruler Color": "자 색상",
|
||||
"Command Palette": "명령 팔레트",
|
||||
"Search settings and actions...": "설정 및 작업 검색...",
|
||||
"No results found for": "에 대한 결과를 찾을 수 없음",
|
||||
"Type to search settings and actions": "입력하여 설정 및 작업 검색",
|
||||
"Recent": "최근",
|
||||
"navigate": "탐색",
|
||||
"select": "선택",
|
||||
"close": "닫기",
|
||||
"Search Settings": "설정 검색",
|
||||
"Page Margins": "페이지 여백",
|
||||
"AI Provider": "AI 제공처",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama 모델",
|
||||
"AI Gateway Model": "AI 게이트웨이 모델",
|
||||
"Actions": "작업",
|
||||
"Navigation": "탐색",
|
||||
"Set status for {{count}} book(s)_other": "책 {{count}}권 상태 설정",
|
||||
"Mark as Unread": "읽지 않음으로 표시",
|
||||
"Mark as Finished": "완료로 표시",
|
||||
"Finished": "완료",
|
||||
"Unread": "읽지 않음",
|
||||
"Clear Status": "상태 지우기",
|
||||
"Status": "상태",
|
||||
"Loading": "로드 중...",
|
||||
"Exit Paragraph Mode": "단락 모드 종료",
|
||||
"Paragraph Mode": "단락 모드",
|
||||
"Embedding Model": "임베딩 모델",
|
||||
"{{count}} book(s) synced_other": "{{count}}권의 책이 동기화되었습니다",
|
||||
"Unable to start RSVP": "RSVP를 시작할 수 없습니다",
|
||||
"RSVP not supported for PDF": "PDF는 RSVP를 지원하지 않습니다",
|
||||
"Select Chapter": "챕터 선택",
|
||||
"Context": "문맥",
|
||||
"Ready": "준비됨",
|
||||
"Chapter Progress": "챕터 진행 상황",
|
||||
"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 독서 시작",
|
||||
"Choose where to start reading": "독서를 시작할 위치 선택",
|
||||
"From Chapter Start": "챕터 시작부터",
|
||||
"Start reading from the beginning of the chapter": "챕터 처음부터 다시 시작",
|
||||
"Resume": "재개",
|
||||
"Continue from where you left off": "중단한 지점부터 계속 읽기",
|
||||
"From Current Page": "현재 페이지부터",
|
||||
"Start from where you are currently reading": "현재 읽고 있는 위치부터 시작",
|
||||
"From Selection": "선택 범위부터",
|
||||
"Speed Reading Mode": "속독 모드",
|
||||
"Scroll left": "왼쪽으로 스크롤",
|
||||
"Scroll right": "오른쪽으로 스크롤",
|
||||
"Library Sync Progress": "라이브러리 동기화 진행 상황",
|
||||
"Back to library": "라이브러리로 돌아가기",
|
||||
"Group by...": "그룹화 기준...",
|
||||
"Export as Plain Text": "일반 텍스트로 내보내기",
|
||||
"Export as Markdown": "Markdown으로 내보내기",
|
||||
"Show Page Navigation Buttons": "탐색 버튼 표시",
|
||||
"Page {{number}}": "{{number}} 페이지",
|
||||
"highlight": "하이라이트",
|
||||
"underline": "밑줄",
|
||||
"squiggly": "구불구불한 선",
|
||||
"red": "빨간색",
|
||||
"violet": "보라색",
|
||||
"blue": "파란색",
|
||||
"green": "초록색",
|
||||
"yellow": "노란색",
|
||||
"Select {{style}} style": "{{style}} 스타일 선택",
|
||||
"Select {{color}} color": "{{color}} 색상 선택",
|
||||
"Close Book": "책 닫기",
|
||||
"Speed Reading": "속독",
|
||||
"Close Speed Reading": "속독 닫기",
|
||||
"Authors": "저자",
|
||||
"Books": "도서",
|
||||
"Groups": "그룹",
|
||||
"Back to TTS Location": "TTS 위치로 돌아가기",
|
||||
"Metadata": "메타데이터",
|
||||
"Image viewer": "이미지 뷰어",
|
||||
"Previous Image": "이전 이미지",
|
||||
"Next Image": "다음 이미지",
|
||||
"Zoomed": "확대됨",
|
||||
"Zoom level": "확대 수준",
|
||||
"Table viewer": "테이블 뷰어",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise에 연결할 수 없습니다. 네트워크 연결을 확인해 주세요.",
|
||||
"Invalid Readwise access token": "유효하지 않은 Readwise 액세스 토큰입니다",
|
||||
"Disconnected from Readwise": "Readwise와 연결이 끊어졌습니다",
|
||||
"Never": "안 함",
|
||||
"Readwise Settings": "Readwise 설정",
|
||||
"Connected to Readwise": "Readwise에 연결됨",
|
||||
"Last synced: {{time}}": "마지막 동기화: {{time}}",
|
||||
"Sync Enabled": "동기화 활성화됨",
|
||||
"Disconnect": "연결 해제",
|
||||
"Connect your Readwise account to sync highlights.": "하이라이트를 동기화하려면 Readwise 계정렬 연결하세요.",
|
||||
"Get your access token at": "액세스 토큰 받기:",
|
||||
"Access Token": "액세스 토큰",
|
||||
"Paste your Readwise access token": "Readwise 액세스 토큰을 붙여넣으세요",
|
||||
"Config": "구성",
|
||||
"Readwise Sync": "Readwise 동기화",
|
||||
"Push Highlights": "하이라이트 푸시",
|
||||
"Highlights synced to Readwise": "하이라이트가 Readwise에 동기화되었습니다",
|
||||
"Readwise sync failed: no internet connection": "Readwise 동기화 실패: 인터넷 연결 없음",
|
||||
"Readwise sync failed: {{error}}": "Readwise 동기화 실패: {{error}}",
|
||||
"System Screen Brightness": "시스템 화면 밝기",
|
||||
"Page:": "페이지:",
|
||||
"Page: {{number}}": "페이지: {{number}}",
|
||||
"Annotation page number": "주석 페이지 번호"
|
||||
}
|
||||
|
||||
@@ -248,8 +248,8 @@
|
||||
"Add your notes here...": "Tambah nota anda di sini...",
|
||||
"Search notes and excerpts...": "Cari nota dan petikan...",
|
||||
"{{time}} min left in chapter": "{{time}} min lagi dalam bab",
|
||||
"{{number}} pages left in chapter": "{{number}} halaman lagi dalam bab",
|
||||
"{{count}} pages left in chapter_other": "{{count}} halaman lagi dalam bab",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> halaman lagi dalam bab</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> halaman lagi dalam bab</1>",
|
||||
"On {{current}} of {{total}} page": "Di halaman {{current}} daripada {{total}}",
|
||||
"Unable to open book": "Tidak dapat membuka buku",
|
||||
"Section Title": "Tajuk Seksyen",
|
||||
@@ -517,7 +517,6 @@
|
||||
"Paging Animation": "Animasi Halaman",
|
||||
"Device": "Peranti",
|
||||
"E-Ink Mode": "Mod E-Ink",
|
||||
"Auto Screen Brightness": "Kecerahan Skrin Auto",
|
||||
"Security": "Keselamatan",
|
||||
"Allow JavaScript": "Benarkan JavaScript",
|
||||
"Enable only if you trust the file.": "Aktifkan hanya jika anda mempercayai fail.",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "Tunjukkan Hasil",
|
||||
"Clear search": "Kosongkan carian",
|
||||
"Clear search history": "Kosongkan sejarah carian",
|
||||
"Quick action disabled": "Tindakan pantas dilumpuhkan",
|
||||
"Tap to Toggle Footer": "Ketik untuk togol pengaki",
|
||||
"Exported successfully": "Berjaya dieksport",
|
||||
"Book exported successfully.": "Buku berjaya dieksport.",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "Senarai anotasi",
|
||||
"Highlighted text": "Teks yang diserlahkan",
|
||||
"Annotation note": "Nota anotasi",
|
||||
"Update time": "Masa kemas kini",
|
||||
"Date Format Tokens:": "Token Format Tarikh:",
|
||||
"Year (4 digits)": "Tahun (4 digit)",
|
||||
"Month (01-12)": "Bulan (01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "Pilih suara",
|
||||
"Toggle Sticky Bottom TTS Bar": "Togol bar TTS melekit",
|
||||
"Display what I'm reading on Discord": "Papar buku yang sedang dibaca di Discord",
|
||||
"Show on Discord": "Papar di Discord"
|
||||
"Show on Discord": "Papar di Discord",
|
||||
"Instant {{action}}": "{{action}} Segera",
|
||||
"Instant {{action}} Disabled": "{{action}} Segera Dilumpuhkan",
|
||||
"Annotation": "Anotasi",
|
||||
"Reset Template": "Tetapkan Semula Templat",
|
||||
"Annotation style": "Gaya anotasi",
|
||||
"Annotation color": "Warna anotasi",
|
||||
"Annotation time": "Masa anotasi",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Adakah anda pasti mahu mengindeks semula buku ini?",
|
||||
"Enable AI in Settings": "Aktifkan AI dalam Tetapan",
|
||||
"Index This Book": "Indeks Buku Ini",
|
||||
"Enable AI search and chat for this book": "Aktifkan carian AI dan sembang untuk buku ini",
|
||||
"Start Indexing": "Mula Mengindeks",
|
||||
"Indexing book...": "Mengindeks buku...",
|
||||
"Preparing...": "Menyediakan...",
|
||||
"Delete this conversation?": "Padam perbualan ini?",
|
||||
"No conversations yet": "Tiada perbualan lagi",
|
||||
"Start a new chat to ask questions about this book": "Mulakan sembang baharu untuk bertanya tentang buku ini",
|
||||
"Rename": "Namakan Semula",
|
||||
"New Chat": "Sembang Baharu",
|
||||
"Chat": "Sembang",
|
||||
"Please enter a model ID": "Sila masukkan ID model",
|
||||
"Model not available or invalid": "Model tidak tersedia atau tidak sah",
|
||||
"Failed to validate model": "Gagal mengesahkan model",
|
||||
"Couldn't connect to Ollama. Is it running?": "Tidak dapat menyambung ke Ollama. Adakah ia sedang berjalan?",
|
||||
"Invalid API key or connection failed": "Kunci API tidak sah atau sambungan gagal",
|
||||
"Connection failed": "Sambungan gagal",
|
||||
"AI Assistant": "Pembantu AI",
|
||||
"Enable AI Assistant": "Aktifkan Pembantu AI",
|
||||
"Provider": "Pembekal",
|
||||
"Ollama (Local)": "Ollama (Tempatan)",
|
||||
"AI Gateway (Cloud)": "Gateway AI (Awan)",
|
||||
"Ollama Configuration": "Konfigurasi Ollama",
|
||||
"Refresh Models": "Muat Semula Model",
|
||||
"AI Model": "Model AI",
|
||||
"No models detected": "Tiada model dikesan",
|
||||
"AI Gateway Configuration": "Konfigurasi Gateway AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Pilih dari pelbagai model AI berkualiti tinggi dan ekonomi. Anda juga boleh menggunakan model anda sendiri dengan memilih \"Model Tersuai\" di bawah.",
|
||||
"API Key": "Kunci API",
|
||||
"Get Key": "Dapatkan Kunci",
|
||||
"Model": "Model",
|
||||
"Custom Model...": "Model Tersuai...",
|
||||
"Custom Model ID": "ID Model Tersuai",
|
||||
"Validate": "Sahkan",
|
||||
"Model available": "Model tersedia",
|
||||
"Connection": "Sambungan",
|
||||
"Test Connection": "Uji Sambungan",
|
||||
"Connected": "Disambung",
|
||||
"Custom Colors": "Warna Tersuai",
|
||||
"Color E-Ink Mode": "Mod E-Ink Warna",
|
||||
"Reading Ruler": "Pembaris Membaca",
|
||||
"Enable Reading Ruler": "Dayakan Pembaris Membaca",
|
||||
"Lines to Highlight": "Baris untuk Diserlahkan",
|
||||
"Ruler Color": "Warna Pembaris",
|
||||
"Command Palette": "Palet Perintah",
|
||||
"Search settings and actions...": "Cari tetapan dan tindakan...",
|
||||
"No results found for": "Tiada hasil dijumpai untuk",
|
||||
"Type to search settings and actions": "Taip untuk mencari tetapan dan tindakan",
|
||||
"Recent": "Baru-baru ini",
|
||||
"navigate": "navigasi",
|
||||
"select": "pilih",
|
||||
"close": "tutup",
|
||||
"Search Settings": "Cari Tetapan",
|
||||
"Page Margins": "Margin Halaman",
|
||||
"AI Provider": "Penyedia AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi",
|
||||
"Set status for {{count}} book(s)_other": "Tetapkan status untuk {{count}} buku",
|
||||
"Mark as Unread": "Tandakan sebagai Belum Dibaca",
|
||||
"Mark as Finished": "Tandakan sebagai Selesai",
|
||||
"Finished": "Selesai",
|
||||
"Unread": "Belum Dibaca",
|
||||
"Clear Status": "Kosongkan Status",
|
||||
"Status": "Status",
|
||||
"Loading": "Memuatkan...",
|
||||
"Exit Paragraph Mode": "Keluar dari Mod Perenggan",
|
||||
"Paragraph Mode": "Mod Perenggan",
|
||||
"Embedding Model": "Model Benaman",
|
||||
"{{count}} book(s) synced_other": "{{count}} buku telah disegerakkan",
|
||||
"Unable to start RSVP": "Tidak dapat memulakan RSVP",
|
||||
"RSVP not supported for PDF": "RSVP tidak disokong untuk PDF",
|
||||
"Select Chapter": "Pilih Bab",
|
||||
"Context": "Konteks",
|
||||
"Ready": "Sedia",
|
||||
"Chapter Progress": "Kemajuan Bab",
|
||||
"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",
|
||||
"Choose where to start reading": "Pilih tempat untuk mula membaca",
|
||||
"From Chapter Start": "Dari Permulaan Bab",
|
||||
"Start reading from the beginning of the chapter": "Mula membaca dari awal bab",
|
||||
"Resume": "Sambung",
|
||||
"Continue from where you left off": "Sambung dari tempat anda berhenti",
|
||||
"From Current Page": "Dari Halaman Semasa",
|
||||
"Start from where you are currently reading": "Mula dari tempat anda sedang membaca",
|
||||
"From Selection": "Dari Pilihan",
|
||||
"Speed Reading Mode": "Mod Bacaan Pantas",
|
||||
"Scroll left": "Tatal ke kiri",
|
||||
"Scroll right": "Tatal ke kanan",
|
||||
"Library Sync Progress": "Kemajuan Segerak Perpustakaan",
|
||||
"Back to library": "Kembali ke perpustakaan",
|
||||
"Group by...": "Kumpulan mengikut...",
|
||||
"Export as Plain Text": "Eksport sebagai Teks Biasa",
|
||||
"Export as Markdown": "Eksport sebagai Markdown",
|
||||
"Show Page Navigation Buttons": "Butang navigasi",
|
||||
"Page {{number}}": "Halaman {{number}}",
|
||||
"highlight": "serlah",
|
||||
"underline": "garis bawah",
|
||||
"squiggly": "berombak",
|
||||
"red": "merah",
|
||||
"violet": "ungu",
|
||||
"blue": "biru",
|
||||
"green": "hijau",
|
||||
"yellow": "kuning",
|
||||
"Select {{style}} style": "Pilih gaya {{style}}",
|
||||
"Select {{color}} color": "Pilih warna {{color}}",
|
||||
"Close Book": "Tutup Buku",
|
||||
"Speed Reading": "Bacaan Pantas",
|
||||
"Close Speed Reading": "Tutup Bacaan Pantas",
|
||||
"Authors": "Penulis",
|
||||
"Books": "Buku",
|
||||
"Groups": "Kumpulan",
|
||||
"Back to TTS Location": "Kembali ke Lokasi TTS",
|
||||
"Metadata": "Metadata",
|
||||
"Image viewer": "Penyorot imej",
|
||||
"Previous Image": "Imej Sebelumnya",
|
||||
"Next Image": "Imej Seterusnya",
|
||||
"Zoomed": "Dizum",
|
||||
"Zoom level": "Tahap zum",
|
||||
"Table viewer": "Penyorot jadual",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Tidak dapat menyambung ke Readwise. Sila periksa sambungan rangkaian anda.",
|
||||
"Invalid Readwise access token": "Token akses Readwise tidak sah",
|
||||
"Disconnected from Readwise": "Terputus sambungan daripada Readwise",
|
||||
"Never": "Tidak pernah",
|
||||
"Readwise Settings": "Tetapan Readwise",
|
||||
"Connected to Readwise": "Disambungkan ke Readwise",
|
||||
"Last synced: {{time}}": "Kali terakhir disinkronkan: {{time}}",
|
||||
"Sync Enabled": "Penyinkronan Didayakan",
|
||||
"Disconnect": "Putuskan sambungan",
|
||||
"Connect your Readwise account to sync highlights.": "Sambungkan akaun Readwise anda untuk menyinkronkan sorotan.",
|
||||
"Get your access token at": "Dapatkan token akses anda di",
|
||||
"Access Token": "Token Akses",
|
||||
"Paste your Readwise access token": "Tampal token akses Readwise anda",
|
||||
"Config": "Konfigurasi",
|
||||
"Readwise Sync": "Penyinkronan Readwise",
|
||||
"Push Highlights": "Hantar Sorotan",
|
||||
"Highlights synced to Readwise": "Sorotan disinkronkan ke Readwise",
|
||||
"Readwise sync failed: no internet connection": "Penyinkronan Readwise gagal: tiada sambungan internet",
|
||||
"Readwise sync failed: {{error}}": "Penyinkronan Readwise gagal: {{error}}",
|
||||
"System Screen Brightness": "Kecerahan Skrin Sistem",
|
||||
"Page:": "Halaman:",
|
||||
"Page: {{number}}": "Halaman: {{number}}",
|
||||
"Annotation page number": "Nombor halaman anotasi"
|
||||
}
|
||||
|
||||
@@ -335,8 +335,8 @@
|
||||
"Fit": "Passend maken",
|
||||
"Reset {{settings}}": "Reset {{settings}}",
|
||||
"Reset Settings": "Instellingen resetten",
|
||||
"{{count}} pages left in chapter_one": "Nog {{count}} pagina in hoofdstuk",
|
||||
"{{count}} pages left in chapter_other": "Nog {{count}} pagina’s in hoofdstuk",
|
||||
"{{count}} pages left in chapter_one": "<1>Nog </1><0>{{count}}</0><1> pagina in hoofdstuk</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Nog </1><0>{{count}}</0><1> pagina’s in hoofdstuk</1>",
|
||||
"Show Remaining Pages": "Toon resterende pagina’s",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "Grootte",
|
||||
"Cover": "Omslag",
|
||||
"Contain": "Bevatten",
|
||||
"{{number}} pages left in chapter": "Nog {{number}} pagina’s in hoofdstuk",
|
||||
"{{number}} pages left in chapter": "<1>Nog </1><0>{{number}}</0><1> pagina’s in hoofdstuk</1>",
|
||||
"Device": "Apparaat",
|
||||
"E-Ink Mode": "E-Ink Modus",
|
||||
"Highlight Colors": "Markeerkleuren",
|
||||
"Auto Screen Brightness": "Automatische Schermhelderheid",
|
||||
"Pagination": "Paginering",
|
||||
"Disable Double Tap": "Schakel Dubbel Tikken Uit",
|
||||
"Tap to Paginate": "Tik om te Pagineren",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "Resultaten tonen",
|
||||
"Clear search": "Zoekopdracht wissen",
|
||||
"Clear search history": "Zoekgeschiedenis wissen",
|
||||
"Quick action disabled": "Snelle actie uitgeschakeld",
|
||||
"Tap to Toggle Footer": "Tik om voettekst te wisselen",
|
||||
"Exported successfully": "Succesvol geëxporteerd",
|
||||
"Book exported successfully.": "Boek succesvol geëxporteerd.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "Reeks annotaties",
|
||||
"Highlighted text": "Gemarkeerde tekst",
|
||||
"Annotation note": "Annotatienotitie",
|
||||
"Update time": "Updatetijd",
|
||||
"Date Format Tokens:": "Datumformaattokens:",
|
||||
"Year (4 digits)": "Jaar (4 cijfers)",
|
||||
"Month (01-12)": "Maand (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "Stem selecteren",
|
||||
"Toggle Sticky Bottom TTS Bar": "Vaste TTS-balk schakelen",
|
||||
"Display what I'm reading on Discord": "Toon wat ik lees op Discord",
|
||||
"Show on Discord": "Toon op Discord"
|
||||
"Show on Discord": "Toon op Discord",
|
||||
"Instant {{action}}": "Directe {{action}}",
|
||||
"Instant {{action}} Disabled": "Directe {{action}} uitgeschakeld",
|
||||
"Annotation": "Annotatie",
|
||||
"Reset Template": "Sjabloon resetten",
|
||||
"Annotation style": "Annotatiestijl",
|
||||
"Annotation color": "Annotatiekleur",
|
||||
"Annotation time": "Annotatietijd",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Weet u zeker dat u dit boek opnieuw wilt indexeren?",
|
||||
"Enable AI in Settings": "AI inschakelen in Instellingen",
|
||||
"Index This Book": "Dit boek indexeren",
|
||||
"Enable AI search and chat for this book": "AI-zoeken en chat voor dit boek inschakelen",
|
||||
"Start Indexing": "Indexeren starten",
|
||||
"Indexing book...": "Boek wordt geïndexeerd...",
|
||||
"Preparing...": "Voorbereiden...",
|
||||
"Delete this conversation?": "Dit gesprek verwijderen?",
|
||||
"No conversations yet": "Nog geen gesprekken",
|
||||
"Start a new chat to ask questions about this book": "Start een nieuwe chat om vragen te stellen over dit boek",
|
||||
"Rename": "Hernoemen",
|
||||
"New Chat": "Nieuwe chat",
|
||||
"Chat": "Chat",
|
||||
"Please enter a model ID": "Voer een model-ID in",
|
||||
"Model not available or invalid": "Model niet beschikbaar of ongeldig",
|
||||
"Failed to validate model": "Model valideren mislukt",
|
||||
"Couldn't connect to Ollama. Is it running?": "Kon geen verbinding maken met Ollama. Is het actief?",
|
||||
"Invalid API key or connection failed": "Ongeldige API-sleutel of verbinding mislukt",
|
||||
"Connection failed": "Verbinding mislukt",
|
||||
"AI Assistant": "AI-assistent",
|
||||
"Enable AI Assistant": "AI-assistent inschakelen",
|
||||
"Provider": "Provider",
|
||||
"Ollama (Local)": "Ollama (Lokaal)",
|
||||
"AI Gateway (Cloud)": "AI-gateway (Cloud)",
|
||||
"Ollama Configuration": "Ollama-configuratie",
|
||||
"Refresh Models": "Modellen vernieuwen",
|
||||
"AI Model": "AI-model",
|
||||
"No models detected": "Geen modellen gedetecteerd",
|
||||
"AI Gateway Configuration": "AI-gateway configuratie",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Kies uit een selectie van hoogwaardige, voordelige AI-modellen. U kunt ook uw eigen model gebruiken door hieronder \"Aangepast model\" te selecteren.",
|
||||
"API Key": "API-sleutel",
|
||||
"Get Key": "Sleutel ophalen",
|
||||
"Model": "Model",
|
||||
"Custom Model...": "Aangepast model...",
|
||||
"Custom Model ID": "Aangepast model-ID",
|
||||
"Validate": "Valideren",
|
||||
"Model available": "Model beschikbaar",
|
||||
"Connection": "Verbinding",
|
||||
"Test Connection": "Verbinding testen",
|
||||
"Connected": "Verbonden",
|
||||
"Custom Colors": "Aangepaste kleuren",
|
||||
"Color E-Ink Mode": "Kleur E-Ink modus",
|
||||
"Reading Ruler": "Leesliniaal",
|
||||
"Enable Reading Ruler": "Leesliniaal inschakelen",
|
||||
"Lines to Highlight": "Regels om te markeren",
|
||||
"Ruler Color": "Kleur van liniaal",
|
||||
"Command Palette": "Opdrachtenpalet",
|
||||
"Search settings and actions...": "Zoek instellingen en acties...",
|
||||
"No results found for": "Geen resultaten gevonden voor",
|
||||
"Type to search settings and actions": "Typ om instellingen en acties te zoeken",
|
||||
"Recent": "Recent",
|
||||
"navigate": "navigeren",
|
||||
"select": "selecteren",
|
||||
"close": "sluiten",
|
||||
"Search Settings": "Instellingen zoeken",
|
||||
"Page Margins": "Paginamarges",
|
||||
"AI Provider": "AI-provider",
|
||||
"Ollama URL": "Ollama-URL",
|
||||
"Ollama Model": "Ollama-model",
|
||||
"AI Gateway Model": "AI Gateway-model",
|
||||
"Actions": "Acties",
|
||||
"Navigation": "Navigatie",
|
||||
"Set status for {{count}} book(s)_one": "Stel status in voor {{count}} boek",
|
||||
"Set status for {{count}} book(s)_other": "Stel status in voor {{count}} boeken",
|
||||
"Mark as Unread": "Markeren als ongelezen",
|
||||
"Mark as Finished": "Markeren als voltooid",
|
||||
"Finished": "Voltooid",
|
||||
"Unread": "Ongelezen",
|
||||
"Clear Status": "Status wissen",
|
||||
"Status": "Status",
|
||||
"Loading": "Laden...",
|
||||
"Exit Paragraph Mode": "Alineamodus verlaten",
|
||||
"Paragraph Mode": "Alineamodus",
|
||||
"Embedding Model": "Insluitmodel",
|
||||
"{{count}} book(s) synced_one": "{{count}} boek gesynchroniseerd",
|
||||
"{{count}} book(s) synced_other": "{{count}} boeken gesynchroniseerd",
|
||||
"Unable to start RSVP": "Kan RSVP niet starten",
|
||||
"RSVP not supported for PDF": "RSVP niet ondersteund voor PDF",
|
||||
"Select Chapter": "Hoofdstuk selecteren",
|
||||
"Context": "Context",
|
||||
"Ready": "Klaar",
|
||||
"Chapter Progress": "Hoofdstukvoortgang",
|
||||
"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",
|
||||
"Choose where to start reading": "Kies waar u wilt beginnen met lezen",
|
||||
"From Chapter Start": "Vanaf begin hoofdstuk",
|
||||
"Start reading from the beginning of the chapter": "Begin met lezen aan het begin van het hoofdstuk",
|
||||
"Resume": "Hervatten",
|
||||
"Continue from where you left off": "Ga verder waar u was gebleven",
|
||||
"From Current Page": "Vanaf huidige pagina",
|
||||
"Start from where you are currently reading": "Begin waar u momenteel leest",
|
||||
"From Selection": "Vanaf selectie",
|
||||
"Speed Reading Mode": "Snelle leesmodus",
|
||||
"Scroll left": "Naar links scrollen",
|
||||
"Scroll right": "Naar rechts scrollen",
|
||||
"Library Sync Progress": "Voortgang bibliotheeksynchronisatie",
|
||||
"Back to library": "Terug naar bibliotheek",
|
||||
"Group by...": "Groeperen op...",
|
||||
"Export as Plain Text": "Exporteren als platte tekst",
|
||||
"Export as Markdown": "Exporteren als Markdown",
|
||||
"Show Page Navigation Buttons": "Navigatieknoppen",
|
||||
"Page {{number}}": "Pagina {{number}}",
|
||||
"highlight": "markering",
|
||||
"underline": "onderstreping",
|
||||
"squiggly": "gegolfd",
|
||||
"red": "rood",
|
||||
"violet": "paars",
|
||||
"blue": "blauw",
|
||||
"green": "groen",
|
||||
"yellow": "geel",
|
||||
"Select {{style}} style": "Selecteer {{style}} stijl",
|
||||
"Select {{color}} color": "Selecteer {{color}} kleur",
|
||||
"Close Book": "Boek sluiten",
|
||||
"Speed Reading": "Snel lezen",
|
||||
"Close Speed Reading": "Snel lezen sluiten",
|
||||
"Authors": "Auteurs",
|
||||
"Books": "Boeken",
|
||||
"Groups": "Groepen",
|
||||
"Back to TTS Location": "Terug naar TTS-locatie",
|
||||
"Metadata": "Metadata",
|
||||
"Image viewer": "Afbeeldingenviewer",
|
||||
"Previous Image": "Vorige afbeelding",
|
||||
"Next Image": "Volgende afbeelding",
|
||||
"Zoomed": "Gezoomd",
|
||||
"Zoom level": "Zoomniveau",
|
||||
"Table viewer": "Tabelviewer",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Kan geen verbinding maken met Readwise. Controleer uw netwerkverbinding.",
|
||||
"Invalid Readwise access token": "Ongeldige Readwise-toegangstoken",
|
||||
"Disconnected from Readwise": "Verbinding met Readwise verbroken",
|
||||
"Never": "Nooit",
|
||||
"Readwise Settings": "Readwise-instellingen",
|
||||
"Connected to Readwise": "Verbonden met Readwise",
|
||||
"Last synced: {{time}}": "Laatst gesynchroniseerd: {{time}}",
|
||||
"Sync Enabled": "Synchronisatie ingeschakeld",
|
||||
"Disconnect": "Verbinding verbreken",
|
||||
"Connect your Readwise account to sync highlights.": "Verbind uw Readwise-account om markeringen te synchroniseren.",
|
||||
"Get your access token at": "Verkrijg uw toegangstoken op",
|
||||
"Access Token": "Toegangstoken",
|
||||
"Paste your Readwise access token": "Plak uw Readwise-toegangstoken",
|
||||
"Config": "Configuratie",
|
||||
"Readwise Sync": "Readwise-synchronisatie",
|
||||
"Push Highlights": "Markeringen verzenden",
|
||||
"Highlights synced to Readwise": "Markeringen gesynchroniseerd met Readwise",
|
||||
"Readwise sync failed: no internet connection": "Readwise-synchronisatie mislukt: geen internetverbinding",
|
||||
"Readwise sync failed: {{error}}": "Readwise-synchronisatie mislukt: {{error}}",
|
||||
"System Screen Brightness": "Systeemhelderheid van het scherm",
|
||||
"Page:": "Pagina:",
|
||||
"Page: {{number}}": "Pagina: {{number}}",
|
||||
"Annotation page number": "Annotatiepaginanummer"
|
||||
}
|
||||
|
||||
@@ -341,10 +341,10 @@
|
||||
"Fit": "Pasuje",
|
||||
"Reset {{settings}}": "Zresetuj {{settings}}",
|
||||
"Reset Settings": "Zresetuj ustawienia",
|
||||
"{{count}} pages left in chapter_one": "Pozostała {{count}} strona w rozdziale",
|
||||
"{{count}} pages left in chapter_few": "Pozostały {{count}} strony w rozdziale",
|
||||
"{{count}} pages left in chapter_many": "Pozostało {{count}} stron w rozdziale",
|
||||
"{{count}} pages left in chapter_other": "Pozostało {{count}} stron w rozdziale",
|
||||
"{{count}} pages left in chapter_one": "<1>Pozostała </1><0>{{count}}</0><1> strona w rozdziale</1>",
|
||||
"{{count}} pages left in chapter_few": "<1>Pozostały </1><0>{{count}}</0><1> strony w rozdziale</1>",
|
||||
"{{count}} pages left in chapter_many": "<1>Pozostało </1><0>{{count}}</0><1> stron w rozdziale</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Pozostało </1><0>{{count}}</0><1> stron w rozdziale</1>",
|
||||
"Show Remaining Pages": "Pokaż pozostałe strony",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -607,11 +607,10 @@
|
||||
"Size": "Rozmiar",
|
||||
"Cover": "Okładka",
|
||||
"Contain": "Zawierać",
|
||||
"{{number}} pages left in chapter": "Pozostało {{number}} stron w rozdziale",
|
||||
"{{number}} pages left in chapter": "<1>Pozostało </1><0>{{number}}</0><1> stron w rozdziale</1>",
|
||||
"Device": "Urządzenie",
|
||||
"E-Ink Mode": "Tryb E-Ink",
|
||||
"Highlight Colors": "Kolory wyróżnień",
|
||||
"Auto Screen Brightness": "Automatyczna jasność ekranu",
|
||||
"Pagination": "Stronicowanie",
|
||||
"Disable Double Tap": "Wyłącz podwójne dotknięcie",
|
||||
"Tap to Paginate": "Dotknij, aby stronicować",
|
||||
@@ -852,7 +851,6 @@
|
||||
"Show Results": "Pokaż wyniki",
|
||||
"Clear search": "Wyczyść wyszukiwanie",
|
||||
"Clear search history": "Wyczyść historię wyszukiwania",
|
||||
"Quick action disabled": "Szybka akcja wyłączona",
|
||||
"Tap to Toggle Footer": "Dotknij, aby przełączyć stopkę",
|
||||
"Exported successfully": "Wyeksportowano pomyślnie",
|
||||
"Book exported successfully.": "Książka została wyeksportowana pomyślnie.",
|
||||
@@ -895,7 +893,6 @@
|
||||
"Array of annotations": "Tablica adnotacji",
|
||||
"Highlighted text": "Zaznaczony tekst",
|
||||
"Annotation note": "Notatka adnotacji",
|
||||
"Update time": "Czas aktualizacji",
|
||||
"Date Format Tokens:": "Tokeny formatu daty:",
|
||||
"Year (4 digits)": "Rok (4 cyfry)",
|
||||
"Month (01-12)": "Miesiąc (01-12)",
|
||||
@@ -910,5 +907,181 @@
|
||||
"Select Voice": "Wybierz głos",
|
||||
"Toggle Sticky Bottom TTS Bar": "Przełącz przypiętą dolną belkę TTS",
|
||||
"Display what I'm reading on Discord": "Wyświetl to, co czytam na Discord",
|
||||
"Show on Discord": "Pokaż na Discord"
|
||||
"Show on Discord": "Pokaż na Discord",
|
||||
"Instant {{action}}": "Natychmiastowe {{action}}",
|
||||
"Instant {{action}} Disabled": "Natychmiastowe {{action}} wyłączone",
|
||||
"Annotation": "Adnotacja",
|
||||
"Reset Template": "Resetuj szablon",
|
||||
"Annotation style": "Styl adnotacji",
|
||||
"Annotation color": "Kolor adnotacji",
|
||||
"Annotation time": "Czas adnotacji",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Czy na pewno chcesz ponownie zaindeksować tę książkę?",
|
||||
"Enable AI in Settings": "Włącz AI w ustawieniach",
|
||||
"Index This Book": "Zaindeksuj tę książkę",
|
||||
"Enable AI search and chat for this book": "Włącz wyszukiwanie AI i czat dla tej książki",
|
||||
"Start Indexing": "Rozpocznij indeksowanie",
|
||||
"Indexing book...": "Indeksowanie książki...",
|
||||
"Preparing...": "Przygotowywanie...",
|
||||
"Delete this conversation?": "Usunąć tę rozmowę?",
|
||||
"No conversations yet": "Brak rozmów",
|
||||
"Start a new chat to ask questions about this book": "Rozpocznij nowy czat, aby zadać pytania o tę książkę",
|
||||
"Rename": "Zmień nazwę",
|
||||
"New Chat": "Nowy czat",
|
||||
"Chat": "Czat",
|
||||
"Please enter a model ID": "Wprowadź ID modelu",
|
||||
"Model not available or invalid": "Model niedostępny lub nieprawidłowy",
|
||||
"Failed to validate model": "Nie udało się zwalidować modelu",
|
||||
"Couldn't connect to Ollama. Is it running?": "Nie można połączyć się z Ollama. Czy jest uruchomiona?",
|
||||
"Invalid API key or connection failed": "Nieprawidłowy klucz API lub błąd połączenia",
|
||||
"Connection failed": "Połączenie nieudane",
|
||||
"AI Assistant": "Asystent AI",
|
||||
"Enable AI Assistant": "Włącz asystenta AI",
|
||||
"Provider": "Dostawca",
|
||||
"Ollama (Local)": "Ollama (Lokalny)",
|
||||
"AI Gateway (Cloud)": "Brama AI (Chmura)",
|
||||
"Ollama Configuration": "Konfiguracja Ollama",
|
||||
"Refresh Models": "Odśwież modele",
|
||||
"AI Model": "Model AI",
|
||||
"No models detected": "Nie wykryto modeli",
|
||||
"AI Gateway Configuration": "Konfiguracja bramy AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Wybierz spośród wysokiej jakości, ekonomicznych modeli AI. Możesz również użyć własnego modelu, wybierając \"Model niestandardowy\" poniżej.",
|
||||
"API Key": "Klucz API",
|
||||
"Get Key": "Pobierz klucz",
|
||||
"Model": "Model",
|
||||
"Custom Model...": "Model niestandardowy...",
|
||||
"Custom Model ID": "ID modelu niestandardowego",
|
||||
"Validate": "Waliduj",
|
||||
"Model available": "Model dostępny",
|
||||
"Connection": "Połączenie",
|
||||
"Test Connection": "Testuj połączenie",
|
||||
"Connected": "Połączono",
|
||||
"Custom Colors": "Niestandardowe kolory",
|
||||
"Color E-Ink Mode": "Tryb kolorowego e-papieru",
|
||||
"Reading Ruler": "Linijka do czytania",
|
||||
"Enable Reading Ruler": "Włącz linijkę do czytania",
|
||||
"Lines to Highlight": "Linie do wyróżnienia",
|
||||
"Ruler Color": "Kolor linijki",
|
||||
"Command Palette": "Paleta poleceń",
|
||||
"Search settings and actions...": "Szukaj ustawień i akcji...",
|
||||
"No results found for": "Brak wyników dla",
|
||||
"Type to search settings and actions": "Wpisz, aby szukać ustawień i akcji",
|
||||
"Recent": "Ostatnie",
|
||||
"navigate": "nawiguj",
|
||||
"select": "wybierz",
|
||||
"close": "zamknij",
|
||||
"Search Settings": "Szukaj ustawień",
|
||||
"Page Margins": "Marginesy strony",
|
||||
"AI Provider": "Dostawca AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Akcje",
|
||||
"Navigation": "Nawigacja",
|
||||
"Set status for {{count}} book(s)_one": "Ustaw status dla {{count}} książki",
|
||||
"Set status for {{count}} book(s)_other": "Ustaw status dla {{count}} książek",
|
||||
"Mark as Unread": "Oznacz jako nieprzeczytane",
|
||||
"Mark as Finished": "Oznacz jako ukończone",
|
||||
"Finished": "Ukończone",
|
||||
"Unread": "Nieprzeczytane",
|
||||
"Clear Status": "Wyczyść status",
|
||||
"Status": "Status",
|
||||
"Set status for {{count}} book(s)_few": "Ustaw status dla {{count}} książek",
|
||||
"Set status for {{count}} book(s)_many": "Ustaw status dla {{count}} książek",
|
||||
"Loading": "Ładowanie...",
|
||||
"Exit Paragraph Mode": "Wyjdź z trybu akapitu",
|
||||
"Paragraph Mode": "Tryb akapitu",
|
||||
"Embedding Model": "Model osadzania",
|
||||
"{{count}} book(s) synced_one": "{{count}} książka zsynchronizowana",
|
||||
"{{count}} book(s) synced_few": "{{count}} książki zsynchronizowane",
|
||||
"{{count}} book(s) synced_many": "{{count}} książek zsynchronizowanych",
|
||||
"{{count}} book(s) synced_other": "{{count}} książek zsynchronizowanych",
|
||||
"Unable to start RSVP": "Nie można uruchomić RSVP",
|
||||
"RSVP not supported for PDF": "RSVP nie jest obsługiwane dla PDF",
|
||||
"Select Chapter": "Wybierz rozdział",
|
||||
"Context": "Kontekst",
|
||||
"Ready": "Gotowy",
|
||||
"Chapter Progress": "Postęp rozdziału",
|
||||
"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",
|
||||
"Choose where to start reading": "Wybierz, gdzie zacząć czytać",
|
||||
"From Chapter Start": "Od początku rozdziału",
|
||||
"Start reading from the beginning of the chapter": "Zacznij czytać od początku rozdziału",
|
||||
"Resume": "Wznów",
|
||||
"Continue from where you left off": "Kontynuuj od miejsca, w którym przerwałeś",
|
||||
"From Current Page": "Od bieżącej strony",
|
||||
"Start from where you are currently reading": "Zacznij od miejsca, w którym aktualnie czytasz",
|
||||
"From Selection": "Z zaznaczenia",
|
||||
"Speed Reading Mode": "Tryb szybkiego czytania",
|
||||
"Scroll left": "Przewiń w lewo",
|
||||
"Scroll right": "Przewiń w prawo",
|
||||
"Library Sync Progress": "Postęp synchronizacji biblioteki",
|
||||
"Back to library": "Powrót do biblioteki",
|
||||
"Group by...": "Grupuj według...",
|
||||
"Export as Plain Text": "Eksportuj jako zwykły tekst",
|
||||
"Export as Markdown": "Eksportuj jako Markdown",
|
||||
"Show Page Navigation Buttons": "Przyciski nawigacji",
|
||||
"Page {{number}}": "Strona {{number}}",
|
||||
"highlight": "wyróżnienie",
|
||||
"underline": "podkreślenie",
|
||||
"squiggly": "wężyk",
|
||||
"red": "czerwony",
|
||||
"violet": "fioletowy",
|
||||
"blue": "niebieski",
|
||||
"green": "zielony",
|
||||
"yellow": "żółty",
|
||||
"Select {{style}} style": "Wybierz styl {{style}}",
|
||||
"Select {{color}} color": "Wybierz kolor {{color}}",
|
||||
"Close Book": "Zamknij książkę",
|
||||
"Speed Reading": "Szybkie czytanie",
|
||||
"Close Speed Reading": "Zamknij szybkie czytanie",
|
||||
"Authors": "Autorzy",
|
||||
"Books": "Książki",
|
||||
"Groups": "Grupy",
|
||||
"Back to TTS Location": "Powrót do lokalizacji TTS",
|
||||
"Metadata": "Metadane",
|
||||
"Image viewer": "Przeglądarka obrazów",
|
||||
"Previous Image": "Poprzedni obraz",
|
||||
"Next Image": "Następny obraz",
|
||||
"Zoomed": "Powiększone",
|
||||
"Zoom level": "Poziom powiększenia",
|
||||
"Table viewer": "Przeglądarka tabel",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Nie można połączyć się z Readwise. Sprawdź połączenie sieciowe.",
|
||||
"Invalid Readwise access token": "Nieprawidłowy token dostępu Readwise",
|
||||
"Disconnected from Readwise": "Rozłączono z Readwise",
|
||||
"Never": "Nigdy",
|
||||
"Readwise Settings": "Ustawienia Readwise",
|
||||
"Connected to Readwise": "Połączono z Readwise",
|
||||
"Last synced: {{time}}": "Ostatnia synchronizacja: {{time}}",
|
||||
"Sync Enabled": "Synchronizacja włączona",
|
||||
"Disconnect": "Rozłącz",
|
||||
"Connect your Readwise account to sync highlights.": "Połącz swoje konto Readwise, aby synchronizować wyróżnienia.",
|
||||
"Get your access token at": "Pobierz token dostępu pod adresem",
|
||||
"Access Token": "Token dostępu",
|
||||
"Paste your Readwise access token": "Wklej swój token dostępu Readwise",
|
||||
"Config": "Konfiguracja",
|
||||
"Readwise Sync": "Synchronizacja Readwise",
|
||||
"Push Highlights": "Wyślij wyróżnienia",
|
||||
"Highlights synced to Readwise": "Wyróżnienia zsynchronizowane z Readwise",
|
||||
"Readwise sync failed: no internet connection": "Synchronizacja Readwise nie powiodła się: brak połączenia z Internetem",
|
||||
"Readwise sync failed: {{error}}": "Synchronizacja Readwise nie powiodła się: {{error}}",
|
||||
"System Screen Brightness": "Jasność ekranu systemowego",
|
||||
"Page:": "Strona:",
|
||||
"Page: {{number}}": "Strona: {{number}}",
|
||||
"Annotation page number": "Numer strony adnotacji"
|
||||
}
|
||||
|
||||
@@ -338,9 +338,9 @@
|
||||
"Fit": "Encaixar",
|
||||
"Reset {{settings}}": "Redefinir {{settings}}",
|
||||
"Reset Settings": "Redefinir Configurações",
|
||||
"{{count}} pages left in chapter_one": "Falta {{count}} página neste capítulo",
|
||||
"{{count}} pages left in chapter_many": "Faltam {{count}} páginas neste capítulo",
|
||||
"{{count}} pages left in chapter_other": "Faltam {{count}} páginas neste capítulo",
|
||||
"{{count}} pages left in chapter_one": "<1>Falta </1><0>{{count}}</0><1> página neste capítulo</1>",
|
||||
"{{count}} pages left in chapter_many": "<1>Faltam </1><0>{{count}}</0><1> páginas neste capítulo</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Faltam </1><0>{{count}}</0><1> páginas neste capítulo</1>",
|
||||
"Show Remaining Pages": "Mostrar páginas restantes",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -603,11 +603,10 @@
|
||||
"Size": "Tamanho",
|
||||
"Cover": "Capa",
|
||||
"Contain": "Contém",
|
||||
"{{number}} pages left in chapter": "Faltam {{number}} páginas neste capítulo",
|
||||
"{{number}} pages left in chapter": "<1>Faltam </1><0>{{number}}</0><1> páginas neste capítulo</1>",
|
||||
"Device": "Dispositivo",
|
||||
"E-Ink Mode": "Modo E-Ink",
|
||||
"Highlight Colors": "Cores de Destaque",
|
||||
"Auto Screen Brightness": "Brilho Automático da Tela",
|
||||
"Pagination": "Paginação",
|
||||
"Disable Double Tap": "Desativar Toque Duplo",
|
||||
"Tap to Paginate": "Toque para Paginar",
|
||||
@@ -842,7 +841,6 @@
|
||||
"Show Results": "Mostrar resultados",
|
||||
"Clear search": "Limpar pesquisa",
|
||||
"Clear search history": "Limpar histórico de pesquisa",
|
||||
"Quick action disabled": "Ação rápida desativada",
|
||||
"Tap to Toggle Footer": "Toque para alternar rodapé",
|
||||
"Exported successfully": "Exportado com sucesso",
|
||||
"Book exported successfully.": "Livro exportado com sucesso.",
|
||||
@@ -885,7 +883,6 @@
|
||||
"Array of annotations": "Lista de anotações",
|
||||
"Highlighted text": "Texto destacado",
|
||||
"Annotation note": "Nota de anotação",
|
||||
"Update time": "Hora de atualização",
|
||||
"Date Format Tokens:": "Tokens de formato de data:",
|
||||
"Year (4 digits)": "Ano (4 dígitos)",
|
||||
"Month (01-12)": "Mês (01-12)",
|
||||
@@ -900,5 +897,179 @@
|
||||
"Select Voice": "Selecionar voz",
|
||||
"Toggle Sticky Bottom TTS Bar": "Alternar barra TTS fixada",
|
||||
"Display what I'm reading on Discord": "Mostrar o que estou lendo no Discord",
|
||||
"Show on Discord": "Mostrar no Discord"
|
||||
"Show on Discord": "Mostrar no Discord",
|
||||
"Instant {{action}}": "{{action}} instantâneo",
|
||||
"Instant {{action}} Disabled": "{{action}} instantâneo desativado",
|
||||
"Annotation": "Anotação",
|
||||
"Reset Template": "Redefinir modelo",
|
||||
"Annotation style": "Estilo de anotação",
|
||||
"Annotation color": "Cor da anotação",
|
||||
"Annotation time": "Hora da anotação",
|
||||
"AI": "IA",
|
||||
"Are you sure you want to re-index this book?": "Tem certeza de que deseja reindexar este livro?",
|
||||
"Enable AI in Settings": "Ativar IA nas configurações",
|
||||
"Index This Book": "Indexar este livro",
|
||||
"Enable AI search and chat for this book": "Ativar pesquisa e chat com IA para este livro",
|
||||
"Start Indexing": "Iniciar indexação",
|
||||
"Indexing book...": "Indexando livro...",
|
||||
"Preparing...": "Preparando...",
|
||||
"Delete this conversation?": "Excluir esta conversa?",
|
||||
"No conversations yet": "Ainda não há conversas",
|
||||
"Start a new chat to ask questions about this book": "Inicie um novo chat para fazer perguntas sobre este livro",
|
||||
"Rename": "Renomear",
|
||||
"New Chat": "Novo chat",
|
||||
"Chat": "Chat",
|
||||
"Please enter a model ID": "Por favor, insira um ID de modelo",
|
||||
"Model not available or invalid": "Modelo não disponível ou inválido",
|
||||
"Failed to validate model": "Falha ao validar modelo",
|
||||
"Couldn't connect to Ollama. Is it running?": "Não foi possível conectar ao Ollama. Está em execução?",
|
||||
"Invalid API key or connection failed": "Chave API inválida ou falha na conexão",
|
||||
"Connection failed": "Falha na conexão",
|
||||
"AI Assistant": "Assistente de IA",
|
||||
"Enable AI Assistant": "Ativar assistente de IA",
|
||||
"Provider": "Provedor",
|
||||
"Ollama (Local)": "Ollama (Local)",
|
||||
"AI Gateway (Cloud)": "Gateway de IA (Nuvem)",
|
||||
"Ollama Configuration": "Configuração do Ollama",
|
||||
"Refresh Models": "Atualizar modelos",
|
||||
"AI Model": "Modelo de IA",
|
||||
"No models detected": "Nenhum modelo detectado",
|
||||
"AI Gateway Configuration": "Configuração do gateway de IA",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Escolha entre uma seleção de modelos de IA de alta qualidade e econômicos. Você também pode usar seu próprio modelo selecionando \"Modelo personalizado\" abaixo.",
|
||||
"API Key": "Chave API",
|
||||
"Get Key": "Obter chave",
|
||||
"Model": "Modelo",
|
||||
"Custom Model...": "Modelo personalizado...",
|
||||
"Custom Model ID": "ID do modelo personalizado",
|
||||
"Validate": "Validar",
|
||||
"Model available": "Modelo disponível",
|
||||
"Connection": "Conexão",
|
||||
"Test Connection": "Testar conexão",
|
||||
"Connected": "Conectado",
|
||||
"Custom Colors": "Cores personalizadas",
|
||||
"Color E-Ink Mode": "Modo E-Ink colorido",
|
||||
"Reading Ruler": "Régua de leitura",
|
||||
"Enable Reading Ruler": "Ativar régua de leitura",
|
||||
"Lines to Highlight": "Linhas para destacar",
|
||||
"Ruler Color": "Cor da régua",
|
||||
"Command Palette": "Paleta de Comandos",
|
||||
"Search settings and actions...": "Pesquisar definições e ações...",
|
||||
"No results found for": "Nenhum resultado encontrado para",
|
||||
"Type to search settings and actions": "Digite para pesquisar definições e ações",
|
||||
"Recent": "Recente",
|
||||
"navigate": "navegar",
|
||||
"select": "selecionar",
|
||||
"close": "fechar",
|
||||
"Search Settings": "Pesquisar Definições",
|
||||
"Page Margins": "Margens da Página",
|
||||
"AI Provider": "Fornecedor de IA",
|
||||
"Ollama URL": "URL do Ollama",
|
||||
"Ollama Model": "Modelo do Ollama",
|
||||
"AI Gateway Model": "Modelo de AI Gateway",
|
||||
"Actions": "Ações",
|
||||
"Navigation": "Navegação",
|
||||
"Set status for {{count}} book(s)_one": "Definir estado para {{count}} livro",
|
||||
"Set status for {{count}} book(s)_other": "Definir estado para {{count}} livros",
|
||||
"Mark as Unread": "Marcar como não lido",
|
||||
"Mark as Finished": "Marcar como concluído",
|
||||
"Finished": "Concluído",
|
||||
"Unread": "Não lido",
|
||||
"Clear Status": "Limpar estado",
|
||||
"Status": "Estado",
|
||||
"Set status for {{count}} book(s)_many": "Definir status para {{count}} livros",
|
||||
"Loading": "Carregando...",
|
||||
"Exit Paragraph Mode": "Sair do modo de parágrafo",
|
||||
"Paragraph Mode": "Modo de parágrafo",
|
||||
"Embedding Model": "Modelo de incorporação",
|
||||
"{{count}} book(s) synced_one": "{{count}} livro sincronizado",
|
||||
"{{count}} book(s) synced_many": "{{count}} livros sincronizados",
|
||||
"{{count}} book(s) synced_other": "{{count}} livros sincronizados",
|
||||
"Unable to start RSVP": "Não foi possível iniciar o RSVP",
|
||||
"RSVP not supported for PDF": "RSVP não suportado para PDF",
|
||||
"Select Chapter": "Selecionar capítulo",
|
||||
"Context": "Contexto",
|
||||
"Ready": "Pronto",
|
||||
"Chapter Progress": "Progresso do capítulo",
|
||||
"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",
|
||||
"Choose where to start reading": "Escolha onde começar a ler",
|
||||
"From Chapter Start": "Do início do capítulo",
|
||||
"Start reading from the beginning of the chapter": "Começar a ler do início do capítulo",
|
||||
"Resume": "Retomar",
|
||||
"Continue from where you left off": "Continuar de onde parou",
|
||||
"From Current Page": "Da página atual",
|
||||
"Start from where you are currently reading": "Começar de onde está lendo no momento",
|
||||
"From Selection": "Da seleção",
|
||||
"Speed Reading Mode": "Modo de leitura rápida",
|
||||
"Scroll left": "Rolar para a esquerda",
|
||||
"Scroll right": "Rolar para a direita",
|
||||
"Library Sync Progress": "Progresso de sincronização da biblioteca",
|
||||
"Back to library": "Voltar à biblioteca",
|
||||
"Group by...": "Agrupar por...",
|
||||
"Export as Plain Text": "Exportar como texto simples",
|
||||
"Export as Markdown": "Exportar como Markdown",
|
||||
"Show Page Navigation Buttons": "Botões de navegação",
|
||||
"Page {{number}}": "Página {{number}}",
|
||||
"highlight": "destaque",
|
||||
"underline": "sublinhado",
|
||||
"squiggly": "ondulado",
|
||||
"red": "vermelho",
|
||||
"violet": "violeta",
|
||||
"blue": "azul",
|
||||
"green": "verde",
|
||||
"yellow": "amarelo",
|
||||
"Select {{style}} style": "Selecionar estilo {{style}}",
|
||||
"Select {{color}} color": "Selecionar cor {{color}}",
|
||||
"Close Book": "Fechar livro",
|
||||
"Speed Reading": "Leitura rápida",
|
||||
"Close Speed Reading": "Fechar leitura rápida",
|
||||
"Authors": "Autores",
|
||||
"Books": "Livros",
|
||||
"Groups": "Grupos",
|
||||
"Back to TTS Location": "Voltar para a localização do TTS",
|
||||
"Metadata": "Metadados",
|
||||
"Image viewer": "Visualizador de imagens",
|
||||
"Previous Image": "Imagem anterior",
|
||||
"Next Image": "Próxima imagem",
|
||||
"Zoomed": "Com zoom",
|
||||
"Zoom level": "Nível de zoom",
|
||||
"Table viewer": "Visualizador de tabelas",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Não foi possível conectar ao Readwise. Por favor, verifique sua conexão de rede.",
|
||||
"Invalid Readwise access token": "Token de acesso do Readwise inválido",
|
||||
"Disconnected from Readwise": "Desconectado do Readwise",
|
||||
"Never": "Nunca",
|
||||
"Readwise Settings": "Configurações do Readwise",
|
||||
"Connected to Readwise": "Conectado ao Readwise",
|
||||
"Last synced: {{time}}": "Última sincronização: {{time}}",
|
||||
"Sync Enabled": "Sincronização ativada",
|
||||
"Disconnect": "Desconectar",
|
||||
"Connect your Readwise account to sync highlights.": "Conecte sua conta do Readwise para sincronizar os destaques.",
|
||||
"Get your access token at": "Obtenha seu token de acesso em",
|
||||
"Access Token": "Token de acesso",
|
||||
"Paste your Readwise access token": "Cole seu token de acesso do Readwise",
|
||||
"Config": "Configuração",
|
||||
"Readwise Sync": "Sincronização com o Readwise",
|
||||
"Push Highlights": "Enviar destaques",
|
||||
"Highlights synced to Readwise": "Destaques sincronizados com o Readwise",
|
||||
"Readwise sync failed: no internet connection": "Sincronização com o Readwise falhou: sem conexão com a internet",
|
||||
"Readwise sync failed: {{error}}": "Sincronização com o Readwise falhou: {{error}}",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -341,10 +341,10 @@
|
||||
"Fit": "Подогнать",
|
||||
"Reset {{settings}}": "Сбросить {{settings}}",
|
||||
"Reset Settings": "Сбросить настройки",
|
||||
"{{count}} pages left in chapter_one": "Осталась {{count}} страница в главе",
|
||||
"{{count}} pages left in chapter_few": "Осталось {{count}} страницы в главе",
|
||||
"{{count}} pages left in chapter_many": "Осталось {{count}} страниц в главе",
|
||||
"{{count}} pages left in chapter_other": "Осталось {{count}} страниц в главе",
|
||||
"{{count}} pages left in chapter_one": "<1>Осталась </1><0>{{count}}</0><1> страница в главе</1>",
|
||||
"{{count}} pages left in chapter_few": "<1>Осталось </1><0>{{count}}</0><1> страницы в главе</1>",
|
||||
"{{count}} pages left in chapter_many": "<1>Осталось </1><0>{{count}}</0><1> страниц в главе</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Осталось </1><0>{{count}}</0><1> страниц в главе</1>",
|
||||
"Show Remaining Pages": "Показать оставшиеся страницы",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -607,11 +607,10 @@
|
||||
"Size": "Размер",
|
||||
"Cover": "Обложка",
|
||||
"Contain": "Содержать",
|
||||
"{{number}} pages left in chapter": "Осталось {{number}} страниц в главе",
|
||||
"{{number}} pages left in chapter": "<1>Осталось </1><0>{{number}}</0><1> страниц в главе</1>",
|
||||
"Device": "Устройство",
|
||||
"E-Ink Mode": "E-Ink режим",
|
||||
"Highlight Colors": "Цвета выделения",
|
||||
"Auto Screen Brightness": "Автояркость экрана",
|
||||
"Pagination": "Пагинация",
|
||||
"Disable Double Tap": "Отключить двойной тап",
|
||||
"Tap to Paginate": "Тапните, чтобы разбить на страницы",
|
||||
@@ -852,7 +851,6 @@
|
||||
"Show Results": "Показать результаты",
|
||||
"Clear search": "Очистить поиск",
|
||||
"Clear search history": "Очистить историю поиска",
|
||||
"Quick action disabled": "Быстрое действие отключено",
|
||||
"Tap to Toggle Footer": "Нажмите для переключения нижнего колонтитула",
|
||||
"Exported successfully": "Успешно экспортировано",
|
||||
"Book exported successfully.": "Книга успешно экспортирована.",
|
||||
@@ -895,7 +893,6 @@
|
||||
"Array of annotations": "Массив примечаний",
|
||||
"Highlighted text": "Выделенный текст",
|
||||
"Annotation note": "Примечание аннотации",
|
||||
"Update time": "Время обновления",
|
||||
"Date Format Tokens:": "Токены формата даты:",
|
||||
"Year (4 digits)": "Год (4 цифры)",
|
||||
"Month (01-12)": "Месяц (01-12)",
|
||||
@@ -910,5 +907,181 @@
|
||||
"Select Voice": "Выбрать голос",
|
||||
"Toggle Sticky Bottom TTS Bar": "Переключить закреплённую панель TTS",
|
||||
"Display what I'm reading on Discord": "Показывать книгу в Discord",
|
||||
"Show on Discord": "Показать в Discord"
|
||||
"Show on Discord": "Показать в Discord",
|
||||
"Instant {{action}}": "Мгновенное {{action}}",
|
||||
"Instant {{action}} Disabled": "Мгновенное {{action}} отключено",
|
||||
"Annotation": "Аннотация",
|
||||
"Reset Template": "Сбросить шаблон",
|
||||
"Annotation style": "Стиль аннотации",
|
||||
"Annotation color": "Цвет аннотации",
|
||||
"Annotation time": "Время аннотации",
|
||||
"AI": "ИИ",
|
||||
"Are you sure you want to re-index this book?": "Вы уверены, что хотите переиндексировать эту книгу?",
|
||||
"Enable AI in Settings": "Включить ИИ в настройках",
|
||||
"Index This Book": "Индексировать эту книгу",
|
||||
"Enable AI search and chat for this book": "Включить ИИ-поиск и чат для этой книги",
|
||||
"Start Indexing": "Начать индексацию",
|
||||
"Indexing book...": "Индексация книги...",
|
||||
"Preparing...": "Подготовка...",
|
||||
"Delete this conversation?": "Удалить этот разговор?",
|
||||
"No conversations yet": "Пока нет разговоров",
|
||||
"Start a new chat to ask questions about this book": "Начните новый чат, чтобы задать вопросы об этой книге",
|
||||
"Rename": "Переименовать",
|
||||
"New Chat": "Новый чат",
|
||||
"Chat": "Чат",
|
||||
"Please enter a model ID": "Пожалуйста, введите ID модели",
|
||||
"Model not available or invalid": "Модель недоступна или недействительна",
|
||||
"Failed to validate model": "Не удалось проверить модель",
|
||||
"Couldn't connect to Ollama. Is it running?": "Не удалось подключиться к Ollama. Он запущен?",
|
||||
"Invalid API key or connection failed": "Недействительный API-ключ или ошибка подключения",
|
||||
"Connection failed": "Ошибка подключения",
|
||||
"AI Assistant": "ИИ-ассистент",
|
||||
"Enable AI Assistant": "Включить ИИ-ассистента",
|
||||
"Provider": "Провайдер",
|
||||
"Ollama (Local)": "Ollama (Локальный)",
|
||||
"AI Gateway (Cloud)": "ИИ-шлюз (Облако)",
|
||||
"Ollama Configuration": "Настройка Ollama",
|
||||
"Refresh Models": "Обновить модели",
|
||||
"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.": "Выберите из высококачественных, экономичных ИИ-моделей. Вы также можете использовать свою модель, выбрав \"Пользовательская модель\" ниже.",
|
||||
"API Key": "API-ключ",
|
||||
"Get Key": "Получить ключ",
|
||||
"Model": "Модель",
|
||||
"Custom Model...": "Пользовательская модель...",
|
||||
"Custom Model ID": "ID пользовательской модели",
|
||||
"Validate": "Проверить",
|
||||
"Model available": "Модель доступна",
|
||||
"Connection": "Соединение",
|
||||
"Test Connection": "Проверить соединение",
|
||||
"Connected": "Подключено",
|
||||
"Custom Colors": "Пользовательские цвета",
|
||||
"Color E-Ink Mode": "Режим цветных электронных чернил",
|
||||
"Reading Ruler": "Линейка для чтения",
|
||||
"Enable Reading Ruler": "Включить линейку для чтения",
|
||||
"Lines to Highlight": "Строк для выделения",
|
||||
"Ruler Color": "Цвет линейки",
|
||||
"Command Palette": "Палитра команд",
|
||||
"Search settings and actions...": "Поиск настроек и действий...",
|
||||
"No results found for": "Результатов не найдено для",
|
||||
"Type to search settings and actions": "Введите для поиска настроек и действий",
|
||||
"Recent": "Недавние",
|
||||
"navigate": "навигация",
|
||||
"select": "выбрать",
|
||||
"close": "закрыть",
|
||||
"Search Settings": "Поиск настроек",
|
||||
"Page Margins": "Поля страницы",
|
||||
"AI Provider": "Провайдер ИИ",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Модель Ollama",
|
||||
"AI Gateway Model": "Модель AI Gateway",
|
||||
"Actions": "Действия",
|
||||
"Navigation": "Навигация",
|
||||
"Set status for {{count}} book(s)_one": "Установить статус для {{count}} книги",
|
||||
"Set status for {{count}} book(s)_other": "Установить статус для {{count}} книг",
|
||||
"Mark as Unread": "Отметить как непрочитанное",
|
||||
"Mark as Finished": "Отметить как прочитанное",
|
||||
"Finished": "Прочитано",
|
||||
"Unread": "Не прочитано",
|
||||
"Clear Status": "Очистить статус",
|
||||
"Status": "Статус",
|
||||
"Set status for {{count}} book(s)_few": "Установить статус для {{count}} книг",
|
||||
"Set status for {{count}} book(s)_many": "Установить статус для {{count}} книг",
|
||||
"Loading": "Загрузка...",
|
||||
"Exit Paragraph Mode": "Выйти из режима абзаца",
|
||||
"Paragraph Mode": "Режим абзаца",
|
||||
"Embedding Model": "Модель встраивания",
|
||||
"{{count}} book(s) synced_one": "{{count}} книга синхронизирована",
|
||||
"{{count}} book(s) synced_few": "{{count}} книги синхронизированы",
|
||||
"{{count}} book(s) synced_many": "{{count}} книг синхронизировано",
|
||||
"{{count}} book(s) synced_other": "{{count}} книг синхронизировано",
|
||||
"Unable to start RSVP": "Не удалось запустить RSVP",
|
||||
"RSVP not supported for PDF": "RSVP не поддерживается для PDF",
|
||||
"Select Chapter": "Выбрать главу",
|
||||
"Context": "Контекст",
|
||||
"Ready": "Готово",
|
||||
"Chapter Progress": "Прогресс главы",
|
||||
"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",
|
||||
"Choose where to start reading": "Выберите, где начать чтение",
|
||||
"From Chapter Start": "С начала главы",
|
||||
"Start reading from the beginning of the chapter": "Начать чтение с начала главы",
|
||||
"Resume": "Продолжить",
|
||||
"Continue from where you left off": "Продолжить с того места, где вы остановились",
|
||||
"From Current Page": "С текущей страницы",
|
||||
"Start from where you are currently reading": "Начать с того места, где вы сейчас читаете",
|
||||
"From Selection": "Из выбранного",
|
||||
"Speed Reading Mode": "Режим быстрого чтения",
|
||||
"Scroll left": "Прокрутить влево",
|
||||
"Scroll right": "Прокрутить вправо",
|
||||
"Library Sync Progress": "Прогресс синхронизации библиотеки",
|
||||
"Back to library": "Назад в библиотеку",
|
||||
"Group by...": "Группировать по...",
|
||||
"Export as Plain Text": "Экспортировать как обычный текст",
|
||||
"Export as Markdown": "Экспортировать как Markdown",
|
||||
"Show Page Navigation Buttons": "Кнопки навигации",
|
||||
"Page {{number}}": "Страница {{number}}",
|
||||
"highlight": "выделение",
|
||||
"underline": "подчеркивание",
|
||||
"squiggly": "волнистая линия",
|
||||
"red": "красный",
|
||||
"violet": "фиолетовый",
|
||||
"blue": "синий",
|
||||
"green": "зеленый",
|
||||
"yellow": "желтый",
|
||||
"Select {{style}} style": "Выбрать стиль {{style}}",
|
||||
"Select {{color}} color": "Выбрать цвет {{color}}",
|
||||
"Close Book": "Закрыть книгу",
|
||||
"Speed Reading": "Скорочтение",
|
||||
"Close Speed Reading": "Закрыть скорочтение",
|
||||
"Authors": "Авторы",
|
||||
"Books": "Книги",
|
||||
"Groups": "Группы",
|
||||
"Back to TTS Location": "Вернуться к местоположению TTS",
|
||||
"Metadata": "Метаданные",
|
||||
"Image viewer": "Просмотрщик изображений",
|
||||
"Previous Image": "Предыдущее изображение",
|
||||
"Next Image": "Следующее изображение",
|
||||
"Zoomed": "Увеличено",
|
||||
"Zoom level": "Уровень масштаба",
|
||||
"Table viewer": "Просмотрщик таблиц",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Не удалось подключиться к Readwise. Пожалуйста, проверьте ваше сетевое соединение.",
|
||||
"Invalid Readwise access token": "Недействительный токен доступа Readwise",
|
||||
"Disconnected from Readwise": "Отключено от Readwise",
|
||||
"Never": "Никогда",
|
||||
"Readwise Settings": "Настройки Readwise",
|
||||
"Connected to Readwise": "Подключено к Readwise",
|
||||
"Last synced: {{time}}": "Последняя синхронизация: {{time}}",
|
||||
"Sync Enabled": "Синхронизация включена",
|
||||
"Disconnect": "Отключить",
|
||||
"Connect your Readwise account to sync highlights.": "Подключите ваш аккаунт Readwise для синхронизации выделений.",
|
||||
"Get your access token at": "Получите ваш токен доступа на",
|
||||
"Access Token": "Токен доступа",
|
||||
"Paste your Readwise access token": "Вставьте ваш токен доступа Readwise",
|
||||
"Config": "Конфигурация",
|
||||
"Readwise Sync": "Синхронизация Readwise",
|
||||
"Push Highlights": "Отправить выделения",
|
||||
"Highlights synced to Readwise": "Выделения синхронизированы с Readwise",
|
||||
"Readwise sync failed: no internet connection": "Синхронизация Readwise не удалась: нет интернет-соединения",
|
||||
"Readwise sync failed: {{error}}": "Синхронизация Readwise не удалась: {{error}}",
|
||||
"System Screen Brightness": "Систেমная яркость экрана",
|
||||
"Page:": "Страница:",
|
||||
"Page: {{number}}": "Страница: {{number}}",
|
||||
"Annotation page number": "Номер страницы аннотации"
|
||||
}
|
||||
|
||||
@@ -186,8 +186,8 @@
|
||||
"Add your notes here...": "මෙහි ඔබේ සටහන් එක් කරන්න...",
|
||||
"Search notes and excerpts...": "සටහන් සහ උදාහරණ සොයන්න...",
|
||||
"{{time}} min left in chapter": "පරිච්ඡේදයේ මිනිත්තු {{time}} ක් ඉතිරි",
|
||||
"{{count}} pages left in chapter_one": "පරිච්ඡේදයේ පිටුව 1 ක් ඉතිරි",
|
||||
"{{count}} pages left in chapter_other": "පරිච්ඡේදයේ පිටු {{count}} ක් ඉතිරි",
|
||||
"{{count}} pages left in chapter_one": "<1>පරිච්ඡේදයේ පිටුව 1 ක් ඉතිරි</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>පරිච්ඡේදයේ පිටු </1><0>{{count}}</0><1> ක් ඉතිරි</1>",
|
||||
"Theme Mode": "තේමා ආකාරය",
|
||||
"Invert Image In Dark Mode": "අඳුරු ආකාරයේදී රූපය පරිවර්තනය කරන්න",
|
||||
"Override Book Color": "පොතේ වර්ණය අභිබවන්න",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "ප්රමාණය",
|
||||
"Cover": "ආවරණය",
|
||||
"Contain": "අඩංගු වන්න",
|
||||
"{{number}} pages left in chapter": "පරිච්ඡේදයේ පිටු {{number}} ඉතිරිව ඇත",
|
||||
"{{number}} pages left in chapter": "<1>පරිච්ඡේදයේ පිටු </1><0>{{number}}</0><1> ඉතිරිව ඇත</1>",
|
||||
"Device": "උපකරණය",
|
||||
"E-Ink Mode": "E-Ink ආකාරය",
|
||||
"Highlight Colors": "ඉස්මතු වර්ණ",
|
||||
"Auto Screen Brightness": "ස්වයංක්රීය තිර දිදුලනුම",
|
||||
"Pagination": "පිටුගත කිරීම",
|
||||
"Disable Double Tap": "දෙවරක් ටැප් කිරීම අබල කරන්න",
|
||||
"Tap to Paginate": "පිටුගත කිරීමට ටැප් කරන්න",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "ප්රතිඵල පෙන්වන්න",
|
||||
"Clear search": "සෙවුම හිස් කරන්න",
|
||||
"Clear search history": "සෙවුම් ඉතිහාසය හිස් කරන්න",
|
||||
"Quick action disabled": "ඉක්මන් ක්රියාව අක්රීයයි",
|
||||
"Tap to Toggle Footer": "පාදකය ටොගල් කිරීමට තට්ටු කරන්න",
|
||||
"Exported successfully": "සාර්ථකව අපනයනය කරන ලදී",
|
||||
"Book exported successfully.": "පොත සාර්ථකව අපනයනය කරන ලදී.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "අනුසටහන් අරය",
|
||||
"Highlighted text": "ඉස්මතු කළ පෙළ",
|
||||
"Annotation note": "අනුසටහන් සටහන",
|
||||
"Update time": "යාවත්කාලීන කාලය",
|
||||
"Date Format Tokens:": "දිනය ආකෘති ටෝකන:",
|
||||
"Year (4 digits)": "වසර (අංක 4)",
|
||||
"Month (01-12)": "මාසය (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "හඬ තෝරන්න",
|
||||
"Toggle Sticky Bottom TTS Bar": "ඇලවූ TTS තීරුව මාරු කරන්න",
|
||||
"Display what I'm reading on Discord": "Discord හි කියවන පොත පෙන්වන්න",
|
||||
"Show on Discord": "Discord හි පෙන්වන්න"
|
||||
"Show on Discord": "Discord හි පෙන්වන්න",
|
||||
"Instant {{action}}": "ක්ෂණික {{action}}",
|
||||
"Instant {{action}} Disabled": "ක්ෂණික {{action}} අබල කර ඇත",
|
||||
"Annotation": "විවරණය",
|
||||
"Reset Template": "සැකිල්ල නැවත සකස් කරන්න",
|
||||
"Annotation style": "විවරණ විලාසය",
|
||||
"Annotation color": "විවරණ වර්ණය",
|
||||
"Annotation time": "විවරණ වේලාව",
|
||||
"AI": "කෘතිම බුද්ධිය (AI)",
|
||||
"Are you sure you want to re-index this book?": "ඔබට මෙම පොත නැවත දර්ශකය කිරීමට අවශ්ය බව සහතිකද?",
|
||||
"Enable AI in Settings": "සැකසුම් තුළ AI සක්රීය කරන්න",
|
||||
"Index This Book": "මෙම පොත දර්ශකය කරන්න",
|
||||
"Enable AI search and chat for this book": "මෙම පොත සඳහා AI සෙවීම සහ කතාබස් සක්රීය කරන්න",
|
||||
"Start Indexing": "දර්ශකය කිරීම ආරම්භ කරන්න",
|
||||
"Indexing book...": "පොත දර්ශකය කරමින්...",
|
||||
"Preparing...": "සූදානම් වෙමින්...",
|
||||
"Delete this conversation?": "මෙම සංවාදය මකා දමන්නද?",
|
||||
"No conversations yet": "තවම සංවාද නැත",
|
||||
"Start a new chat to ask questions about this book": "මෙම පොත ගැන ප්රශ්න ඇසීමට නව කතාබහක් ආරම්භ කරන්න",
|
||||
"Rename": "නම වෙනස් කරන්න",
|
||||
"New Chat": "නව කතාබහ",
|
||||
"Chat": "කතාබහ",
|
||||
"Please enter a model ID": "කරුණාකර මාදිලි හැඳුනුම්පතක් ඇතුළත් කරන්න",
|
||||
"Model not available or invalid": "මාදිලිය ලබා ගත නොහැක හෝ අවලංගුයි",
|
||||
"Failed to validate model": "මාදිලිය තහවුරු කිරීමට අසමත් විය",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama වෙත සම්බන්ධ වීමට නොහැකි විය. එය ක්රියාත්මක වේද?",
|
||||
"Invalid API key or connection failed": "අවලංගු API යතුරක් හෝ සම්බන්ධතාවය අසාර්ථක විය",
|
||||
"Connection failed": "සම්බන්ධතාවය අසාර්ථක විය",
|
||||
"AI Assistant": "AI සහායකයා",
|
||||
"Enable AI Assistant": "AI සහායකයා සක්රීය කරන්න",
|
||||
"Provider": "සපයන්නා",
|
||||
"Ollama (Local)": "Ollama (දේශීය)",
|
||||
"AI Gateway (Cloud)": "AI Gateway (Cloud)",
|
||||
"Ollama Configuration": "Ollama වින්යාසය",
|
||||
"Refresh Models": "මාදිලි යාවත්කාලීන කරන්න",
|
||||
"AI Model": "AI මාදිලිය",
|
||||
"No models detected": "කිසිදු මාදිලියක් හඳුනාගෙන නැත",
|
||||
"AI Gateway Configuration": "AI Gateway වින්යාසය",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "උසස් තත්ත්වයේ, ලාභදායී AI මාදිලි එකතුවකින් තෝරන්න. පහතින් \"අභිරුචි මාදිලිය\" තේරීමෙන් ඔබට ඔබේම මාදිලියක් ද ගෙන ඒමට හැකිය.",
|
||||
"API Key": "API යතුර",
|
||||
"Get Key": "යතුර ලබා ගන්න",
|
||||
"Model": "මාදිලිය",
|
||||
"Custom Model...": "අභිරුචි මාදිලිය...",
|
||||
"Custom Model ID": "අභිරුචි මාදිලි හැඳුනුම්පත",
|
||||
"Validate": "තහවුරු කරන්න",
|
||||
"Model available": "මාදිලිය ලබා ගත හැකිය",
|
||||
"Connection": "සම්බන්ධතාවය",
|
||||
"Test Connection": "සම්බන්ධතාවය පරීක්ෂා කරන්න",
|
||||
"Connected": "සම්බන්ධ විය",
|
||||
"Custom Colors": "අභිමත වර්ණ",
|
||||
"Color E-Ink Mode": "වර්ණ E-Ink මාදිලිය",
|
||||
"Reading Ruler": "කියවීමේ රූල",
|
||||
"Enable Reading Ruler": "කියවීමේ රූල සක්රීය කරන්න",
|
||||
"Lines to Highlight": "මතුකර පෙන්විය යුතු පේළි",
|
||||
"Ruler Color": "රූලේ වර්ණය",
|
||||
"Command Palette": "විධාන පුවරුව",
|
||||
"Search settings and actions...": "සැකසුම් සහ ක්රියා සොයන්න...",
|
||||
"No results found for": "සඳහා ප්රතිඵල හමු නොවීය",
|
||||
"Type to search settings and actions": "සැකසුම් සහ ක්රියා සෙවීමට ටයිප් කරන්න",
|
||||
"Recent": "මෑත කාලීන",
|
||||
"navigate": "සංචාලනය",
|
||||
"select": "තෝරන්න",
|
||||
"close": "වසා දමන්න",
|
||||
"Search Settings": "සැකසුම් සොයන්න",
|
||||
"Page Margins": "පිටු මායිම්",
|
||||
"AI Provider": "AI සපයන්නා",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama ආකෘතිය",
|
||||
"AI Gateway Model": "AI ਗੇਟਵੇ ਆਕෘතිය",
|
||||
"Actions": "ක්රියා",
|
||||
"Navigation": "සංචාලනය",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} පොත සඳහා තත්ත්වය සකසන්න",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} පොත් සඳහා තත්ත්වය සකසන්න",
|
||||
"Mark as Unread": "නොකියවූ ලෙස සලකුණු කරන්න",
|
||||
"Mark as Finished": "අවසන් වූ ලෙස සලකුණු කරන්න",
|
||||
"Finished": "අවසන්",
|
||||
"Unread": "නොකියවූ",
|
||||
"Clear Status": "තත්ත්වය හිස් කරන්න",
|
||||
"Status": "තත්ත්වය",
|
||||
"Loading": "පූරණය වෙමින් පවතී...",
|
||||
"Exit Paragraph Mode": "ඡේද ප්රකාරයෙන් ඉවත් වන්න",
|
||||
"Paragraph Mode": "ඡේද ප්රකාරය",
|
||||
"Embedding Model": "එබ්බවීමේ ආකෘතිය",
|
||||
"{{count}} book(s) synced_one": "{{count}} පොතක් සමමුහුර්ත කරන ලදී",
|
||||
"{{count}} book(s) synced_other": "{{count}} පොත් සමමුහුර්ත කරන ලදී",
|
||||
"Unable to start RSVP": "RSVP ආරම්භ කිරීමට නොහැක",
|
||||
"RSVP not supported for PDF": "PDF සඳහා RSVP සහාය නොදක්වයි",
|
||||
"Select Chapter": "පරිච්ඡේදය තෝරන්න",
|
||||
"Context": "සන්දර්භය",
|
||||
"Ready": "සූදානම්",
|
||||
"Chapter Progress": "පරිච්ඡේදයේ ප්රගතිය",
|
||||
"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 කියවීම ආරම්භ කරන්න",
|
||||
"Choose where to start reading": "කියවීම ආරම්භ කළ යුතු ස්ථානය තෝරන්න",
|
||||
"From Chapter Start": "පරිච්ඡේදයේ ආරම්භයේ සිට",
|
||||
"Start reading from the beginning of the chapter": "පරිච්ඡේදයේ ආරම්භයේ සිට කියවීම ආරම්භ කරන්න",
|
||||
"Resume": "නැවත ආරම්භ කරන්න",
|
||||
"Continue from where you left off": "ඔබ නතර කළ තැන සිට දිගටම කරගෙන යන්න",
|
||||
"From Current Page": "වත්මන් පිටුවේ සිට",
|
||||
"Start from where you are currently reading": "ඔබ දැනට කියවන තැනින් ආරම්භ කරන්න",
|
||||
"From Selection": "තේරීමෙන්",
|
||||
"Speed Reading Mode": "වේගයෙන් කියවීමේ ක්රමය",
|
||||
"Scroll left": "වමට අනුචලනය කරන්න",
|
||||
"Scroll right": "දකුණට අනුචලනය කරන්න",
|
||||
"Library Sync Progress": "පුස්තකාල සමමුහුර්ත කිරීමේ ප්රගතිය",
|
||||
"Back to library": "පුස්තකාලයට ආපසු",
|
||||
"Group by...": "මගින් සමූහනය කරන්න...",
|
||||
"Export as Plain Text": "සාමාන්ය පෙළ ලෙස අපනයනය කරන්න",
|
||||
"Export as Markdown": "Markdown ලෙස අපනයනය කරන්න",
|
||||
"Show Page Navigation Buttons": "සංචාලන බොත්තම්",
|
||||
"Page {{number}}": "පිටුව {{number}}",
|
||||
"highlight": "මතුකර පෙන්වීම",
|
||||
"underline": "යටින් ඉරි ඇඳීම",
|
||||
"squiggly": "zigzag",
|
||||
"red": "රතු",
|
||||
"violet": "දම්",
|
||||
"blue": "නිල්",
|
||||
"green": "කොළ",
|
||||
"yellow": "කහ",
|
||||
"Select {{style}} style": "{{style}} විලාසය තෝරන්න",
|
||||
"Select {{color}} color": "{{color}} වර්ණය තෝරන්න",
|
||||
"Close Book": "පොත වසා දමන්න",
|
||||
"Speed Reading": "වේග කියවීම",
|
||||
"Close Speed Reading": "වේග කියවීම වසන්න",
|
||||
"Authors": "කතුවරුන්",
|
||||
"Books": "පොත්",
|
||||
"Groups": "කණ්ඩායම්",
|
||||
"Back to TTS Location": "TTS ස්ථානයට ආපසු යන්න",
|
||||
"Metadata": "මෙටාඩේටා",
|
||||
"Image viewer": "රූප නරඹන්නා",
|
||||
"Previous Image": "පූර්ව රූපය",
|
||||
"Next Image": "මීළඟ රූපය",
|
||||
"Zoomed": "විශාලනය කරන ලදී",
|
||||
"Zoom level": "විශාලන මට්ටම",
|
||||
"Table viewer": "වගු නරඹන්නා",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise වෙත සම්බන්ධ වීමට නොහැක. කරුණාකර ඔබගේ ජාල සම්බන්ධතාවය පරීක්ෂා කරන්න.",
|
||||
"Invalid Readwise access token": "අවලංගු Readwise ප්රවේශ ටෝකනය",
|
||||
"Disconnected from Readwise": "Readwise වෙතින් විසන්ධි විය",
|
||||
"Never": "කවදාවත් නැහැ",
|
||||
"Readwise Settings": "Readwise සැකසුම්",
|
||||
"Connected to Readwise": "Readwise වෙත සම්බන්ධ විය",
|
||||
"Last synced: {{time}}": "අවසානයට සමමුහුර්ත කළේ: {{time}}",
|
||||
"Sync Enabled": "සමමුහුර්ත කිරීම සබල කර ඇත",
|
||||
"Disconnect": "විසන්ධි කරන්න",
|
||||
"Connect your Readwise account to sync highlights.": "විශේෂ අවස්ථා සමමුහුර්ත කිරීමට ඔබගේ Readwise ගිණුම සම්බන්ධ කරන්න.",
|
||||
"Get your access token at": "ඔබගේ ප්රවේශ ටෝකනය මෙතැනින් ලබා ගන්න",
|
||||
"Access Token": "ප්රවේශ ටෝකනය",
|
||||
"Paste your Readwise access token": "ඔබගේ Readwise ප්රවේශ ටෝකනය මෙහි අලවන්න",
|
||||
"Config": "වින්යාසය",
|
||||
"Readwise Sync": "Readwise සමමුහුර්තකරණය",
|
||||
"Push Highlights": "විශේෂ අවස්ථා යොමු කරන්න",
|
||||
"Highlights synced to Readwise": "විශේෂ අවස්ථා Readwise සමඟ සමමුහුර්ත විය",
|
||||
"Readwise sync failed: no internet connection": "Readwise සමමුහුර්තකරණය අසාර්ථක විය: අන්තර්ජාල සම්බන්ධතාවයක් නොමැත",
|
||||
"Readwise sync failed: {{error}}": "Readwise සමමුහුර්තකරණය අසාර්ථක විය: {{error}}",
|
||||
"System Screen Brightness": "පද්ධති තිර දීප්තිය",
|
||||
"Page:": "පිටුව:",
|
||||
"Page: {{number}}": "පිටුව: {{number}}",
|
||||
"Annotation page number": "විවරණ පිටු අංකය"
|
||||
}
|
||||
|
||||
@@ -234,8 +234,8 @@
|
||||
"Add your notes here...": "Lägg till dina anteckningar här...",
|
||||
"Search notes and excerpts...": "Sök anteckningar och utdrag...",
|
||||
"{{time}} min left in chapter": "{{time}} min kvar i kapitlet",
|
||||
"{{count}} pages left in chapter_one": "{{count}} sida kvar i kapitlet",
|
||||
"{{count}} pages left in chapter_other": "{{count}} sidor kvar i kapitlet",
|
||||
"{{count}} pages left in chapter_one": "<0>{{count}}</0><1> sida kvar i kapitlet</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> sidor kvar i kapitlet</1>",
|
||||
"On {{current}} of {{total}} page": "På {{current}} av {{total}} sidor",
|
||||
"Section Title": "Sektionstitel",
|
||||
"More Info": "Mer info",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "Storlek",
|
||||
"Cover": "Omslag",
|
||||
"Contain": "Innehålla",
|
||||
"{{number}} pages left in chapter": "{{number}} sidor kvar i kapitlet",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> sidor kvar i kapitlet</1>",
|
||||
"Device": "Enhet",
|
||||
"E-Ink Mode": "E-Ink-läge",
|
||||
"Highlight Colors": "Markeringsfärger",
|
||||
"Auto Screen Brightness": "Auto-skärmens ljusstyrka",
|
||||
"Pagination": "Pagination",
|
||||
"Disable Double Tap": "Inaktivera dubbeltryck",
|
||||
"Tap to Paginate": "Tryck för att bläddra",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "Visa resultat",
|
||||
"Clear search": "Rensa sökning",
|
||||
"Clear search history": "Rensa sökhistorik",
|
||||
"Quick action disabled": "Snabbåtgärd inaktiverad",
|
||||
"Tap to Toggle Footer": "Tryck för att växla sidfot",
|
||||
"Exported successfully": "Exporterad",
|
||||
"Book exported successfully.": "Boken har exporterats.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "Lista med anteckningar",
|
||||
"Highlighted text": "Markerad text",
|
||||
"Annotation note": "Anteckningsnotering",
|
||||
"Update time": "Uppdateringstid",
|
||||
"Date Format Tokens:": "Datumformattecken:",
|
||||
"Year (4 digits)": "År (4 siffror)",
|
||||
"Month (01-12)": "Månad (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "Välj röst",
|
||||
"Toggle Sticky Bottom TTS Bar": "Växla fast TTS-fält",
|
||||
"Display what I'm reading on Discord": "Visa vad jag läser på Discord",
|
||||
"Show on Discord": "Visa på Discord"
|
||||
"Show on Discord": "Visa på Discord",
|
||||
"Instant {{action}}": "Omedelbar {{action}}",
|
||||
"Instant {{action}} Disabled": "Omedelbar {{action}} inaktiverad",
|
||||
"Annotation": "Anteckning",
|
||||
"Reset Template": "Återställ mall",
|
||||
"Annotation style": "Anteckningsstil",
|
||||
"Annotation color": "Anteckningsfärg",
|
||||
"Annotation time": "Anteckningstid",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Är du säker på att du vill indexera om denna bok?",
|
||||
"Enable AI in Settings": "Aktivera AI i inställningar",
|
||||
"Index This Book": "Indexera denna bok",
|
||||
"Enable AI search and chat for this book": "Aktivera AI-sökning och chatt för denna bok",
|
||||
"Start Indexing": "Starta indexering",
|
||||
"Indexing book...": "Indexerar bok...",
|
||||
"Preparing...": "Förbereder...",
|
||||
"Delete this conversation?": "Ta bort denna konversation?",
|
||||
"No conversations yet": "Inga konversationer ännu",
|
||||
"Start a new chat to ask questions about this book": "Starta en ny chatt för att ställa frågor om denna bok",
|
||||
"Rename": "Byt namn",
|
||||
"New Chat": "Ny chatt",
|
||||
"Chat": "Chatt",
|
||||
"Please enter a model ID": "Ange ett modell-ID",
|
||||
"Model not available or invalid": "Modell ej tillgänglig eller ogiltig",
|
||||
"Failed to validate model": "Misslyckades att validera modell",
|
||||
"Couldn't connect to Ollama. Is it running?": "Kunde inte ansluta till Ollama. Körs den?",
|
||||
"Invalid API key or connection failed": "Ogiltig API-nyckel eller anslutningen misslyckades",
|
||||
"Connection failed": "Anslutningen misslyckades",
|
||||
"AI Assistant": "AI-assistent",
|
||||
"Enable AI Assistant": "Aktivera AI-assistent",
|
||||
"Provider": "Leverantör",
|
||||
"Ollama (Local)": "Ollama (Lokal)",
|
||||
"AI Gateway (Cloud)": "AI-gateway (Moln)",
|
||||
"Ollama Configuration": "Ollama-konfiguration",
|
||||
"Refresh Models": "Uppdatera modeller",
|
||||
"AI Model": "AI-modell",
|
||||
"No models detected": "Inga modeller hittades",
|
||||
"AI Gateway Configuration": "AI-gateway-konfiguration",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Välj bland ett urval av högkvalitativa, ekonomiska AI-modeller. Du kan också använda din egen modell genom att välja \"Anpassad modell\" nedan.",
|
||||
"API Key": "API-nyckel",
|
||||
"Get Key": "Hämta nyckel",
|
||||
"Model": "Modell",
|
||||
"Custom Model...": "Anpassad modell...",
|
||||
"Custom Model ID": "Anpassat modell-ID",
|
||||
"Validate": "Validera",
|
||||
"Model available": "Modell tillgänglig",
|
||||
"Connection": "Anslutning",
|
||||
"Test Connection": "Testa anslutning",
|
||||
"Connected": "Ansluten",
|
||||
"Custom Colors": "Anpassade färger",
|
||||
"Color E-Ink Mode": "Färg-E-Ink-läge",
|
||||
"Reading Ruler": "Läs-linjal",
|
||||
"Enable Reading Ruler": "Aktivera läs-linjal",
|
||||
"Lines to Highlight": "Rader att markera",
|
||||
"Ruler Color": "Linjalens färg",
|
||||
"Command Palette": "Kommandopalett",
|
||||
"Search settings and actions...": "Sök inställningar och åtgärder...",
|
||||
"No results found for": "Inga resultat hittades för",
|
||||
"Type to search settings and actions": "Skriv för att söka inställningar och åtgärder",
|
||||
"Recent": "Senaste",
|
||||
"navigate": "navigera",
|
||||
"select": "välj",
|
||||
"close": "stäng",
|
||||
"Search Settings": "Sök inställningar",
|
||||
"Page Margins": "Sidmarginaler",
|
||||
"AI Provider": "AI-leverantör",
|
||||
"Ollama URL": "Ollama-URL",
|
||||
"Ollama Model": "Ollama-modell",
|
||||
"AI Gateway Model": "AI Gateway-modell",
|
||||
"Actions": "Åtgärder",
|
||||
"Navigation": "Navigering",
|
||||
"Set status for {{count}} book(s)_one": "Ange status för {{count}} bok",
|
||||
"Set status for {{count}} book(s)_other": "Ange status för {{count}} böcker",
|
||||
"Mark as Unread": "Markera som oläst",
|
||||
"Mark as Finished": "Markera som läst",
|
||||
"Finished": "Läst",
|
||||
"Unread": "Oläst",
|
||||
"Clear Status": "Rensa status",
|
||||
"Status": "Status",
|
||||
"Loading": "Laddar...",
|
||||
"Exit Paragraph Mode": "Avsluta styckeläge",
|
||||
"Paragraph Mode": "Styckeläge",
|
||||
"Embedding Model": "Inbäddningsmodell",
|
||||
"{{count}} book(s) synced_one": "{{count}} bok synkroniserad",
|
||||
"{{count}} book(s) synced_other": "{{count}} böcker synkroniserade",
|
||||
"Unable to start RSVP": "Kunde inte starta RSVP",
|
||||
"RSVP not supported for PDF": "RSVP stöds inte för PDF",
|
||||
"Select Chapter": "Välj kapitel",
|
||||
"Context": "Sammanhang",
|
||||
"Ready": "Klar",
|
||||
"Chapter Progress": "Kapitelframsteg",
|
||||
"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",
|
||||
"Choose where to start reading": "Välj var du vill börja läsa",
|
||||
"From Chapter Start": "Från början av kapitlet",
|
||||
"Start reading from the beginning of the chapter": "Börja läsa från början av kapitlet",
|
||||
"Resume": "Återuppta",
|
||||
"Continue from where you left off": "Fortsätt där du slutade",
|
||||
"From Current Page": "Från den aktuella sidan",
|
||||
"Start from where you are currently reading": "Börja där du läser just nu",
|
||||
"From Selection": "Från markeringen",
|
||||
"Speed Reading Mode": "Snabbläsningsläge",
|
||||
"Scroll left": "Scrolla åt vänster",
|
||||
"Scroll right": "Scrolla åt höger",
|
||||
"Library Sync Progress": "Synkroniseringsframsteg för bibliotek",
|
||||
"Back to library": "Tillbaka till biblioteket",
|
||||
"Group by...": "Gruppera efter...",
|
||||
"Export as Plain Text": "Exportera som vanlig text",
|
||||
"Export as Markdown": "Exportera som Markdown",
|
||||
"Show Page Navigation Buttons": "Navigeringsknappar",
|
||||
"Page {{number}}": "Sida {{number}}",
|
||||
"highlight": "markering",
|
||||
"underline": "understrykning",
|
||||
"squiggly": "vågig linje",
|
||||
"red": "röd",
|
||||
"violet": "lila",
|
||||
"blue": "blå",
|
||||
"green": "grön",
|
||||
"yellow": "gul",
|
||||
"Select {{style}} style": "Välj stil {{style}}",
|
||||
"Select {{color}} color": "Välj färg {{color}}",
|
||||
"Close Book": "Stäng bok",
|
||||
"Speed Reading": "Snabbläsning",
|
||||
"Close Speed Reading": "Stäng snabbläsning",
|
||||
"Authors": "Författare",
|
||||
"Books": "Böcker",
|
||||
"Groups": "Grupper",
|
||||
"Back to TTS Location": "Tillbaka till TTS-plats",
|
||||
"Metadata": "Metadata",
|
||||
"Image viewer": "Bildvisare",
|
||||
"Previous Image": "Föregående bild",
|
||||
"Next Image": "Nästa bild",
|
||||
"Zoomed": "Zoomad",
|
||||
"Zoom level": "Zoomnivå",
|
||||
"Table viewer": "Tabellvisare",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Kunde inte ansluta till Readwise. Kontrollera din nätverksanslutning.",
|
||||
"Invalid Readwise access token": "Ogiltig Readwise-åtkomsttoken",
|
||||
"Disconnected from Readwise": "Frånkopplad från Readwise",
|
||||
"Never": "Aldrig",
|
||||
"Readwise Settings": "Readwise-inställningar",
|
||||
"Connected to Readwise": "Ansluten till Readwise",
|
||||
"Last synced: {{time}}": "Senast synkroniserad: {{time}}",
|
||||
"Sync Enabled": "Synkronisering aktiverad",
|
||||
"Disconnect": "Koppla från",
|
||||
"Connect your Readwise account to sync highlights.": "Anslut ditt Readwise-konto för att synkronisera markeringar.",
|
||||
"Get your access token at": "Hämta din åtkomsttoken på",
|
||||
"Access Token": "Åtkomsttoken",
|
||||
"Paste your Readwise access token": "Klistra in din Readwise-åtkomsttoken",
|
||||
"Config": "Konfiguration",
|
||||
"Readwise Sync": "Readwise-synkronisering",
|
||||
"Push Highlights": "Skicka markeringar",
|
||||
"Highlights synced to Readwise": "Markeringar synkroniserade till Readwise",
|
||||
"Readwise sync failed: no internet connection": "Readwise-synkronisering misslyckades: ingen internetanslutning",
|
||||
"Readwise sync failed: {{error}}": "Readwise-synkronisering misslyckades: {{error}}",
|
||||
"System Screen Brightness": "Systemets skärmljusstyrka",
|
||||
"Page:": "Sida:",
|
||||
"Page: {{number}}": "Sida: {{number}}",
|
||||
"Annotation page number": "Annoteringssidnummer"
|
||||
}
|
||||
|
||||
@@ -186,8 +186,8 @@
|
||||
"Add your notes here...": "உங்கள் குறிப்புகளை இங்கே சேர்க்கவும்...",
|
||||
"Search notes and excerpts...": "குறிப்புகள் மற்றும் உதாரணங்களைத் தேடவும்...",
|
||||
"{{time}} min left in chapter": "அத்தியாயத்தில் {{time}} நிமிடங்கள் மீதமுள்ளன",
|
||||
"{{count}} pages left in chapter_one": "அத்தியாயத்தில் 1 பக்கம் மீதமுள்ளது",
|
||||
"{{count}} pages left in chapter_other": "அத்தியாயத்தில் {{count}} பக்கங்கள் மீதமுள்ளன",
|
||||
"{{count}} pages left in chapter_one": "<1>அத்தியாயத்தில் 1 பக்கம் மீதமுள்ளது</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>அத்தியாயத்தில் </1><0>{{count}}</0><1> பக்கங்கள் மீதமுள்ளன</1>",
|
||||
"Theme Mode": "தீம் பயன்முறை",
|
||||
"Invert Image In Dark Mode": "இருட்டு பயன்முறையில் படத்தை தலைகீழாக மாற்றவும்",
|
||||
"Override Book Color": "புத்தக நிறத்தை மேலெழுதவும்",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "அளவு",
|
||||
"Cover": "மூடு",
|
||||
"Contain": "அடங்கும்",
|
||||
"{{number}} pages left in chapter": "அத்தியாயத்தில் {{number}} பக்கங்கள் மீதமுள்ளன",
|
||||
"{{number}} pages left in chapter": "<1>அத்தியாயத்தில் </1><0>{{number}}</0><1> பக்கங்கள் மீதமுள்ளன</1>",
|
||||
"Device": "சாதனம்",
|
||||
"E-Ink Mode": "E-Ink முறை",
|
||||
"Highlight Colors": "முத்திரை நிறங்கள்",
|
||||
"Auto Screen Brightness": "தானியங்கி திரை பிரகாசம்",
|
||||
"Pagination": "பக்கமிடுதல்",
|
||||
"Disable Double Tap": "இரட்டை தொடுதலை முடக்கு",
|
||||
"Tap to Paginate": "பக்கம் மாற்ற தொடுங்கள்",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "முடிவுகளைக் காட்டு",
|
||||
"Clear search": "தேடலை அழி",
|
||||
"Clear search history": "தேடல் வரலாற்றை அழி",
|
||||
"Quick action disabled": "விரைவு செயல் முடக்கப்பட்டது",
|
||||
"Tap to Toggle Footer": "அடிக்குறிப்பை மாற்ற தட்டவும்",
|
||||
"Exported successfully": "ஏற்றுமதி வெற்றி",
|
||||
"Book exported successfully.": "புத்தகம் வெற்றிகரமாக ஏற்றுமதி செய்யப்பட்டது.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "குறிப்புகளின் பட்டியல்",
|
||||
"Highlighted text": "முன்னிலைப்படுத்தப்பட்ட உரை",
|
||||
"Annotation note": "குறிப்பு குறிப்பு",
|
||||
"Update time": "புதுப்பிப்பு நேரம்",
|
||||
"Date Format Tokens:": "தேதி வடிவமைப்பு டோக்கன்கள்:",
|
||||
"Year (4 digits)": "ஆண்டு (4 இலக்கங்கள்)",
|
||||
"Month (01-12)": "மாதம் (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "குரலைத் தேர்ந்தெடுக்கவும்",
|
||||
"Toggle Sticky Bottom TTS Bar": "நிலையான TTS பட்டியை மாற்றவும்",
|
||||
"Display what I'm reading on Discord": "Discord இல் படிக்கும் புத்தகத்தை காட்டு",
|
||||
"Show on Discord": "Discord இல் காட்டு"
|
||||
"Show on Discord": "Discord இல் காட்டு",
|
||||
"Instant {{action}}": "உடனடி {{action}}",
|
||||
"Instant {{action}} Disabled": "உடனடி {{action}} முடக்கப்பட்டது",
|
||||
"Annotation": "குறிப்பு",
|
||||
"Reset Template": "வார்ப்புருவை மீட்டமைக்கவும்",
|
||||
"Annotation style": "சிறுகுறிப்பு பாணி",
|
||||
"Annotation color": "சிறுகுறிப்பு நிறம்",
|
||||
"Annotation time": "சிறுகுறிப்பு நேரம்",
|
||||
"AI": "செயற்கை நுண்ணறிவு (AI)",
|
||||
"Are you sure you want to re-index this book?": "இந்த புத்தகத்தை மீண்டும் அட்டவணைப்படுத்த உறுதியாக இருக்கிறீர்களா?",
|
||||
"Enable AI in Settings": "அமைப்புகளில் AI ஐ இயக்கவும்",
|
||||
"Index This Book": "இந்த புத்தகத்தை அட்டவணைப்படுத்தவும்",
|
||||
"Enable AI search and chat for this book": "இந்த புத்தகத்திற்கு AI தேடல் மற்றும் அரட்டையை இயக்கவும்",
|
||||
"Start Indexing": "அட்டவணைப்படுத்தலைத் தொடங்கவும்",
|
||||
"Indexing book...": "புத்தகம் அட்டவணைப்படுத்தப்படுகிறது...",
|
||||
"Preparing...": "தயாராகிறது...",
|
||||
"Delete this conversation?": "இந்த உரையாடலை நீக்கவா?",
|
||||
"No conversations yet": "இதுவரை உரையாடல்கள் எதுவும் இல்லை",
|
||||
"Start a new chat to ask questions about this book": "இந்த புத்தகத்தைப் பற்றி கேள்விகள் கேட்க புதிய அரட்டையைத் தொடங்கவும்",
|
||||
"Rename": "மறுபெயரிடு",
|
||||
"New Chat": "புதிய அரட்டை",
|
||||
"Chat": "அரட்டை",
|
||||
"Please enter a model ID": "மாதிரி ஐடியை உள்ளிடவும்",
|
||||
"Model not available or invalid": "மாதிரி கிடைக்கவில்லை அல்லது தவறானது",
|
||||
"Failed to validate model": "மாதிரியைச் சரிபார்க்கத் தவறிவிட்டது",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama வுடன் இணைக்க முடியவில்லை. அது இயங்குகிறதா?",
|
||||
"Invalid API key or connection failed": "தவறான ஏபிஐ (API) விசை அல்லது இணைப்பு தோல்வியடைந்தது",
|
||||
"Connection failed": "இணைப்பு தோல்வியடைந்தது",
|
||||
"AI Assistant": "AI உதவியாளர்",
|
||||
"Enable AI Assistant": "AI உதவியாளரை இயக்கவும்",
|
||||
"Provider": "வழங்குநர்",
|
||||
"Ollama (Local)": "Ollama (உள்ளூர்)",
|
||||
"AI Gateway (Cloud)": "AI கேட்வே (கிளவுட்)",
|
||||
"Ollama Configuration": "Ollama உள்ளமைவு",
|
||||
"Refresh Models": "மாதிரிகளைப் புதுப்பிக்கவும்",
|
||||
"AI Model": "AI மாதிரி",
|
||||
"No models detected": "மாதிரிகள் எதுவும் கண்டறியப்படவில்லை",
|
||||
"AI Gateway Configuration": "AI கேட்வே உள்ளமைவு",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "உயர்தர, சிக்கனமான AI மாதிரிகளிலிருந்து தேர்ந்தெடுக்கவும். கீழே உள்ள \"தனிப்பயன் மாதிரி\" என்பதைத் தேர்ந்தெடுப்பதன் மூலம் உங்கள் சொந்த மாதிரியையும் கொண்டு வரலாம்.",
|
||||
"API Key": "API விசை",
|
||||
"Get Key": "விசையைப் பெறவும்",
|
||||
"Model": "மாதிரி",
|
||||
"Custom Model...": "தனிப்பயன் மாதிரி...",
|
||||
"Custom Model ID": "தனிப்பயன் மாதிரி ஐடி",
|
||||
"Validate": "சரிபார்க்கவும்",
|
||||
"Model available": "மாதிரி கிடைக்கிறது",
|
||||
"Connection": "இணைப்பு",
|
||||
"Test Connection": "இணைப்பைச் சோதிக்கவும்",
|
||||
"Connected": "இணைக்கப்பட்டது",
|
||||
"Custom Colors": "தனிப்பயன் வண்ணங்கள்",
|
||||
"Color E-Ink Mode": "வண்ண இ-இங்க் பயன்முறை",
|
||||
"Reading Ruler": "வாசிப்பு அளவுகோல்",
|
||||
"Enable Reading Ruler": "வாசிப்பு அளவுகோலை இயக்கு",
|
||||
"Lines to Highlight": "சிறப்பித்துக் காட்ட வேண்டிய வரிகள்",
|
||||
"Ruler Color": "அளவுகோல் வண்ணம்",
|
||||
"Command Palette": "கட்டளை தட்டு",
|
||||
"Search settings and actions...": "அமைப்புகள் மற்றும் செயல்களைத் தேடுங்கள்...",
|
||||
"No results found for": "இதற்கு முடிவுகள் எதுவும் கிடைக்கவில்லை",
|
||||
"Type to search settings and actions": "அமைப்புகள் மற்றும் செயல்களைத் தேட தட்டச்சு செய்க",
|
||||
"Recent": "சமீபத்திய",
|
||||
"navigate": "வழிசெலுத்து",
|
||||
"select": "தேர்ந்தெடு",
|
||||
"close": "மூடு",
|
||||
"Search Settings": "அமைப்புகளைத் தேடு",
|
||||
"Page Margins": "பக்க விளிம்புகள்",
|
||||
"AI Provider": "AI வழங்குநர்",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama மாதிரி",
|
||||
"AI Gateway Model": "AI கேட்வே மாதிரி",
|
||||
"Actions": "செயல்கள்",
|
||||
"Navigation": "வழிசெலுத்தல்",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} புத்தகத்திற்கான நிலையை அமைக்கவும்",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} புத்தகங்களுக்கான நிலையை அமைக்கவும்",
|
||||
"Mark as Unread": "படிக்காததாகக் குறிக்கவும்",
|
||||
"Mark as Finished": "முடித்ததாகக் குறிக்கவும்",
|
||||
"Finished": "முடிந்தது",
|
||||
"Unread": "படிக்கவில்லை",
|
||||
"Clear Status": "நிலையை அழி",
|
||||
"Status": "நிலை",
|
||||
"Loading": "ஏற்றுகிறது...",
|
||||
"Exit Paragraph Mode": "பத்தி பயன்முறையிலிருந்து வெளியேறு",
|
||||
"Paragraph Mode": "பத்தி பயன்முறை",
|
||||
"Embedding Model": "உட்பொதி மாதிரி",
|
||||
"{{count}} book(s) synced_one": "{{count}} புத்தகம் ஒத்திசைக்கப்பட்டது",
|
||||
"{{count}} book(s) synced_other": "{{count}} புத்தகங்கள் ஒத்திசைக்கப்பட்டன",
|
||||
"Unable to start RSVP": "RSVP-ஐத் தொடங்க முடியவில்லை",
|
||||
"RSVP not supported for PDF": "PDF-க்கு RSVP ஆதரவு இல்லை",
|
||||
"Select Chapter": "அத்தியாயத்தைத் தேர்ந்தெடு",
|
||||
"Context": "சூழல்",
|
||||
"Ready": "தயார்",
|
||||
"Chapter Progress": "அத்தியாய முன்னேற்றம்",
|
||||
"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 வாசிப்பைத் தொடங்கு",
|
||||
"Choose where to start reading": "எங்கிருந்து வாசிக்கத் தொடங்க வேண்டும் என்பதைத் தேர்வு செய்க",
|
||||
"From Chapter Start": "அத்தியாயத்தின் தொடக்கத்திலிருந்து",
|
||||
"Start reading from the beginning of the chapter": "அத்தியாயத்தின் தொடக்கத்திலிருந்து வாசிக்கத் தொடங்கு",
|
||||
"Resume": "தொடரவும்",
|
||||
"Continue from where you left off": "நீங்கள் விட்ட இடத்திலிருந்து தொடரவும்",
|
||||
"From Current Page": "தற்போதைய பக்கத்திலிருந்து",
|
||||
"Start from where you are currently reading": "நீங்கள் தற்போது வாசிக்கும் இடத்திலிருந்து தொடங்கு",
|
||||
"From Selection": "தேர்விலிருந்து",
|
||||
"Speed Reading Mode": "வேக வாசிப்பு முறை",
|
||||
"Scroll left": "இடதுபுறம் உருட்டவும்",
|
||||
"Scroll right": "வலதுபுறம் உருட்டவும்",
|
||||
"Library Sync Progress": "நூலக ஒத்திசைவு நிலை",
|
||||
"Back to library": "நூலகத்திற்குத் திரும்பு",
|
||||
"Group by...": "இதன் படி குழுவாக்கு...",
|
||||
"Export as Plain Text": "எளிய உரையாக ஏற்றுமதி செய்",
|
||||
"Export as Markdown": "Markdown ஆக ஏற்றுமதி செய்",
|
||||
"Show Page Navigation Buttons": "வழிசெலுத்தல் பொத்தான்கள்",
|
||||
"Page {{number}}": "பக்கம் {{number}}",
|
||||
"highlight": "சிறப்பித்துக் காட்டு",
|
||||
"underline": "அடிக்கோடு",
|
||||
"squiggly": "வளைந்த கோடு",
|
||||
"red": "சிவப்பு",
|
||||
"violet": "ஊதா",
|
||||
"blue": "நீலம்",
|
||||
"green": "பச்சை",
|
||||
"yellow": "மஞ்சள்",
|
||||
"Select {{style}} style": "{{style}} பாணியைத் தேர்ந்தெடுக்கவும்",
|
||||
"Select {{color}} color": "{{color}} நிறத்தைத் தேர்ந்தெடுக்கவும்",
|
||||
"Close Book": "புத்தகத்தை மூடு",
|
||||
"Speed Reading": "வேக வாசிப்பு",
|
||||
"Close Speed Reading": "வேக வாசிப்பை மூடு",
|
||||
"Authors": "ஆசிரியர்கள்",
|
||||
"Books": "புத்தகங்கள்",
|
||||
"Groups": "குழுக்கள்",
|
||||
"Back to TTS Location": "TTS இடத்திற்குத் திரும்பு",
|
||||
"Metadata": "மெட்டாடேட்டா",
|
||||
"Image viewer": "படக் காட்சிப்பவர்",
|
||||
"Previous Image": "முந்தைய படம்",
|
||||
"Next Image": "அடுத்த படம்",
|
||||
"Zoomed": "பெரிதாக்கப்பட்டது",
|
||||
"Zoom level": "பெரிதாக்கும் நிலை",
|
||||
"Table viewer": "அட்டவணை காட்சிப்பவர்",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise உடன் இணைக்க முடியவில்லை. உங்கள் நெட்வொர்க் இணைப்பைச் சரிபார்க்கவும்.",
|
||||
"Invalid Readwise access token": "தவறான Readwise அணுகல் டோக்கன்",
|
||||
"Disconnected from Readwise": "Readwise இலிருந்து துண்டிக்கப்பட்டது",
|
||||
"Never": "ஒருபோதும் இல்லை",
|
||||
"Readwise Settings": "Readwise அமைப்புகள்",
|
||||
"Connected to Readwise": "Readwise உடன் இணைக்கப்பட்டது",
|
||||
"Last synced: {{time}}": "கடைசியாக ஒத்திசைக்கப்பட்டது: {{time}}",
|
||||
"Sync Enabled": "ஒத்திசைவு இயக்கப்பட்டது",
|
||||
"Disconnect": "துண்டி",
|
||||
"Connect your Readwise account to sync highlights.": "சிறப்பம்சங்களை ஒத்திசைக்க உங்கள் Readwise கணக்கை இணைக்கவும்.",
|
||||
"Get your access token at": "உங்கள் அணுகல் டோக்கனை இங்கே பெறவும்",
|
||||
"Access Token": "அணுகல் டோக்கன்",
|
||||
"Paste your Readwise access token": "உங்கள் Readwise அணுகல் டோக்கனை ஒட்டவும்",
|
||||
"Config": "கட்டமைப்பு",
|
||||
"Readwise Sync": "Readwise ஒத்திசைவு",
|
||||
"Push Highlights": "சிறப்பம்சங்களை அனுப்பு",
|
||||
"Highlights synced to Readwise": "சிறப்பம்சங்கள் Readwise உடன் ஒத்திசைக்கப்பட்டன",
|
||||
"Readwise sync failed: no internet connection": "Readwise ஒத்திசைவு தோல்வியடைந்தது: இணைய இணைப்பு இல்லை",
|
||||
"Readwise sync failed: {{error}}": "Readwise ஒத்திசைவு தோல்வியடைந்தது: {{error}}",
|
||||
"System Screen Brightness": "கணினித் திரை பிரகாசம்",
|
||||
"Page:": "பக்கம்:",
|
||||
"Page: {{number}}": "பக்கம்: {{number}}",
|
||||
"Annotation page number": "சிறுகுறிப்புப் பக்க எண்"
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"Add your notes here...": "เพิ่มบันทึกที่นี่...",
|
||||
"Search notes and excerpts...": "ค้นหาบันทึกและข้อความ...",
|
||||
"{{time}} min left in chapter": "เหลือ {{time}} นาทีในบท",
|
||||
"{{count}} pages left in chapter_other": "เหลือ {{count}} หน้าในบท",
|
||||
"{{count}} pages left in chapter_other": "<1>เหลือ </1><0>{{count}}</0><1> หน้าในบท</1>",
|
||||
"Theme Mode": "โหมดธีม",
|
||||
"Invert Image In Dark Mode": "กลับสีรูปในโหมดมืด",
|
||||
"Override Book Color": "เขียนทับสีหนังสือ",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "ขนาด",
|
||||
"Cover": "ปก",
|
||||
"Contain": "บรรจุ",
|
||||
"{{number}} pages left in chapter": "เหลือ {{number}} หน้าในบท",
|
||||
"{{number}} pages left in chapter": "<1>เหลือ </1><0>{{number}}</0><1> หน้าในบท</1>",
|
||||
"Device": "อุปกรณ์",
|
||||
"E-Ink Mode": "โหมด E-Ink",
|
||||
"Highlight Colors": "สีไฮไลต์",
|
||||
"Auto Screen Brightness": "ความสว่างหน้าจออัตโนมัติ",
|
||||
"Pagination": "การแบ่งหน้า",
|
||||
"Disable Double Tap": "ปิดการแตะสองครั้ง",
|
||||
"Tap to Paginate": "แตะเพื่อแบ่งหน้า",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "แสดงผลลัพธ์",
|
||||
"Clear search": "ล้างการค้นหา",
|
||||
"Clear search history": "ล้างประวัติการค้นหา",
|
||||
"Quick action disabled": "ปิดใช้งานการดำเนินการด่วน",
|
||||
"Tap to Toggle Footer": "แตะเพื่อสลับส่วนท้าย",
|
||||
"Exported successfully": "ส่งออกสำเร็จ",
|
||||
"Book exported successfully.": "ส่งออกหนังสือสำเร็จ",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "อาร์เรย์ของคำอธิบาย",
|
||||
"Highlighted text": "ข้อความที่ไฮไลต์",
|
||||
"Annotation note": "หมายเหตุคำอธิบาย",
|
||||
"Update time": "เวลาอัปเดต",
|
||||
"Date Format Tokens:": "โทเค็นรูปแบบวันที่:",
|
||||
"Year (4 digits)": "ปี (4 หลัก)",
|
||||
"Month (01-12)": "เดือน (01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "เลือกเสียง",
|
||||
"Toggle Sticky Bottom TTS Bar": "สลับแถบ TTS แบบติด",
|
||||
"Display what I'm reading on Discord": "แสดงหนังสือที่กำลังอ่านบน Discord",
|
||||
"Show on Discord": "แสดงบน Discord"
|
||||
"Show on Discord": "แสดงบน Discord",
|
||||
"Instant {{action}}": "{{action}}ทันที",
|
||||
"Instant {{action}} Disabled": "{{action}}ทันทีถูกปิดใช้งาน",
|
||||
"Annotation": "คำอธิบาย",
|
||||
"Reset Template": "รีเซ็ตเทมเพลต",
|
||||
"Annotation style": "สไตล์คำอธิบาย",
|
||||
"Annotation color": "สีคำอธิบาย",
|
||||
"Annotation time": "เวลาคำอธิบาย",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "คุณแน่ใจหรือไม่ว่าต้องการสร้างดัชนีหนังสือเล่มนี้ใหม่?",
|
||||
"Enable AI in Settings": "เปิดใช้งาน AI ในการตั้งค่า",
|
||||
"Index This Book": "สร้างดัชนีหนังสือเล่มนี้",
|
||||
"Enable AI search and chat for this book": "เปิดใช้งานการค้นหา AI และแชทสำหรับหนังสือเล่มนี้",
|
||||
"Start Indexing": "เริ่มสร้างดัชนี",
|
||||
"Indexing book...": "กำลังสร้างดัชนีหนังสือ...",
|
||||
"Preparing...": "กำลังเตรียม...",
|
||||
"Delete this conversation?": "ลบการสนทนานี้?",
|
||||
"No conversations yet": "ยังไม่มีการสนทนา",
|
||||
"Start a new chat to ask questions about this book": "เริ่มแชทใหม่เพื่อถามคำถามเกี่ยวกับหนังสือเล่มนี้",
|
||||
"Rename": "เปลี่ยนชื่อ",
|
||||
"New Chat": "แชทใหม่",
|
||||
"Chat": "แชท",
|
||||
"Please enter a model ID": "กรุณาป้อน ID โมเดล",
|
||||
"Model not available or invalid": "โมเดลไม่พร้อมใช้งานหรือไม่ถูกต้อง",
|
||||
"Failed to validate model": "ไม่สามารถตรวจสอบโมเดลได้",
|
||||
"Couldn't connect to Ollama. Is it running?": "ไม่สามารถเชื่อมต่อกับ Ollama ได้ กำลังทำงานอยู่หรือไม่?",
|
||||
"Invalid API key or connection failed": "คีย์ API ไม่ถูกต้องหรือการเชื่อมต่อล้มเหลว",
|
||||
"Connection failed": "การเชื่อมต่อล้มเหลว",
|
||||
"AI Assistant": "ผู้ช่วย AI",
|
||||
"Enable AI Assistant": "เปิดใช้งานผู้ช่วย AI",
|
||||
"Provider": "ผู้ให้บริการ",
|
||||
"Ollama (Local)": "Ollama (ในเครื่อง)",
|
||||
"AI Gateway (Cloud)": "เกตเวย์ AI (คลาวด์)",
|
||||
"Ollama Configuration": "การกำหนดค่า Ollama",
|
||||
"Refresh Models": "รีเฟรชโมเดล",
|
||||
"AI Model": "โมเดล AI",
|
||||
"No models detected": "ไม่พบโมเดล",
|
||||
"AI Gateway Configuration": "การกำหนดค่าเกตเวย์ AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "เลือกจากโมเดล AI คุณภาพสูงและประหยัด คุณยังสามารถใช้โมเดลของคุณเองโดยเลือก \"โมเดลกำหนดเอง\" ด้านล่าง",
|
||||
"API Key": "คีย์ API",
|
||||
"Get Key": "รับคีย์",
|
||||
"Model": "โมเดล",
|
||||
"Custom Model...": "โมเดลกำหนดเอง...",
|
||||
"Custom Model ID": "ID โมเดลกำหนดเอง",
|
||||
"Validate": "ตรวจสอบ",
|
||||
"Model available": "โมเดลพร้อมใช้งาน",
|
||||
"Connection": "การเชื่อมต่อ",
|
||||
"Test Connection": "ทดสอบการเชื่อมต่อ",
|
||||
"Connected": "เชื่อมต่อแล้ว",
|
||||
"Custom Colors": "สีที่กำหนดเอง",
|
||||
"Color E-Ink Mode": "โหมด E-Ink สี",
|
||||
"Reading Ruler": "ไม้บรรทัดการอ่าน",
|
||||
"Enable Reading Ruler": "เปิดใช้งานไม้บรรทัดการอ่าน",
|
||||
"Lines to Highlight": "บรรทัดที่ต้องการเน้น",
|
||||
"Ruler Color": "สีไม้บรรทัด",
|
||||
"Command Palette": "พาเลทคำสั่ง",
|
||||
"Search settings and actions...": "ค้นหาการตั้งค่าและคำสั่ง...",
|
||||
"No results found for": "ไม่พบผลลัพธ์สำหรับ",
|
||||
"Type to search settings and actions": "พิมพ์เพื่อค้นหาการตั้งค่าและคำสั่ง",
|
||||
"Recent": "ล่าสุด",
|
||||
"navigate": "นำทาง",
|
||||
"select": "เลือก",
|
||||
"close": "ปิด",
|
||||
"Search Settings": "ค้นหาการตั้งค่า",
|
||||
"Page Margins": "ระยะขอบหน้า",
|
||||
"AI Provider": "ผู้ให้บริการ AI",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "โมเดล Ollama",
|
||||
"AI Gateway Model": "โมเดล AI Gateway",
|
||||
"Actions": "การกระทำ",
|
||||
"Navigation": "การนำทาง",
|
||||
"Set status for {{count}} book(s)_other": "ตั้งสถานะสำหรับหนังสือ {{count}} เล่ม",
|
||||
"Mark as Unread": "ทำเครื่องหมายว่ายังไม่อ่าน",
|
||||
"Mark as Finished": "ทำเครื่องหมายว่าอ่านจบแล้ว",
|
||||
"Finished": "อ่านจบแล้ว",
|
||||
"Unread": "ยังไม่ได้อ่าน",
|
||||
"Clear Status": "ล้างสถานะ",
|
||||
"Status": "สถานะ",
|
||||
"Loading": "กำลังโหลด...",
|
||||
"Exit Paragraph Mode": "ออกจากโหมดพารากราฟ",
|
||||
"Paragraph Mode": "โหมดพารากราฟ",
|
||||
"Embedding Model": "โมเดลการฝังตัว",
|
||||
"{{count}} book(s) synced_other": "ซิงค์หนังสือ {{count}} เล่มแล้ว",
|
||||
"Unable to start RSVP": "ไม่สามารถเริ่ม RSVP ได้",
|
||||
"RSVP not supported for PDF": "ไม่รองรับ RSVP สำหรับ PDF",
|
||||
"Select Chapter": "เลือกบท",
|
||||
"Context": "บริบท",
|
||||
"Ready": "พร้อม",
|
||||
"Chapter Progress": "ความคืบหน้าของบท",
|
||||
"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",
|
||||
"Choose where to start reading": "เลือกจุดที่ต้องการเริ่มอ่าน",
|
||||
"From Chapter Start": "จากจุดเริ่มต้นของบท",
|
||||
"Start reading from the beginning of the chapter": "เริ่มอ่านใหม่ตั้งแต่ต้นบทนี้",
|
||||
"Resume": "อ่านต่อ",
|
||||
"Continue from where you left off": "อ่านต่อจากจุดที่ค้างไว้",
|
||||
"From Current Page": "จากหน้าปัจจุบัน",
|
||||
"Start from where you are currently reading": "เริ่มจากจุดที่คุณกำลังอ่านอยู่ตอนนี้",
|
||||
"From Selection": "จากส่วนที่เลือก",
|
||||
"Speed Reading Mode": "โหมดการอ่านเร็ว",
|
||||
"Scroll left": "เลื่อนไปทางซ้าย",
|
||||
"Scroll right": "เลื่อนไปทางขวา",
|
||||
"Library Sync Progress": "ความคืบหน้าการซิงค์ห้องสมุด",
|
||||
"Back to library": "กลับไปที่ห้องสมุด",
|
||||
"Group by...": "จัดกลุ่มตาม...",
|
||||
"Export as Plain Text": "ส่งออกเป็นข้อความธรรมดา",
|
||||
"Export as Markdown": "ส่งออกเป็น Markdown",
|
||||
"Show Page Navigation Buttons": "ปุ่มนำทาง",
|
||||
"Page {{number}}": "หน้า {{number}}",
|
||||
"highlight": "ไฮไลต์",
|
||||
"underline": "ขีดเส้นใต้",
|
||||
"squiggly": "เส้นหยัก",
|
||||
"red": "สีแดง",
|
||||
"violet": "สีม่วง",
|
||||
"blue": "สีน้ำเงิน",
|
||||
"green": "สีเขียว",
|
||||
"yellow": "สีเหลือง",
|
||||
"Select {{style}} style": "เลือกสไตล์ {{style}}",
|
||||
"Select {{color}} color": "เลือกสี {{color}}",
|
||||
"Close Book": "ปิดหนังสือ",
|
||||
"Speed Reading": "การอ่านเร็ว",
|
||||
"Close Speed Reading": "ปิดการอ่านเร็ว",
|
||||
"Authors": "ผู้เขียน",
|
||||
"Books": "หนังสือ",
|
||||
"Groups": "กลุ่ม",
|
||||
"Back to TTS Location": "กลับไปยังตำแหน่ง TTS",
|
||||
"Metadata": "เมทาดาตา",
|
||||
"Image viewer": "ตัวดูรูปภาพ",
|
||||
"Previous Image": "รูปภาพก่อนหน้า",
|
||||
"Next Image": "รูปภาพถัดไป",
|
||||
"Zoomed": "ซูมแล้ว",
|
||||
"Zoom level": "ระดับการซูม",
|
||||
"Table viewer": "ตัวดูตาราง",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "ไม่สามารถเชื่อมต่อกับ Readwise ได้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ",
|
||||
"Invalid Readwise access token": "โทเคนการเข้าถึง Readwise ไม่ถูกต้อง",
|
||||
"Disconnected from Readwise": "ยกเลิกการเชื่อมต่อกับ Readwise แล้ว",
|
||||
"Never": "ไม่เคย",
|
||||
"Readwise Settings": "การตั้งค่า Readwise",
|
||||
"Connected to Readwise": "เชื่อมต่อกับ Readwise แล้ว",
|
||||
"Last synced: {{time}}": "ซิงค์ล่าสุดเมื่อ: {{time}}",
|
||||
"Sync Enabled": "เปิดใช้งานการซิงค์",
|
||||
"Disconnect": "ยกเลิกการเชื่อมต่อ",
|
||||
"Connect your Readwise account to sync highlights.": "เชื่อมต่อบัญชี Readwise ของคุณเพื่อซิงค์ไฮไลต์",
|
||||
"Get your access token at": "รับโทเคนการเข้าถึงของคุณได้ที่",
|
||||
"Access Token": "โทเคนการเข้าถึง",
|
||||
"Paste your Readwise access token": "วางโทเคนการเข้าถึง Readwise ของคุณ",
|
||||
"Config": "การกำหนดค่า",
|
||||
"Readwise Sync": "การซิงค์ Readwise",
|
||||
"Push Highlights": "พุชไฮไลต์",
|
||||
"Highlights synced to Readwise": "ซิงค์ไฮไลต์ไปยัง Readwise แล้ว",
|
||||
"Readwise sync failed: no internet connection": "การซิงค์ Readwise ล้มเหลว: ไม่มีการเชื่อมต่ออินเทอร์เน็ต",
|
||||
"Readwise sync failed: {{error}}": "การซิงค์ Readwise ล้มเหลว: {{error}}",
|
||||
"System Screen Brightness": "ความสว่างหน้าจอระบบ",
|
||||
"Page:": "หน้า:",
|
||||
"Page: {{number}}": "หน้า: {{number}}",
|
||||
"Annotation page number": "หมายเลขหน้าคำอธิบายประกอบ"
|
||||
}
|
||||
|
||||
@@ -335,8 +335,8 @@
|
||||
"Fit": "Uygun",
|
||||
"Reset {{settings}}": "{{settings}}'i Sıfırla",
|
||||
"Reset Settings": "Ayarları Sıfırla",
|
||||
"{{count}} pages left in chapter_one": "Bu bölümde {{count}} sayfa kaldı",
|
||||
"{{count}} pages left in chapter_other": "Bu bölümde {{count}} sayfa kaldı",
|
||||
"{{count}} pages left in chapter_one": "<1>Bu bölümde </1><0>{{count}}</0><1> sayfa kaldı</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Bu bölümde </1><0>{{count}}</0><1> sayfa kaldı</1>",
|
||||
"Show Remaining Pages": "Kalan sayfaları göster",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -599,11 +599,10 @@
|
||||
"Size": "Boyut",
|
||||
"Cover": "Kapak",
|
||||
"Contain": "İçerir",
|
||||
"{{number}} pages left in chapter": "Bu bölümde {{number}} sayfa kaldı",
|
||||
"{{number}} pages left in chapter": "<1>Bu bölümde </1><0>{{number}}</0><1> sayfa kaldı</1>",
|
||||
"Device": "Cihaz",
|
||||
"E-Ink Mode": "E-Ink Modu",
|
||||
"Highlight Colors": "Vurgu Renkleri",
|
||||
"Auto Screen Brightness": "Otomatik Ekran Parlaklığı",
|
||||
"Pagination": "Sayfalama",
|
||||
"Disable Double Tap": "Çift Dokunmayı Devre Dışı Bırak",
|
||||
"Tap to Paginate": "Sayfalamak için Dokunun",
|
||||
@@ -832,7 +831,6 @@
|
||||
"Show Results": "Sonuçları Göster",
|
||||
"Clear search": "Aramayı temizle",
|
||||
"Clear search history": "Arama geçmişini temizle",
|
||||
"Quick action disabled": "Hızlı eylem devre dışı",
|
||||
"Tap to Toggle Footer": "Altbilgiyi değiştirmek için dokunun",
|
||||
"Exported successfully": "Başarıyla dışa aktarıldı",
|
||||
"Book exported successfully.": "Kitap başarıyla dışa aktarıldı.",
|
||||
@@ -875,7 +873,6 @@
|
||||
"Array of annotations": "Notlar dizisi",
|
||||
"Highlighted text": "Vurgulanan metin",
|
||||
"Annotation note": "Not açıklaması",
|
||||
"Update time": "Güncelleme zamanı",
|
||||
"Date Format Tokens:": "Tarih Biçimi Belirteçleri:",
|
||||
"Year (4 digits)": "Yıl (4 basamak)",
|
||||
"Month (01-12)": "Ay (01-12)",
|
||||
@@ -890,5 +887,177 @@
|
||||
"Select Voice": "Ses seç",
|
||||
"Toggle Sticky Bottom TTS Bar": "Sabit TTS çubuğunu değiştir",
|
||||
"Display what I'm reading on Discord": "Discord'da okuduğumu göster",
|
||||
"Show on Discord": "Discord'da göster"
|
||||
"Show on Discord": "Discord'da göster",
|
||||
"Instant {{action}}": "Anında {{action}}",
|
||||
"Instant {{action}} Disabled": "Anında {{action}} Devre Dışı",
|
||||
"Annotation": "Açıklama",
|
||||
"Reset Template": "Şablonu Sıfırla",
|
||||
"Annotation style": "Açıklama stili",
|
||||
"Annotation color": "Açıklama rengi",
|
||||
"Annotation time": "Açıklama zamanı",
|
||||
"AI": "YZ",
|
||||
"Are you sure you want to re-index this book?": "Bu kitabı yeniden dizinlemek istediğinizden emin misiniz?",
|
||||
"Enable AI in Settings": "Ayarlarda YZ'yi Etkinleştir",
|
||||
"Index This Book": "Bu Kitabı Dizinle",
|
||||
"Enable AI search and chat for this book": "Bu kitap için YZ arama ve sohbeti etkinleştir",
|
||||
"Start Indexing": "Dizinlemeyi Başlat",
|
||||
"Indexing book...": "Kitap dizinleniyor...",
|
||||
"Preparing...": "Hazırlanıyor...",
|
||||
"Delete this conversation?": "Bu konuşmayı sil?",
|
||||
"No conversations yet": "Henüz konuşma yok",
|
||||
"Start a new chat to ask questions about this book": "Bu kitap hakkında soru sormak için yeni bir sohbet başlatın",
|
||||
"Rename": "Yeniden Adlandır",
|
||||
"New Chat": "Yeni Sohbet",
|
||||
"Chat": "Sohbet",
|
||||
"Please enter a model ID": "Lütfen bir model kimliği girin",
|
||||
"Model not available or invalid": "Model kullanılamıyor veya geçersiz",
|
||||
"Failed to validate model": "Model doğrulanamadı",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama'ya bağlanılamadı. Çalışıyor mu?",
|
||||
"Invalid API key or connection failed": "Geçersiz API anahtarı veya bağlantı başarısız",
|
||||
"Connection failed": "Bağlantı başarısız",
|
||||
"AI Assistant": "YZ Asistanı",
|
||||
"Enable AI Assistant": "YZ Asistanını Etkinleştir",
|
||||
"Provider": "Sağlayıcı",
|
||||
"Ollama (Local)": "Ollama (Yerel)",
|
||||
"AI Gateway (Cloud)": "YZ Geçidi (Bulut)",
|
||||
"Ollama Configuration": "Ollama Yapılandırması",
|
||||
"Refresh Models": "Modelleri Yenile",
|
||||
"AI Model": "YZ Modeli",
|
||||
"No models detected": "Model algılanmadı",
|
||||
"AI Gateway Configuration": "YZ Geçidi Yapılandırması",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Yüksek kaliteli, ekonomik YZ modelleri arasından seçim yapın. Aşağıdaki \"Özel Model\"i seçerek kendi modelinizi de kullanabilirsiniz.",
|
||||
"API Key": "API Anahtarı",
|
||||
"Get Key": "Anahtar Al",
|
||||
"Model": "Model",
|
||||
"Custom Model...": "Özel Model...",
|
||||
"Custom Model ID": "Özel Model Kimliği",
|
||||
"Validate": "Doğrula",
|
||||
"Model available": "Model mevcut",
|
||||
"Connection": "Bağlantı",
|
||||
"Test Connection": "Bağlantıyı Test Et",
|
||||
"Connected": "Bağlandı",
|
||||
"Custom Colors": "Özel Renkler",
|
||||
"Color E-Ink Mode": "Renkli E-Ink Modu",
|
||||
"Reading Ruler": "Okuma Cetveli",
|
||||
"Enable Reading Ruler": "Okuma Cetvelini Etkinleştir",
|
||||
"Lines to Highlight": "Vurgulanacak Satırlar",
|
||||
"Ruler Color": "Cetvel Rengi",
|
||||
"Command Palette": "Komut Paleti",
|
||||
"Search settings and actions...": "Ayarları ve eylemleri ara...",
|
||||
"No results found for": "Şunun için sonuç bulunamadı",
|
||||
"Type to search settings and actions": "Ayarları ve eylemleri aramak için yazın",
|
||||
"Recent": "Son kullanılanlar",
|
||||
"navigate": "gezin",
|
||||
"select": "seç",
|
||||
"close": "kapat",
|
||||
"Search Settings": "Ayarları Ara",
|
||||
"Page Margins": "Sayfa Kenar Boşlukları",
|
||||
"AI Provider": "Yapay Zeka Sağlayıcısı",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama Modeli",
|
||||
"AI Gateway Model": "AI Gateway Modeli",
|
||||
"Actions": "Eylemler",
|
||||
"Navigation": "Gezinme",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} kitap için durumu ayarla",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} kitap için durumu ayarla",
|
||||
"Mark as Unread": "Okunmadı olarak işaretle",
|
||||
"Mark as Finished": "Bitirildi olarak işaretle",
|
||||
"Finished": "Bitirildi",
|
||||
"Unread": "Okunmadı",
|
||||
"Clear Status": "Durumu Temizle",
|
||||
"Status": "Durum",
|
||||
"Loading": "Yükleniyor...",
|
||||
"Exit Paragraph Mode": "Paragraf modundan çık",
|
||||
"Paragraph Mode": "Paragraf modu",
|
||||
"Embedding Model": "Gömme Modeli",
|
||||
"{{count}} book(s) synced_one": "{{count}} kitap senkronize edildi",
|
||||
"{{count}} book(s) synced_other": "{{count}} kitap senkronize edildi",
|
||||
"Unable to start RSVP": "RSVP başlatılamadı",
|
||||
"RSVP not supported for PDF": "PDF için RSVP desteklenmiyor",
|
||||
"Select Chapter": "Bölüm Seç",
|
||||
"Context": "Bağlam",
|
||||
"Ready": "Hazır",
|
||||
"Chapter Progress": "Bölüm İlerlemesi",
|
||||
"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",
|
||||
"Choose where to start reading": "Nereden okumaya başlayacağınızı seçin",
|
||||
"From Chapter Start": "Bölüm Başından",
|
||||
"Start reading from the beginning of the chapter": "Bölümün başından itibaren okumaya başla",
|
||||
"Resume": "Devam Et",
|
||||
"Continue from where you left off": "Kaldığınız yerden devam edin",
|
||||
"From Current Page": "Mevcut Sayfadan",
|
||||
"Start from where you are currently reading": "Şu anda okuduğunuz yerden başlayın",
|
||||
"From Selection": "Seçimden",
|
||||
"Speed Reading Mode": "Hızlı Okuma Modu",
|
||||
"Scroll left": "Sola kaydır",
|
||||
"Scroll right": "Sağa kaydır",
|
||||
"Library Sync Progress": "Kütüphane Senkronizasyon İlerlemesi",
|
||||
"Back to library": "Kütüphaneye dön",
|
||||
"Group by...": "Şuna göre grupla...",
|
||||
"Export as Plain Text": "Düz Metin Olarak Dışa Aktar",
|
||||
"Export as Markdown": "Markdown Olarak Dışa Aktar",
|
||||
"Show Page Navigation Buttons": "Gezinme düğmeleri",
|
||||
"Page {{number}}": "Sayfa {{number}}",
|
||||
"highlight": "vurgula",
|
||||
"underline": "altı çizili",
|
||||
"squiggly": "dalgalı",
|
||||
"red": "kırmızı",
|
||||
"violet": "menekşe",
|
||||
"blue": "mavi",
|
||||
"green": "yeşil",
|
||||
"yellow": "sarı",
|
||||
"Select {{style}} style": "{{style}} stilini seç",
|
||||
"Select {{color}} color": "{{color}} rengini seç",
|
||||
"Close Book": "Kitabı Kapat",
|
||||
"Speed Reading": "Hızlı Okuma",
|
||||
"Close Speed Reading": "Hızlı Okumayı Kapat",
|
||||
"Authors": "Yazarlar",
|
||||
"Books": "Kitaplar",
|
||||
"Groups": "Gruplar",
|
||||
"Back to TTS Location": "TTS Konumuna Geri Dön",
|
||||
"Metadata": "Meta veriler",
|
||||
"Image viewer": "Resim görüntüleyici",
|
||||
"Previous Image": "Önceki Resim",
|
||||
"Next Image": "Sonraki Resim",
|
||||
"Zoomed": "Yakınlaştırıldı",
|
||||
"Zoom level": "Yakınlaştırma düzeyi",
|
||||
"Table viewer": "Tablo görüntüleyici",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise'a bağlanılamadı. Lütfen ağ bağlantınızı kontrol edin.",
|
||||
"Invalid Readwise access token": "Geçersiz Readwise erişim kodu",
|
||||
"Disconnected from Readwise": "Readwise bağlantısı kesildi",
|
||||
"Never": "Asla",
|
||||
"Readwise Settings": "Readwise Ayarları",
|
||||
"Connected to Readwise": "Readwise'a Bağlandı",
|
||||
"Last synced: {{time}}": "Son senkronizasyon: {{time}}",
|
||||
"Sync Enabled": "Senkronizasyon Etkin",
|
||||
"Disconnect": "Bağlantıyı Kes",
|
||||
"Connect your Readwise account to sync highlights.": "Vurguları senkronize etmek için Readwise hesabınızı bağlayın.",
|
||||
"Get your access token at": "Erişim kodunuzu şuradan alın:",
|
||||
"Access Token": "Erişim Kodu",
|
||||
"Paste your Readwise access token": "Readwise erişim kodunuzu yapıştırın",
|
||||
"Config": "Yapılandırma",
|
||||
"Readwise Sync": "Readwise Senkronizasyonu",
|
||||
"Push Highlights": "Vurguları Gönder",
|
||||
"Highlights synced to Readwise": "Vurgular Readwise ile senkronize edildi",
|
||||
"Readwise sync failed: no internet connection": "Readwise senkronizasyonu başarısız: internet bağlantısı yok",
|
||||
"Readwise sync failed: {{error}}": "Readwise senkronizasyonu başarısız: {{error}}",
|
||||
"System Screen Brightness": "Sistem Ekran Parlaklığı",
|
||||
"Page:": "Sayfa:",
|
||||
"Page: {{number}}": "Sayfa: {{number}}",
|
||||
"Annotation page number": "Açıklama sayfa numarası"
|
||||
}
|
||||
|
||||
@@ -341,10 +341,10 @@
|
||||
"Fit": "Припасувати",
|
||||
"Reset {{settings}}": "Скинути {{settings}}",
|
||||
"Reset Settings": "Скинути налаштування",
|
||||
"{{count}} pages left in chapter_one": "Залишилася {{count}} сторінка у розділі",
|
||||
"{{count}} pages left in chapter_few": "Залишилось {{count}} сторінки у розділі",
|
||||
"{{count}} pages left in chapter_many": "Залишилось {{count}} сторінок у розділі",
|
||||
"{{count}} pages left in chapter_other": "Залишилось {{count}} сторінок у розділі",
|
||||
"{{count}} pages left in chapter_one": "<1>Залишилася </1><0>{{count}}</0><1> сторінка у розділі</1>",
|
||||
"{{count}} pages left in chapter_few": "<1>Залишилось </1><0>{{count}}</0><1> сторінки у розділі</1>",
|
||||
"{{count}} pages left in chapter_many": "<1>Залишилось </1><0>{{count}}</0><1> сторінок у розділі</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Залишилось </1><0>{{count}}</0><1> сторінок у розділі</1>",
|
||||
"Show Remaining Pages": "Показати залишок сторінок",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -607,11 +607,10 @@
|
||||
"Size": "Розмір",
|
||||
"Cover": "Покрити",
|
||||
"Contain": "Втиснути",
|
||||
"{{number}} pages left in chapter": "Залишилось {{number}} сторінок у розділі",
|
||||
"{{number}} pages left in chapter": "<1>Залишилось </1><0>{{number}}</0><1> сторінок у розділі</1>",
|
||||
"Device": "Пристрій",
|
||||
"E-Ink Mode": "E-Ink режим",
|
||||
"Highlight Colors": "Кольори виділення",
|
||||
"Auto Screen Brightness": "Автоматична яскравість екрану",
|
||||
"Pagination": "Розбиття на сторінки",
|
||||
"Disable Double Tap": "Вимкнути подвійне натискання",
|
||||
"Tap to Paginate": "Натисніть, щоб розбити на сторінки",
|
||||
@@ -852,7 +851,6 @@
|
||||
"Show Results": "Показати результати",
|
||||
"Clear search": "Очистити пошук",
|
||||
"Clear search history": "Очистити історію пошуку",
|
||||
"Quick action disabled": "Швидку дію вимкнено",
|
||||
"Tap to Toggle Footer": "Торкніться, щоб перемкнути нижній колонтитул",
|
||||
"Exported successfully": "Успішно експортовано",
|
||||
"Book exported successfully.": "Книгу успішно експортовано.",
|
||||
@@ -895,7 +893,6 @@
|
||||
"Array of annotations": "Масив приміток",
|
||||
"Highlighted text": "Виділений текст",
|
||||
"Annotation note": "Примітка анотації",
|
||||
"Update time": "Час оновлення",
|
||||
"Date Format Tokens:": "Токени формату дати:",
|
||||
"Year (4 digits)": "Рік (4 цифри)",
|
||||
"Month (01-12)": "Місяць (01-12)",
|
||||
@@ -910,5 +907,181 @@
|
||||
"Select Voice": "Вибрати голос",
|
||||
"Toggle Sticky Bottom TTS Bar": "Перемкнути закріплену панель TTS",
|
||||
"Display what I'm reading on Discord": "Показувати книгу в Discord",
|
||||
"Show on Discord": "Показати в Discord"
|
||||
"Show on Discord": "Показати в Discord",
|
||||
"Instant {{action}}": "Миттєва {{action}}",
|
||||
"Instant {{action}} Disabled": "Миттєву {{action}} вимкнено",
|
||||
"Annotation": "Анотація",
|
||||
"Reset Template": "Скинути шаблон",
|
||||
"Annotation style": "Стиль анотації",
|
||||
"Annotation color": "Колір анотації",
|
||||
"Annotation time": "Час анотації",
|
||||
"AI": "ШІ",
|
||||
"Are you sure you want to re-index this book?": "Ви впевнені, що хочете переіндексувати цю книгу?",
|
||||
"Enable AI in Settings": "Увімкнути ШІ в налаштуваннях",
|
||||
"Index This Book": "Індексувати цю книгу",
|
||||
"Enable AI search and chat for this book": "Увімкнути ШІ-пошук та чат для цієї книги",
|
||||
"Start Indexing": "Почати індексацію",
|
||||
"Indexing book...": "Індексація книги...",
|
||||
"Preparing...": "Підготовка...",
|
||||
"Delete this conversation?": "Видалити цю розмову?",
|
||||
"No conversations yet": "Поки немає розмов",
|
||||
"Start a new chat to ask questions about this book": "Почніть новий чат, щоб задати питання про цю книгу",
|
||||
"Rename": "Перейменувати",
|
||||
"New Chat": "Новий чат",
|
||||
"Chat": "Чат",
|
||||
"Please enter a model ID": "Будь ласка, введіть ID моделі",
|
||||
"Model not available or invalid": "Модель недоступна або недійсна",
|
||||
"Failed to validate model": "Не вдалося перевірити модель",
|
||||
"Couldn't connect to Ollama. Is it running?": "Не вдалося підключитися до Ollama. Чи вона запущена?",
|
||||
"Invalid API key or connection failed": "Недійсний API-ключ або помилка підключення",
|
||||
"Connection failed": "Помилка підключення",
|
||||
"AI Assistant": "ШІ-асистент",
|
||||
"Enable AI Assistant": "Увімкнути ШІ-асистента",
|
||||
"Provider": "Провайдер",
|
||||
"Ollama (Local)": "Ollama (Локальний)",
|
||||
"AI Gateway (Cloud)": "ШІ-шлюз (Хмара)",
|
||||
"Ollama Configuration": "Налаштування Ollama",
|
||||
"Refresh Models": "Оновити моделі",
|
||||
"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.": "Оберіть з високоякісних, економічних ШІ-моделей. Ви також можете використати власну модель, обравши \"Користувацька модель\" нижче.",
|
||||
"API Key": "API-ключ",
|
||||
"Get Key": "Отримати ключ",
|
||||
"Model": "Модель",
|
||||
"Custom Model...": "Користувацька модель...",
|
||||
"Custom Model ID": "ID користувацької моделі",
|
||||
"Validate": "Перевірити",
|
||||
"Model available": "Модель доступна",
|
||||
"Connection": "З'єднання",
|
||||
"Test Connection": "Перевірити з'єднання",
|
||||
"Connected": "Підключено",
|
||||
"Custom Colors": "Спеціальні кольори",
|
||||
"Color E-Ink Mode": "Кольоровий режим E-Ink",
|
||||
"Reading Ruler": "Лінійка для читання",
|
||||
"Enable Reading Ruler": "Увімкнути лінійку для читання",
|
||||
"Lines to Highlight": "Рядки для виділення",
|
||||
"Ruler Color": "Коліර් лінійки",
|
||||
"Command Palette": "Палітра команд",
|
||||
"Search settings and actions...": "Пошук налаштувань та дій...",
|
||||
"No results found for": "Результатів не знайдено для",
|
||||
"Type to search settings and actions": "Введіть для пошуку налаштувань та дій",
|
||||
"Recent": "Нещодавні",
|
||||
"navigate": "навігація",
|
||||
"select": "вибрати",
|
||||
"close": "закрити",
|
||||
"Search Settings": "Пошук налаштувань",
|
||||
"Page Margins": "Поля сторінки",
|
||||
"AI Provider": "Провайдер ШІ",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Модель Ollama",
|
||||
"AI Gateway Model": "Модель AI Gateway",
|
||||
"Actions": "Дії",
|
||||
"Navigation": "Навігація",
|
||||
"Set status for {{count}} book(s)_one": "Встановити статус для {{count}} книги",
|
||||
"Set status for {{count}} book(s)_other": "Встановити статус для {{count}} книг",
|
||||
"Mark as Unread": "Позначити як непрочитане",
|
||||
"Mark as Finished": "Позначити як завершене",
|
||||
"Finished": "Завершено",
|
||||
"Unread": "Непрочитане",
|
||||
"Clear Status": "Очистити статус",
|
||||
"Status": "Статус",
|
||||
"Set status for {{count}} book(s)_few": "Встановити статус для {{count}} книг",
|
||||
"Set status for {{count}} book(s)_many": "Встановити статус для {{count}} книг",
|
||||
"Loading": "Завантаження...",
|
||||
"Exit Paragraph Mode": "Вийти з режиму абзацу",
|
||||
"Paragraph Mode": "Режим абзацу",
|
||||
"Embedding Model": "Модель вбудовування",
|
||||
"{{count}} book(s) synced_one": "{{count}} книга синхронізована",
|
||||
"{{count}} book(s) synced_few": "{{count}} книги синхронізовані",
|
||||
"{{count}} book(s) synced_many": "{{count}} книг синхронізовано",
|
||||
"{{count}} book(s) synced_other": "{{count}} книг синхронізовано",
|
||||
"Unable to start RSVP": "Не вдалося запустити RSVP",
|
||||
"RSVP not supported for PDF": "RSVP не підтримується для PDF",
|
||||
"Select Chapter": "Вибрати розділ",
|
||||
"Context": "Контекст",
|
||||
"Ready": "Готово",
|
||||
"Chapter Progress": "Прогрес розділу",
|
||||
"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",
|
||||
"Choose where to start reading": "Виберіть, де почати читання",
|
||||
"From Chapter Start": "З початку розділу",
|
||||
"Start reading from the beginning of the chapter": "Почати читання з початку розділу",
|
||||
"Resume": "Продовжити",
|
||||
"Continue from where you left off": "Продовжити з того місця, де ви зупинилися",
|
||||
"From Current Page": "З поточної сторінки",
|
||||
"Start from where you are currently reading": "Почати з того місця, де ви зараз читаєте",
|
||||
"From Selection": "З виділеного",
|
||||
"Speed Reading Mode": "Режим швидкого читання",
|
||||
"Scroll left": "Прокрутити вліво",
|
||||
"Scroll right": "Прокрутити вправо",
|
||||
"Library Sync Progress": "Прогрес синхронізації бібліотеки",
|
||||
"Back to library": "Назад до бібліотеки",
|
||||
"Group by...": "Групувати за...",
|
||||
"Export as Plain Text": "Експортувати як звичайний текст",
|
||||
"Export as Markdown": "Експортувати як Markdown",
|
||||
"Show Page Navigation Buttons": "Кнопки навігації",
|
||||
"Page {{number}}": "Сторінка {{number}}",
|
||||
"highlight": "виділення",
|
||||
"underline": "підкреслення",
|
||||
"squiggly": "хвиляста лінія",
|
||||
"red": "червоний",
|
||||
"violet": "фіолетовий",
|
||||
"blue": "синій",
|
||||
"green": "зелений",
|
||||
"yellow": "жовтий",
|
||||
"Select {{style}} style": "Вибрати стиль {{style}}",
|
||||
"Select {{color}} color": "Вибрати колір {{color}}",
|
||||
"Close Book": "Закрити книгу",
|
||||
"Speed Reading": "Швидкісне читання",
|
||||
"Close Speed Reading": "Закрити швидкісне читання",
|
||||
"Authors": "Автори",
|
||||
"Books": "Книги",
|
||||
"Groups": "Групи",
|
||||
"Back to TTS Location": "Повернутися до розташування TTS",
|
||||
"Metadata": "Метадані",
|
||||
"Image viewer": "Переглядач зображень",
|
||||
"Previous Image": "Попереднє зображення",
|
||||
"Next Image": "Наступне зображення",
|
||||
"Zoomed": "Збільшено",
|
||||
"Zoom level": "Рівень масштабу",
|
||||
"Table viewer": "Переглядач таблиць",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Не вдалося підключитися до Readwise. Будь ласка, перевірте мережеве з'єднання.",
|
||||
"Invalid Readwise access token": "Недійсний токен доступу Readwise",
|
||||
"Disconnected from Readwise": "Відключено від Readwise",
|
||||
"Never": "Ніколи",
|
||||
"Readwise Settings": "Налаштування Readwise",
|
||||
"Connected to Readwise": "Підключено до Readwise",
|
||||
"Last synced: {{time}}": "Остання синхронізація: {{time}}",
|
||||
"Sync Enabled": "Синхронізацію увімкнено",
|
||||
"Disconnect": "Відключити",
|
||||
"Connect your Readwise account to sync highlights.": "Підключіть свій обліковий запис Readwise, щоб синхронізувати виділення.",
|
||||
"Get your access token at": "Отримайте токен доступу за адресою",
|
||||
"Access Token": "Токен доступу",
|
||||
"Paste your Readwise access token": "Вставте ваш токен доступу Readwise",
|
||||
"Config": "Конфігурація",
|
||||
"Readwise Sync": "Синхронізація Readwise",
|
||||
"Push Highlights": "Надіслати виділення",
|
||||
"Highlights synced to Readwise": "Виділення синхронізовано з Readwise",
|
||||
"Readwise sync failed: no internet connection": "Синхронізація Readwise не вдалася: немає інтернет-з'єднання",
|
||||
"Readwise sync failed: {{error}}": "Синхронізація Readwise не вдалася: {{error}}",
|
||||
"System Screen Brightness": "Системна яскравість екрана",
|
||||
"Page:": "Сторінка:",
|
||||
"Page: {{number}}": "Сторінка: {{number}}",
|
||||
"Annotation page number": "Номер сторінки анотації"
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
"Fit": "Vừa vặn",
|
||||
"Reset {{settings}}": "Đặt lại {{settings}}",
|
||||
"Reset Settings": "Đặt lại cài đặt",
|
||||
"{{count}} pages left in chapter_other": "Còn {{count}} trang trong chương",
|
||||
"{{count}} pages left in chapter_other": "<1>Còn </1><0>{{count}}</0><1> trang trong chương</1>",
|
||||
"Show Remaining Pages": "Hiển thị trang còn lại",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "Kích thước",
|
||||
"Cover": "Bìa",
|
||||
"Contain": "Chứa",
|
||||
"{{number}} pages left in chapter": "Còn {{number}} trang trong chương",
|
||||
"{{number}} pages left in chapter": "<1>Còn </1><0>{{number}}</0><1> trang trong chương</1>",
|
||||
"Device": "Thiết bị",
|
||||
"E-Ink Mode": "Chế độ E-Ink",
|
||||
"Highlight Colors": "Màu nổi bật",
|
||||
"Auto Screen Brightness": "Độ sáng màn hình tự động",
|
||||
"Pagination": "Phân trang",
|
||||
"Disable Double Tap": "Vô hiệu hóa chạm hai lần",
|
||||
"Tap to Paginate": "Chạm để phân trang",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "Hiển thị kết quả",
|
||||
"Clear search": "Xóa tìm kiếm",
|
||||
"Clear search history": "Xóa lịch sử tìm kiếm",
|
||||
"Quick action disabled": "Đã tắt thao tác nhanh",
|
||||
"Tap to Toggle Footer": "Nhấn để bật/tắt chân trang",
|
||||
"Exported successfully": "Xuất thành công",
|
||||
"Book exported successfully.": "Sách đã được xuất thành công.",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "Danh sách chú thích",
|
||||
"Highlighted text": "Văn bản được đánh dấu",
|
||||
"Annotation note": "Ghi chú chú thích",
|
||||
"Update time": "Thời gian cập nhật",
|
||||
"Date Format Tokens:": "Token định dạng ngày:",
|
||||
"Year (4 digits)": "Năm (4 chữ số)",
|
||||
"Month (01-12)": "Tháng (01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "Chọn giọng nói",
|
||||
"Toggle Sticky Bottom TTS Bar": "Bật/tắt thanh TTS cố định",
|
||||
"Display what I'm reading on Discord": "Hiển thị sách đang đọc trên Discord",
|
||||
"Show on Discord": "Hiện trên Discord"
|
||||
"Show on Discord": "Hiện trên Discord",
|
||||
"Instant {{action}}": "{{action}} tức thì",
|
||||
"Instant {{action}} Disabled": "{{action}} tức thì đã tắt",
|
||||
"Annotation": "Chú thích",
|
||||
"Reset Template": "Đặt lại mẫu",
|
||||
"Annotation style": "Kiểu chú thích",
|
||||
"Annotation color": "Màu chú thích",
|
||||
"Annotation time": "Thời gian chú thích",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Bạn có chắc muốn lập chỉ mục lại cuốn sách này?",
|
||||
"Enable AI in Settings": "Bật AI trong Cài đặt",
|
||||
"Index This Book": "Lập chỉ mục sách này",
|
||||
"Enable AI search and chat for this book": "Bật tìm kiếm AI và trò chuyện cho cuốn sách này",
|
||||
"Start Indexing": "Bắt đầu lập chỉ mục",
|
||||
"Indexing book...": "Đang lập chỉ mục sách...",
|
||||
"Preparing...": "Đang chuẩn bị...",
|
||||
"Delete this conversation?": "Xóa cuộc trò chuyện này?",
|
||||
"No conversations yet": "Chưa có cuộc trò chuyện nào",
|
||||
"Start a new chat to ask questions about this book": "Bắt đầu cuộc trò chuyện mới để hỏi về cuốn sách này",
|
||||
"Rename": "Đổi tên",
|
||||
"New Chat": "Cuộc trò chuyện mới",
|
||||
"Chat": "Trò chuyện",
|
||||
"Please enter a model ID": "Vui lòng nhập ID mô hình",
|
||||
"Model not available or invalid": "Mô hình không khả dụng hoặc không hợp lệ",
|
||||
"Failed to validate model": "Xác thực mô hình thất bại",
|
||||
"Couldn't connect to Ollama. Is it running?": "Không thể kết nối với Ollama. Nó có đang chạy không?",
|
||||
"Invalid API key or connection failed": "Khóa API không hợp lệ hoặc kết nối thất bại",
|
||||
"Connection failed": "Kết nối thất bại",
|
||||
"AI Assistant": "Trợ lý AI",
|
||||
"Enable AI Assistant": "Bật trợ lý AI",
|
||||
"Provider": "Nhà cung cấp",
|
||||
"Ollama (Local)": "Ollama (Cục bộ)",
|
||||
"AI Gateway (Cloud)": "Cổng AI (Đám mây)",
|
||||
"Ollama Configuration": "Cấu hình Ollama",
|
||||
"Refresh Models": "Làm mới mô hình",
|
||||
"AI Model": "Mô hình AI",
|
||||
"No models detected": "Không phát hiện mô hình nào",
|
||||
"AI Gateway Configuration": "Cấu hình cổng AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Chọn từ các mô hình AI chất lượng cao, tiết kiệm. Bạn cũng có thể sử dụng mô hình riêng bằng cách chọn \"Mô hình tùy chỉnh\" bên dưới.",
|
||||
"API Key": "Khóa API",
|
||||
"Get Key": "Lấy khóa",
|
||||
"Model": "Mô hình",
|
||||
"Custom Model...": "Mô hình tùy chỉnh...",
|
||||
"Custom Model ID": "ID mô hình tùy chỉnh",
|
||||
"Validate": "Xác thực",
|
||||
"Model available": "Mô hình khả dụng",
|
||||
"Connection": "Kết nối",
|
||||
"Test Connection": "Kiểm tra kết nối",
|
||||
"Connected": "Đã kết nối",
|
||||
"Custom Colors": "Màu tùy chỉnh",
|
||||
"Color E-Ink Mode": "Chế độ E-Ink màu",
|
||||
"Reading Ruler": "Thước đọc",
|
||||
"Enable Reading Ruler": "Bật thước đọc",
|
||||
"Lines to Highlight": "Số dòng cần làm nổi bật",
|
||||
"Ruler Color": "Màu thước",
|
||||
"Command Palette": "Bảng lệnh",
|
||||
"Search settings and actions...": "Tìm kiếm cài đặt và hành động...",
|
||||
"No results found for": "Không tìm thấy kết quả cho",
|
||||
"Type to search settings and actions": "Nhập để tìm kiếm cài đặt và hành động",
|
||||
"Recent": "Gần đây",
|
||||
"navigate": "điều hướng",
|
||||
"select": "chọn",
|
||||
"close": "đóng",
|
||||
"Search Settings": "Tìm kiếm cài đặt",
|
||||
"Page Margins": "Lề trang",
|
||||
"AI Provider": "Nhà cung cấp AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Mô hình Ollama",
|
||||
"AI Gateway Model": "Mô hình AI Gateway",
|
||||
"Actions": "Hành động",
|
||||
"Navigation": "Điều hướng",
|
||||
"Set status for {{count}} book(s)_other": "Đặt trạng thái cho {{count}} cuốn sách",
|
||||
"Mark as Unread": "Đánh dấu là chưa đọc",
|
||||
"Mark as Finished": "Đánh dấu là đã hoàn thành",
|
||||
"Finished": "Đã hoàn thành",
|
||||
"Unread": "Chưa đọc",
|
||||
"Clear Status": "Xóa trạng thái",
|
||||
"Status": "Trạng thái",
|
||||
"Loading": "Đang tải...",
|
||||
"Exit Paragraph Mode": "Thoát chế độ đoạn văn",
|
||||
"Paragraph Mode": "Chế độ đoạn văn",
|
||||
"Embedding Model": "Mô hình nhúng",
|
||||
"{{count}} book(s) synced_other": "Đã đồng bộ {{count}} cuốn sách",
|
||||
"Unable to start RSVP": "Không thể bắt đầu RSVP",
|
||||
"RSVP not supported for PDF": "RSVP không được hỗ trợ cho PDF",
|
||||
"Select Chapter": "Chọn chương",
|
||||
"Context": "Ngữ cảnh",
|
||||
"Ready": "Sẵn sàng",
|
||||
"Chapter Progress": "Tiến độ chương",
|
||||
"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",
|
||||
"Choose where to start reading": "Chọn nơi để bắt đầu đọc",
|
||||
"From Chapter Start": "Từ đầu chương",
|
||||
"Start reading from the beginning of the chapter": "Bắt đầu đọc lại từ đầu chương này",
|
||||
"Resume": "Tiếp tục",
|
||||
"Continue from where you left off": "Tiếp tục từ nơi bạn đã dừng lại",
|
||||
"From Current Page": "Từ trang hiện tại",
|
||||
"Start from where you are currently reading": "Bắt đầu từ nơi bạn hiện đang đọc",
|
||||
"From Selection": "Từ phần đã chọn",
|
||||
"Speed Reading Mode": "Chế độ đọc nhanh",
|
||||
"Scroll left": "Cuộn sang trái",
|
||||
"Scroll right": "Cuộn sang phải",
|
||||
"Library Sync Progress": "Tiến trình đồng bộ hóa thư viện",
|
||||
"Back to library": "Quay lại thư viện",
|
||||
"Group by...": "Nhóm theo...",
|
||||
"Export as Plain Text": "Xuất dưới dạng văn bản thuần túy",
|
||||
"Export as Markdown": "Xuất dưới dạng Markdown",
|
||||
"Show Page Navigation Buttons": "Nút điều hướng trang",
|
||||
"Page {{number}}": "Trang {{number}}",
|
||||
"highlight": "làm nổi bật",
|
||||
"underline": "gạch chân",
|
||||
"squiggly": "ngoằn ngoèo",
|
||||
"red": "đỏ",
|
||||
"violet": "tím",
|
||||
"blue": "xanh lam",
|
||||
"green": "xanh lá",
|
||||
"yellow": "vàng",
|
||||
"Select {{style}} style": "Chọn kiểu {{style}}",
|
||||
"Select {{color}} color": "Chọn màu {{color}}",
|
||||
"Close Book": "Đóng sách",
|
||||
"Speed Reading": "Đọc nhanh",
|
||||
"Close Speed Reading": "Đóng đọc nhanh",
|
||||
"Authors": "Tác giả",
|
||||
"Books": "Sách",
|
||||
"Groups": "Nhóm",
|
||||
"Back to TTS Location": "Quay lại vị trí TTS",
|
||||
"Metadata": "Siêu dữ liệu",
|
||||
"Image viewer": "Trình xem ảnh",
|
||||
"Previous Image": "Ảnh trước",
|
||||
"Next Image": "Ảnh sau",
|
||||
"Zoomed": "Đã thu phóng",
|
||||
"Zoom level": "Mức thu phóng",
|
||||
"Table viewer": "Trình xem bảng",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Không thể kết nối với Readwise. Vui lòng kiểm tra kết nối mạng của bạn.",
|
||||
"Invalid Readwise access token": "Mã xác thực Readwise không hợp lệ",
|
||||
"Disconnected from Readwise": "Đã ngắt kết nối với Readwise",
|
||||
"Never": "Không bao giờ",
|
||||
"Readwise Settings": "Cài đặt Readwise",
|
||||
"Connected to Readwise": "Đã kết nối với Readwise",
|
||||
"Last synced: {{time}}": "Lần đồng bộ cuối: {{time}}",
|
||||
"Sync Enabled": "Đã bật đồng bộ",
|
||||
"Disconnect": "Ngắt kết nối",
|
||||
"Connect your Readwise account to sync highlights.": "Kết nối tài khoản Readwise của bạn để đồng bộ các phần đánh dấu.",
|
||||
"Get your access token at": "Lấy mã xác thực tại",
|
||||
"Access Token": "Mã xác thực",
|
||||
"Paste your Readwise access token": "Dán mã xác thực Readwise của bạn vào đây",
|
||||
"Config": "Cấu hình",
|
||||
"Readwise Sync": "Đồng bộ Readwise",
|
||||
"Push Highlights": "Đẩy các phần đánh dấu",
|
||||
"Highlights synced to Readwise": "Các phần đánh dấu đã được đồng bộ với Readwise",
|
||||
"Readwise sync failed: no internet connection": "Đồng bộ Readwise thất bại: không có kết nối internet",
|
||||
"Readwise sync failed: {{error}}": "Đồng bộ Readwise thất bại: {{error}}",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
"Fit": "适应",
|
||||
"Reset {{settings}}": "重置{{settings}}",
|
||||
"Reset Settings": "重置设置",
|
||||
"{{count}} pages left in chapter_other": "本章剩余 {{count}} 页",
|
||||
"{{count}} pages left in chapter_other": "<1>本章剩余 </1><0>{{count}}</0><1> 页</1>",
|
||||
"Show Remaining Pages": "显示剩余页数",
|
||||
"Source Han Serif CN": "思源宋体",
|
||||
"Huiwen-MinchoGBK": "汇文明朝体",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "大小",
|
||||
"Cover": "覆盖",
|
||||
"Contain": "适应",
|
||||
"{{number}} pages left in chapter": "本章剩余{{number}}页",
|
||||
"{{number}} pages left in chapter": "<1>本章剩余</1><0>{{number}}</0><1>页</1>",
|
||||
"Device": "设备",
|
||||
"E-Ink Mode": "E-Ink 模式",
|
||||
"Highlight Colors": "高亮颜色",
|
||||
"Auto Screen Brightness": "自动屏幕亮度",
|
||||
"Pagination": "翻页",
|
||||
"Disable Double Tap": "禁用双击",
|
||||
"Tap to Paginate": "点击翻页",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "显示结果",
|
||||
"Clear search": "清除搜索",
|
||||
"Clear search history": "清除搜索历史",
|
||||
"Quick action disabled": "快捷操作已禁用",
|
||||
"Tap to Toggle Footer": "点击切换页脚",
|
||||
"Exported successfully": "导出成功",
|
||||
"Book exported successfully.": "书籍导出成功。",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "标注数组",
|
||||
"Highlighted text": "高亮文本",
|
||||
"Annotation note": "标注笔记",
|
||||
"Update time": "更新时间",
|
||||
"Date Format Tokens:": "日期格式标记:",
|
||||
"Year (4 digits)": "年(4位)",
|
||||
"Month (01-12)": "月(01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "选择语音",
|
||||
"Toggle Sticky Bottom TTS Bar": "切换固定TTS栏",
|
||||
"Display what I'm reading on Discord": "在Discord显示阅读状态",
|
||||
"Show on Discord": "在Discord显示"
|
||||
"Show on Discord": "在Discord显示",
|
||||
"Instant {{action}}": "即时{{action}}",
|
||||
"Instant {{action}} Disabled": "即时{{action}}已禁用",
|
||||
"Annotation": "标注",
|
||||
"Reset Template": "重置模板",
|
||||
"Annotation style": "标注样式",
|
||||
"Annotation color": "标注颜色",
|
||||
"Annotation time": "标注时间",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "确定要重新索引这本书吗?",
|
||||
"Enable AI in Settings": "在设置中启用 AI",
|
||||
"Index This Book": "索引这本书",
|
||||
"Enable AI search and chat for this book": "为这本书启用 AI 搜索和聊天",
|
||||
"Start Indexing": "开始索引",
|
||||
"Indexing book...": "正在索引书籍...",
|
||||
"Preparing...": "准备中...",
|
||||
"Delete this conversation?": "删除此对话?",
|
||||
"No conversations yet": "暂无对话",
|
||||
"Start a new chat to ask questions about this book": "开始新对话来询问关于这本书的问题",
|
||||
"Rename": "重命名",
|
||||
"New Chat": "新对话",
|
||||
"Chat": "聊天",
|
||||
"Please enter a model ID": "请输入模型 ID",
|
||||
"Model not available or invalid": "模型不可用或无效",
|
||||
"Failed to validate model": "模型验证失败",
|
||||
"Couldn't connect to Ollama. Is it running?": "无法连接到 Ollama。它是否正在运行?",
|
||||
"Invalid API key or connection failed": "API 密钥无效或连接失败",
|
||||
"Connection failed": "连接失败",
|
||||
"AI Assistant": "AI 助手",
|
||||
"Enable AI Assistant": "启用 AI 助手",
|
||||
"Provider": "提供商",
|
||||
"Ollama (Local)": "Ollama(本地)",
|
||||
"AI Gateway (Cloud)": "AI 网关(云端)",
|
||||
"Ollama Configuration": "Ollama 配置",
|
||||
"Refresh Models": "刷新模型",
|
||||
"AI Model": "AI 模型",
|
||||
"No models detected": "未检测到模型",
|
||||
"AI Gateway Configuration": "AI 网关配置",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "从一系列高质量、经济实惠的 AI 模型中选择。您也可以通过下方的\"自定义模型\"使用自己的模型。",
|
||||
"API Key": "API 密钥",
|
||||
"Get Key": "获取密钥",
|
||||
"Model": "模型",
|
||||
"Custom Model...": "自定义模型...",
|
||||
"Custom Model ID": "自定义模型 ID",
|
||||
"Validate": "验证",
|
||||
"Model available": "模型可用",
|
||||
"Connection": "连接",
|
||||
"Test Connection": "测试连接",
|
||||
"Connected": "已连接",
|
||||
"Custom Colors": "自定义颜色",
|
||||
"Color E-Ink Mode": "彩色墨水屏模式",
|
||||
"Reading Ruler": "阅读尺",
|
||||
"Enable Reading Ruler": "启用阅读尺",
|
||||
"Lines to Highlight": "突出显示行数",
|
||||
"Ruler Color": "尺子颜色",
|
||||
"Command Palette": "命令面板",
|
||||
"Search settings and actions...": "搜索设置和操作...",
|
||||
"No results found for": "未找到结果:",
|
||||
"Type to search settings and actions": "输入以搜索设置和操作",
|
||||
"Recent": "最近",
|
||||
"navigate": "导航",
|
||||
"select": "选择",
|
||||
"close": "关闭",
|
||||
"Search Settings": "搜索设置",
|
||||
"Page Margins": "页边距",
|
||||
"AI Provider": "AI 服务商",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama 模型",
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "导航",
|
||||
"Set status for {{count}} book(s)_other": "设置 {{count}} 本书的状态",
|
||||
"Mark as Unread": "标记为未读",
|
||||
"Mark as Finished": "标记为已读",
|
||||
"Finished": "已读",
|
||||
"Unread": "未读",
|
||||
"Clear Status": "清除状态",
|
||||
"Status": "状态",
|
||||
"Loading": "正在加载",
|
||||
"Exit Paragraph Mode": "退出段落模式",
|
||||
"Paragraph Mode": "段落模式",
|
||||
"Embedding Model": "嵌入模型",
|
||||
"{{count}} book(s) synced_other": "{{count}} 本书已同步",
|
||||
"Unable to start RSVP": "无法启动 RSVP",
|
||||
"RSVP not supported for PDF": "PDF 不支持 RSVP",
|
||||
"Select Chapter": "选择章节",
|
||||
"Context": "上下文",
|
||||
"Ready": "准备就绪",
|
||||
"Chapter Progress": "章节进度",
|
||||
"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 阅读",
|
||||
"Choose where to start reading": "选择阅读起始位置",
|
||||
"From Chapter Start": "从章节开始",
|
||||
"Start reading from the beginning of the chapter": "重新从本章节开始阅读",
|
||||
"Resume": "继续",
|
||||
"Continue from where you left off": "从上次离开的地方继续",
|
||||
"From Current Page": "从当前页",
|
||||
"Start from where you are currently reading": "从当前正在阅读的位置开始",
|
||||
"From Selection": "从选中内容",
|
||||
"Speed Reading Mode": "快读模式",
|
||||
"Scroll left": "向左滚动",
|
||||
"Scroll right": "向右滚动",
|
||||
"Library Sync Progress": "库同步进度",
|
||||
"Back to library": "返回书库",
|
||||
"Group by...": "分组方式...",
|
||||
"Export as Plain Text": "导出为纯文本",
|
||||
"Export as Markdown": "导出为 Markdown",
|
||||
"Show Page Navigation Buttons": "显示翻页按钮",
|
||||
"Page {{number}}": "第 {{number}} 页",
|
||||
"highlight": "高亮",
|
||||
"underline": "下划线",
|
||||
"squiggly": "波浪线",
|
||||
"red": "红色",
|
||||
"violet": "紫色",
|
||||
"blue": "蓝色",
|
||||
"green": "绿色",
|
||||
"yellow": "黄色",
|
||||
"Select {{style}} style": "选择 {{style}} 样式",
|
||||
"Select {{color}} color": "选择 {{color}} 颜色",
|
||||
"Close Book": "关闭书籍",
|
||||
"Speed Reading": "快速阅读",
|
||||
"Close Speed Reading": "关闭快速阅读",
|
||||
"Authors": "作者",
|
||||
"Books": "书籍",
|
||||
"Groups": "分组",
|
||||
"Back to TTS Location": "回到朗读位置",
|
||||
"Metadata": "元数据",
|
||||
"Image viewer": "图片查看器",
|
||||
"Previous Image": "上一张图片",
|
||||
"Next Image": "下一张图片",
|
||||
"Zoomed": "已缩放",
|
||||
"Zoom level": "缩放级别",
|
||||
"Table viewer": "表格查看器",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "无法连接到 Readwise。请检查您的网络连接。",
|
||||
"Invalid Readwise access token": "无效的 Readwise 访问令牌",
|
||||
"Disconnected from Readwise": "已断开与 Readwise 的连接",
|
||||
"Never": "从不",
|
||||
"Readwise Settings": "Readwise 设置",
|
||||
"Connected to Readwise": "已连接到 Readwise",
|
||||
"Last synced: {{time}}": "上次同步时间:{{time}}",
|
||||
"Sync Enabled": "同步已启用",
|
||||
"Disconnect": "断开连接",
|
||||
"Connect your Readwise account to sync highlights.": "连接您的 Readwise 账户以同步高亮内容。",
|
||||
"Get your access token at": "获取您的访问令牌:",
|
||||
"Access Token": "访问令牌",
|
||||
"Paste your Readwise access token": "粘贴您的 Readwise 访问令牌",
|
||||
"Config": "配置",
|
||||
"Readwise Sync": "Readwise 同步",
|
||||
"Push Highlights": "推送高亮内容",
|
||||
"Highlights synced to Readwise": "高亮内容已同步到 Readwise",
|
||||
"Readwise sync failed: no internet connection": "Readwise 同步失败:无网络连接",
|
||||
"Readwise sync failed: {{error}}": "Readwise 同步失败:{{error}}",
|
||||
"System Screen Brightness": "系统屏幕亮度",
|
||||
"Page:": "页面:",
|
||||
"Page: {{number}}": "页面: {{number}}",
|
||||
"Annotation page number": "批注页码"
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
"Fit": "適應",
|
||||
"Reset {{settings}}": "重置{{settings}}",
|
||||
"Reset Settings": "重置設置",
|
||||
"{{count}} pages left in chapter_other": "本章剩餘 {{count}} 頁",
|
||||
"{{count}} pages left in chapter_other": "<1>本章剩餘 </1><0>{{count}}</0><1> 頁</1>",
|
||||
"Show Remaining Pages": "顯示剩餘頁數",
|
||||
"Source Han Serif CN": "思源宋體",
|
||||
"Huiwen-MinchoGBK": "匯文明朝體",
|
||||
@@ -595,11 +595,10 @@
|
||||
"Size": "大小",
|
||||
"Cover": "覆蓋",
|
||||
"Contain": "適應",
|
||||
"{{number}} pages left in chapter": "本章剩餘{{number}}頁",
|
||||
"{{number}} pages left in chapter": "<1>本章剩餘</1><0>{{number}}</0><1>頁</1>",
|
||||
"Device": "設備",
|
||||
"E-Ink Mode": "E-Ink 模式",
|
||||
"Highlight Colors": "高亮顏色",
|
||||
"Auto Screen Brightness": "自動螢幕亮度",
|
||||
"Pagination": "翻頁",
|
||||
"Disable Double Tap": "禁用雙擊",
|
||||
"Tap to Paginate": "點擊翻頁",
|
||||
@@ -822,7 +821,6 @@
|
||||
"Show Results": "顯示結果",
|
||||
"Clear search": "清除搜尋",
|
||||
"Clear search history": "清除搜尋歷史",
|
||||
"Quick action disabled": "快捷操作已停用",
|
||||
"Tap to Toggle Footer": "點擊切換頁尾",
|
||||
"Exported successfully": "匯出成功",
|
||||
"Book exported successfully.": "書籍匯出成功。",
|
||||
@@ -865,7 +863,6 @@
|
||||
"Array of annotations": "標註陣列",
|
||||
"Highlighted text": "標記文字",
|
||||
"Annotation note": "標註筆記",
|
||||
"Update time": "更新時間",
|
||||
"Date Format Tokens:": "日期格式標記:",
|
||||
"Year (4 digits)": "年(4位)",
|
||||
"Month (01-12)": "月(01-12)",
|
||||
@@ -880,5 +877,175 @@
|
||||
"Select Voice": "選擇語音",
|
||||
"Toggle Sticky Bottom TTS Bar": "切換固定TTS欄",
|
||||
"Display what I'm reading on Discord": "在Discord顯示閱讀狀態",
|
||||
"Show on Discord": "在Discord顯示"
|
||||
"Show on Discord": "在Discord顯示",
|
||||
"Instant {{action}}": "即時{{action}}",
|
||||
"Instant {{action}} Disabled": "即時{{action}}已停用",
|
||||
"Annotation": "註解",
|
||||
"Reset Template": "重置範本",
|
||||
"Annotation style": "標註樣式",
|
||||
"Annotation color": "標註顏色",
|
||||
"Annotation time": "標註時間",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "確定要重新索引這本書嗎?",
|
||||
"Enable AI in Settings": "在設定中啟用 AI",
|
||||
"Index This Book": "索引這本書",
|
||||
"Enable AI search and chat for this book": "為這本書啟用 AI 搜尋和聊天",
|
||||
"Start Indexing": "開始索引",
|
||||
"Indexing book...": "正在索引書籍...",
|
||||
"Preparing...": "準備中...",
|
||||
"Delete this conversation?": "刪除此對話?",
|
||||
"No conversations yet": "尚無對話",
|
||||
"Start a new chat to ask questions about this book": "開始新對話來詢問關於這本書的問題",
|
||||
"Rename": "重新命名",
|
||||
"New Chat": "新對話",
|
||||
"Chat": "聊天",
|
||||
"Please enter a model ID": "請輸入模型 ID",
|
||||
"Model not available or invalid": "模型不可用或無效",
|
||||
"Failed to validate model": "模型驗證失敗",
|
||||
"Couldn't connect to Ollama. Is it running?": "無法連線到 Ollama。它是否正在執行?",
|
||||
"Invalid API key or connection failed": "API 金鑰無效或連線失敗",
|
||||
"Connection failed": "連線失敗",
|
||||
"AI Assistant": "AI 助手",
|
||||
"Enable AI Assistant": "啟用 AI 助手",
|
||||
"Provider": "提供者",
|
||||
"Ollama (Local)": "Ollama(本機)",
|
||||
"AI Gateway (Cloud)": "AI 閘道(雲端)",
|
||||
"Ollama Configuration": "Ollama 設定",
|
||||
"Refresh Models": "重新整理模型",
|
||||
"AI Model": "AI 模型",
|
||||
"No models detected": "未偵測到模型",
|
||||
"AI Gateway Configuration": "AI 閘道設定",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "從一系列高品質、經濟實惠的 AI 模型中選擇。您也可以透過下方的「自訂模型」使用自己的模型。",
|
||||
"API Key": "API 金鑰",
|
||||
"Get Key": "取得金鑰",
|
||||
"Model": "模型",
|
||||
"Custom Model...": "自訂模型...",
|
||||
"Custom Model ID": "自訂模型 ID",
|
||||
"Validate": "驗證",
|
||||
"Model available": "模型可用",
|
||||
"Connection": "連線",
|
||||
"Test Connection": "測試連線",
|
||||
"Connected": "已連線",
|
||||
"Custom Colors": "自定義顏色",
|
||||
"Color E-Ink Mode": "彩色墨水屏模式",
|
||||
"Reading Ruler": "閱讀尺",
|
||||
"Enable Reading Ruler": "啟用閱讀尺",
|
||||
"Lines to Highlight": "突出顯示行數",
|
||||
"Ruler Color": "尺子顏色",
|
||||
"Command Palette": "命令面板",
|
||||
"Search settings and actions...": "搜尋設定和操作...",
|
||||
"No results found for": "未找到結果:",
|
||||
"Type to search settings and actions": "輸入以搜尋設定和操作",
|
||||
"Recent": "最近",
|
||||
"navigate": "導覽",
|
||||
"select": "選擇",
|
||||
"close": "關閉",
|
||||
"Search Settings": "搜尋設定",
|
||||
"Page Margins": "頁邊距",
|
||||
"AI Provider": "AI 服務商",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama 模型",
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "導覽",
|
||||
"Set status for {{count}} book(s)_other": "設定 {{count}} 本書的狀態",
|
||||
"Mark as Unread": "標示為未讀",
|
||||
"Mark as Finished": "標示為已讀",
|
||||
"Finished": "已讀",
|
||||
"Unread": "未讀",
|
||||
"Clear Status": "清除狀態",
|
||||
"Status": "狀態",
|
||||
"Loading": "正在載入",
|
||||
"Exit Paragraph Mode": "退出段落模式",
|
||||
"Paragraph Mode": "段落模式",
|
||||
"Embedding Model": "嵌入模型",
|
||||
"{{count}} book(s) synced_other": "{{count}} 本書已同步",
|
||||
"Unable to start RSVP": "無法啟動 RSVP",
|
||||
"RSVP not supported for PDF": "PDF 不支援 RSVP",
|
||||
"Select Chapter": "選擇章節",
|
||||
"Context": "上下文",
|
||||
"Ready": "準備就緒",
|
||||
"Chapter Progress": "章節進度",
|
||||
"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 閱讀",
|
||||
"Choose where to start reading": "選擇閱讀起始位置",
|
||||
"From Chapter Start": "從章節開始",
|
||||
"Start reading from the beginning of the chapter": "重新從本章節開始閱讀",
|
||||
"Resume": "繼續",
|
||||
"Continue from where you left off": "從上次離開的地方繼續",
|
||||
"From Current Page": "從當前頁",
|
||||
"Start from where you are currently reading": "從當前正在閱讀的位置開始",
|
||||
"From Selection": "從選取內容",
|
||||
"Speed Reading Mode": "快讀模式",
|
||||
"Scroll left": "向左捲動",
|
||||
"Scroll right": "向右捲動",
|
||||
"Library Sync Progress": "資料庫同步進度",
|
||||
"Back to library": "返回書庫",
|
||||
"Group by...": "分組方式...",
|
||||
"Export as Plain Text": "匯出為純文字",
|
||||
"Export as Markdown": "匯出為 Markdown",
|
||||
"Show Page Navigation Buttons": "顯示翻頁按鈕",
|
||||
"Page {{number}}": "第 {{number}} 頁",
|
||||
"highlight": "高亮",
|
||||
"underline": "底線",
|
||||
"squiggly": "波浪線",
|
||||
"red": "紅色",
|
||||
"violet": "紫色",
|
||||
"blue": "藍色",
|
||||
"green": "綠色",
|
||||
"yellow": "黃色",
|
||||
"Select {{style}} style": "選擇 {{style}} 樣式",
|
||||
"Select {{color}} color": "選擇 {{color}} 顏色",
|
||||
"Close Book": "關閉書籍",
|
||||
"Speed Reading": "快速閱讀",
|
||||
"Close Speed Reading": "關閉快速閱讀",
|
||||
"Authors": "作者",
|
||||
"Books": "書籍",
|
||||
"Groups": "分組",
|
||||
"Back to TTS Location": "回到朗讀位置",
|
||||
"Metadata": "元數據",
|
||||
"Image viewer": "圖片檢視器",
|
||||
"Previous Image": "上一張圖片",
|
||||
"Next Image": "下一張圖片",
|
||||
"Zoomed": "已縮放",
|
||||
"Zoom level": "縮放級別",
|
||||
"Table viewer": "表格檢視器",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "無法連接到 Readwise。請檢查您的網路連線。",
|
||||
"Invalid Readwise access token": "無效的 Readwise 存取權杖",
|
||||
"Disconnected from Readwise": "已斷開與 Readwise 的連線",
|
||||
"Never": "從不",
|
||||
"Readwise Settings": "Readwise 設定",
|
||||
"Connected to Readwise": "已連接到 Readwise",
|
||||
"Last synced: {{time}}": "上次同步時間:{{time}}",
|
||||
"Sync Enabled": "同步已啟用",
|
||||
"Disconnect": "斷開連線",
|
||||
"Connect your Readwise account to sync highlights.": "連接您的 Readwise 帳戶以同步高亮內容。",
|
||||
"Get your access token at": "獲取您的存取權杖:",
|
||||
"Access Token": "存取權杖",
|
||||
"Paste your Readwise access token": "貼上您的 Readwise 存取權杖",
|
||||
"Config": "配置",
|
||||
"Readwise Sync": "Readwise 同步",
|
||||
"Push Highlights": "推送高亮內容",
|
||||
"Highlights synced to Readwise": "高亮內容已同步到 Readwise",
|
||||
"Readwise sync failed: no internet connection": "Readwise 同步失敗:無網路連線",
|
||||
"Readwise sync failed: {{error}}": "Readwise 同步失敗:{{error}}",
|
||||
"System Screen Brightness": "系統螢幕亮度",
|
||||
"Page:": "頁面:",
|
||||
"Page: {{number}}": "頁面: {{number}}",
|
||||
"Annotation page number": "註解頁碼"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.101": {
|
||||
"date": "2026-02-28",
|
||||
"notes": [
|
||||
"Text-to-Speech: Fixed an issue where TTS could not continue to the next chapter",
|
||||
"Reading: Fixed an issue importing very large TXT files on iOS",
|
||||
"Reading: Fixed the layout of the reading ruler in scrolled mode",
|
||||
"Reading: Smoother page-turn animations for a more fluid experience",
|
||||
"Reading: Added a gallery mode to zoom and browse images within books",
|
||||
"Annotations: Page numbers are now displayed and exported with highlights and notes",
|
||||
"Annotations: Added support for exporting highlights to Readwise",
|
||||
"OPDS: Fixed missing book descriptions in OPDS feeds",
|
||||
"Android: Improved compatibility with older WebView versions on Android"
|
||||
]
|
||||
},
|
||||
"0.9.100": {
|
||||
"date": "2026-02-13",
|
||||
"notes": [
|
||||
"Text-to-Speech: Automatically moves to the next chapter when the current chapter finishes",
|
||||
"Reading: Fixed the reading ruler on Android",
|
||||
"Reading: Improved RSVP layout and stability",
|
||||
"Reading: Fixed an issue where some EPUB books could not adjust font size",
|
||||
"Annotations: Added a magnifier (loupe) when adjusting text selection",
|
||||
"Annotations: Text selection now snaps to word boundaries for better accuracy",
|
||||
"Annotations: Custom highlight colors now apply correctly in the sidebar",
|
||||
"Dictionary: Added a back button for easier navigation",
|
||||
"Dictionary: Fixed an issue where Dictionary and Wikipedia could not open on Android",
|
||||
"Metadata: Added collapsible sections in Book Details",
|
||||
"Metadata: Added support for parsing series information from Calibre-exported EPUB files",
|
||||
"Eink: Improved component layout and colors in E-ink mode",
|
||||
"Layout: Sidebar toggle button position is now persistent"
|
||||
]
|
||||
},
|
||||
"0.9.99": {
|
||||
"date": "2026-02-07",
|
||||
"notes": [
|
||||
"Reading: Added a reading ruler (line highlight) to help guide your eyes while reading",
|
||||
"Reading: Added paragraph-by-paragraph reading mode for more focused navigation",
|
||||
"Reading: Added a speed reading mode to support faster reading workflows",
|
||||
"Reading: Added support for gamepad input to turn pages",
|
||||
"Accessibility: Improved TalkBack support in dropdown menus for smoother navigation",
|
||||
"Accessibility: Page navigation buttons are now always visible to screen readers",
|
||||
"Accessibility: Reading progress is now updated correctly when navigating with screen readers",
|
||||
"Annotations: Expanded highlight customization with more color options",
|
||||
"Annotations: Added an option to export annotations as plain text",
|
||||
"Library: Added grouping by author and series for better library organization",
|
||||
"Library: Added support of marking books as finished",
|
||||
"Search: Added fuzzy search to quickly find settings even with partial keywords",
|
||||
"Table of Contents: Added page numbers for nested TOC entries",
|
||||
"Text-to-Speech: Added navigation controls to return to the current reading position",
|
||||
"Linux: Added an in-app updater for AppImage builds",
|
||||
"E-ink: Improved color support for color E-ink devices to display richer highlights"
|
||||
]
|
||||
},
|
||||
"0.9.98": {
|
||||
"date": "2026-01-21",
|
||||
"notes": [
|
||||
|
||||
@@ -159,6 +159,16 @@
|
||||
"name": "start-readest",
|
||||
"cmd": "cmd",
|
||||
"args": ["/C", "start", "", { "validator": "^.*Readest(.*)\\.exe$" }]
|
||||
},
|
||||
{
|
||||
"name": "chmod-appimage",
|
||||
"cmd": "chmod",
|
||||
"args": ["+x", { "validator": "^.*Readest(.*)\\.AppImage$" }]
|
||||
},
|
||||
{
|
||||
"name": "launch-appimage",
|
||||
"cmd": "setsid",
|
||||
"args": [{ "validator": "^.*Readest(.*)\\.AppImage$" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -54,17 +54,6 @@
|
||||
|
||||
DetailPrint "Thumbnail provider registered successfully."
|
||||
|
||||
Delete "$DESKTOP\Readest.lnk"
|
||||
Delete "$SMPROGRAMS\Readest\Readest.lnk"
|
||||
RMDir "$SMPROGRAMS\Readest"
|
||||
|
||||
; Create new shortcuts pointing to current installation
|
||||
CreateShortcut "$DESKTOP\Readest.lnk" "$INSTDIR\Readest.exe"
|
||||
CreateDirectory "$SMPROGRAMS\Readest"
|
||||
CreateShortcut "$SMPROGRAMS\Readest\Readest.lnk" "$INSTDIR\Readest.exe"
|
||||
|
||||
DetailPrint "Shortcuts updated."
|
||||
|
||||
; Refresh shell to apply changes - SHCNE_ASSOCCHANGED
|
||||
System::Call 'shell32::SHChangeNotify(i 0x08000000, i 0, p 0, p 0)'
|
||||
!macroend
|
||||
|
||||
@@ -21,6 +21,7 @@ const COMMANDS: &[&str] = &[
|
||||
"get_external_sdcard_path",
|
||||
"open_external_url",
|
||||
"select_directory",
|
||||
"get_storefront_region_code",
|
||||
"register_listener",
|
||||
"remove_listener",
|
||||
"request_manage_storage_permission",
|
||||
|
||||
+32
@@ -879,6 +879,38 @@ class NativeBridgePlugin: Plugin {
|
||||
invoke.reject("Failed to copy file: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func get_storefront_region_code(_ invoke: Invoke) {
|
||||
Task {
|
||||
if let storefront = await Storefront.current {
|
||||
invoke.resolve(["regionCode": storefront.countryCode])
|
||||
} else {
|
||||
invoke.reject("Failed to get region code")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func get_safe_area_insets(_ invoke: Invoke) {
|
||||
DispatchQueue.main.async {
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
let insets = window.safeAreaInsets
|
||||
invoke.resolve([
|
||||
"top": insets.top,
|
||||
"left": insets.left,
|
||||
"bottom": insets.bottom,
|
||||
"right": insets.right
|
||||
])
|
||||
} else {
|
||||
invoke.resolve([
|
||||
"error": "No window found",
|
||||
"top": 0,
|
||||
"left": 0,
|
||||
"bottom": 0,
|
||||
"right": 0
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_native_bridge")
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-storefront-region-code"
|
||||
description = "Enables the get_storefront_region_code command without any pre-configured scope."
|
||||
commands.allow = ["get_storefront_region_code"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-storefront-region-code"
|
||||
description = "Denies the get_storefront_region_code command without any pre-configured scope."
|
||||
commands.deny = ["get_storefront_region_code"]
|
||||
+27
@@ -26,6 +26,7 @@ Default permissions for the plugin
|
||||
- `allow-get-external-sdcard-path`
|
||||
- `allow-open-external-url`
|
||||
- `allow-select-directory`
|
||||
- `allow-get-storefront-region-code`
|
||||
- `allow-request-manage-storage-permission`
|
||||
- `allow-register-listener`
|
||||
- `allow-remove-listener`
|
||||
@@ -306,6 +307,32 @@ Denies the get_status_bar_height command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-storefront-region-code`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_storefront_region_code command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-get-storefront-region-code`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_storefront_region_code command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-sys-fonts-list`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -23,6 +23,7 @@ permissions = [
|
||||
"allow-get-external-sdcard-path",
|
||||
"allow-open-external-url",
|
||||
"allow-select-directory",
|
||||
"allow-get-storefront-region-code",
|
||||
"allow-request-manage-storage-permission",
|
||||
"allow-register-listener",
|
||||
"allow-remove-listener",
|
||||
|
||||
+14
-2
@@ -414,6 +414,18 @@
|
||||
"const": "deny-get-status-bar-height",
|
||||
"markdownDescription": "Denies the get_status_bar_height command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_storefront_region_code command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-storefront-region-code",
|
||||
"markdownDescription": "Enables the get_storefront_region_code command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_storefront_region_code command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-storefront-region-code",
|
||||
"markdownDescription": "Denies the get_storefront_region_code command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_sys_fonts_list command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -667,10 +679,10 @@
|
||||
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-is-available`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-is-available`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-get-storefront-region-code`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-is-available`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-is-available`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-get-storefront-region-code`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -186,6 +186,13 @@ pub(crate) async fn select_directory<R: Runtime>(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_storefront_region_code<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<GetStorefrontRegionCodeResponse> {
|
||||
app.native_bridge().get_storefront_region_code()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn request_manage_storage_permission<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
|
||||
@@ -137,6 +137,10 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn get_storefront_region_code(&self) -> crate::Result<GetStorefrontRegionCodeResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
) -> crate::Result<RequestManageStoragePermissionResponse> {
|
||||
|
||||
@@ -77,6 +77,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::get_external_sdcard_path,
|
||||
commands::open_external_url,
|
||||
commands::select_directory,
|
||||
commands::get_storefront_region_code,
|
||||
commands::request_manage_storage_permission,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
|
||||
@@ -224,6 +224,14 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_storefront_region_code(&self) -> crate::Result<GetStorefrontRegionCodeResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_storefront_region_code", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
|
||||
@@ -230,3 +230,10 @@ pub struct SelectDirectoryResponse {
|
||||
pub path: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetStorefrontRegionCodeResponse {
|
||||
pub region_code: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -253,54 +253,47 @@ pub fn run() {
|
||||
allow_dir_in_scopes(app, path);
|
||||
});
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
app.listen("window-ready", move |_| {
|
||||
let webview = app_handle.get_webview_window("main").unwrap();
|
||||
webview
|
||||
.eval("window.__READEST_CLI_ACCESS = true;")
|
||||
.expect("Failed to set cli access config");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let is_appimage = std::env::var("APPIMAGE").is_ok()
|
||||
|| std::env::current_exe()
|
||||
.map(|path| path.to_string_lossy().contains("/tmp/.mount_"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let script =
|
||||
format!("window.__READEST_UPDATER_DISABLED = {};", !is_appimage);
|
||||
webview
|
||||
.eval(&script)
|
||||
.expect("Failed to set updater disabled config");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
let _ = app.deep_link().register_all();
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
}
|
||||
|
||||
// Check for e-ink device on Android before building the window
|
||||
#[cfg(target_os = "android")]
|
||||
let is_eink = android::is_eink_device();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let is_eink = false;
|
||||
|
||||
let eink_script = if is_eink {
|
||||
"window.__READEST_IS_EINK = true;"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
#[cfg(desktop)]
|
||||
let cli_access = true;
|
||||
#[cfg(not(desktop))]
|
||||
let cli_access = false;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let is_appimage = std::env::var("APPIMAGE").is_ok()
|
||||
|| std::env::current_exe()
|
||||
.map(|path| path.to_string_lossy().contains("/tmp/.mount_"))
|
||||
.unwrap_or(false);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let is_appimage = false;
|
||||
|
||||
#[cfg(desktop)]
|
||||
let updater_disabled = std::env::var("READEST_DISABLE_UPDATER").is_ok();
|
||||
#[cfg(not(desktop))]
|
||||
let updater_disabled = false;
|
||||
|
||||
let init_script = format!(
|
||||
r#"
|
||||
{eink_script}
|
||||
if ({is_eink}) window.__READEST_IS_EINK = true;
|
||||
if ({cli_access}) window.__READEST_CLI_ACCESS = true;
|
||||
if ({is_appimage}) window.__READEST_IS_APPIMAGE = true;
|
||||
if ({updater_disabled}) window.__READEST_UPDATER_DISABLED = true;
|
||||
window.addEventListener('DOMContentLoaded', function() {{
|
||||
document.documentElement.classList.add('edge-to-edge');
|
||||
const isTauriLocal = window.location.protocol === 'tauri:' ||
|
||||
@@ -322,7 +315,10 @@ pub fn run() {
|
||||
}}
|
||||
}});
|
||||
"#,
|
||||
eink_script = eink_script
|
||||
is_eink = is_eink,
|
||||
cli_access = cli_access,
|
||||
is_appimage = is_appimage,
|
||||
updater_disabled = updater_disabled
|
||||
);
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://cdnjs.cloudflare.com https://storage.readest.com",
|
||||
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://cdnjs.cloudflare.com https://storage.readest.com",
|
||||
"frame-src": "'self' blob: asset: http://asset.localhost https://*.stripe.com",
|
||||
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com https://*.stripe.com"
|
||||
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' data: blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com https://*.stripe.com"
|
||||
},
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('lunr', () => {
|
||||
// mock lunr index for testing
|
||||
return {
|
||||
default: () => ({
|
||||
search: vi.fn(() => []),
|
||||
}),
|
||||
Index: {
|
||||
load: vi.fn(() => ({
|
||||
search: vi.fn(() => []),
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// mock the global indexedDB
|
||||
const createMockIDB = () => {
|
||||
const stores = new Map<string, Map<string, unknown>>();
|
||||
|
||||
return {
|
||||
open: vi.fn(() => ({
|
||||
result: {
|
||||
createObjectStore: vi.fn(),
|
||||
objectStoreNames: { contains: () => false },
|
||||
transaction: vi.fn(() => ({
|
||||
objectStore: vi.fn((name: string) => ({
|
||||
put: vi.fn((value: unknown, key: string) => {
|
||||
if (!stores.has(name)) stores.set(name, new Map());
|
||||
stores.get(name)!.set(key, value);
|
||||
return { onsuccess: null, onerror: null };
|
||||
}),
|
||||
get: vi.fn((key: string) => {
|
||||
const store = stores.get(name);
|
||||
const value = store?.get(key);
|
||||
return {
|
||||
onsuccess: null,
|
||||
onerror: null,
|
||||
result: value,
|
||||
};
|
||||
}),
|
||||
index: vi.fn(() => ({
|
||||
openCursor: vi.fn(() => ({
|
||||
onsuccess: null,
|
||||
onerror: null,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
oncomplete: null,
|
||||
onerror: null,
|
||||
})),
|
||||
},
|
||||
onsuccess: null,
|
||||
onerror: null,
|
||||
onupgradeneeded: null,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
vi.stubGlobal('indexedDB', createMockIDB());
|
||||
|
||||
import type { TextChunk } from '@/services/ai/types';
|
||||
|
||||
describe('AI Store', () => {
|
||||
describe('cosineSimilarity', () => {
|
||||
// inline implementation for testing since it's private
|
||||
const cosineSimilarity = (a: number[], b: number[]): number => {
|
||||
if (a.length !== b.length) return 0;
|
||||
let dotProduct = 0;
|
||||
let normA = 0;
|
||||
let normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dotProduct += a[i]! * b[i]!;
|
||||
normA += a[i]! * a[i]!;
|
||||
normB += b[i]! * b[i]!;
|
||||
}
|
||||
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denominator === 0 ? 0 : dotProduct / denominator;
|
||||
};
|
||||
|
||||
test('should return 1 for identical vectors', () => {
|
||||
const vec = [1, 2, 3, 4, 5];
|
||||
expect(cosineSimilarity(vec, vec)).toBeCloseTo(1);
|
||||
});
|
||||
|
||||
test('should return 0 for orthogonal vectors', () => {
|
||||
const a = [1, 0, 0];
|
||||
const b = [0, 1, 0];
|
||||
expect(cosineSimilarity(a, b)).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
test('should return -1 for opposite vectors', () => {
|
||||
const a = [1, 1, 1];
|
||||
const b = [-1, -1, -1];
|
||||
expect(cosineSimilarity(a, b)).toBeCloseTo(-1);
|
||||
});
|
||||
|
||||
test('should handle zero vectors', () => {
|
||||
const zero = [0, 0, 0];
|
||||
const vec = [1, 2, 3];
|
||||
expect(cosineSimilarity(zero, vec)).toBe(0);
|
||||
});
|
||||
|
||||
test('should return 0 for different length vectors', () => {
|
||||
const a = [1, 2];
|
||||
const b = [1, 2, 3];
|
||||
expect(cosineSimilarity(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunk operations', () => {
|
||||
const testChunk: TextChunk = {
|
||||
id: 'test-hash-0-0',
|
||||
bookHash: 'test-hash',
|
||||
sectionIndex: 0,
|
||||
chapterTitle: 'Test Chapter',
|
||||
pageNumber: 1,
|
||||
text: 'This is test content for the chunk.',
|
||||
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||
};
|
||||
|
||||
test('should create valid chunk structure', () => {
|
||||
expect(testChunk.id).toBe('test-hash-0-0');
|
||||
expect(testChunk.bookHash).toBe('test-hash');
|
||||
expect(testChunk.embedding).toHaveLength(5);
|
||||
});
|
||||
|
||||
test('should handle chunk without embedding', () => {
|
||||
const chunkNoEmbed: TextChunk = {
|
||||
id: 'test-hash-0-1',
|
||||
bookHash: 'test-hash',
|
||||
sectionIndex: 0,
|
||||
chapterTitle: 'Test Chapter',
|
||||
pageNumber: 1,
|
||||
text: 'Chunk without embedding.',
|
||||
};
|
||||
expect(chunkNoEmbed.embedding).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
|
||||
// mock the types module to avoid import issues
|
||||
vi.mock('../types', () => ({
|
||||
TextChunk: {},
|
||||
}));
|
||||
|
||||
import { extractTextFromDocument, chunkSection } from '@/services/ai/utils/chunker';
|
||||
|
||||
describe('AI Chunker', () => {
|
||||
const createDocument = (html: string): Document => {
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(`<!DOCTYPE html><html><body>${html}</body></html>`, 'text/html');
|
||||
};
|
||||
|
||||
describe('extractTextFromDocument', () => {
|
||||
test('should extract text from simple HTML', () => {
|
||||
const doc = createDocument('<p>Hello world</p>');
|
||||
const text = extractTextFromDocument(doc);
|
||||
expect(text).toBe('Hello world');
|
||||
});
|
||||
|
||||
test('should remove script and style tags', () => {
|
||||
const doc = createDocument(`
|
||||
<p>Visible text</p>
|
||||
<script>console.log('ignored')</script>
|
||||
<style>.hidden { display: none; }</style>
|
||||
<p>More text</p>
|
||||
`);
|
||||
const text = extractTextFromDocument(doc);
|
||||
expect(text).toContain('Visible text');
|
||||
expect(text).toContain('More text');
|
||||
expect(text).not.toContain('console.log');
|
||||
expect(text).not.toContain('.hidden');
|
||||
});
|
||||
|
||||
test('should handle empty document', () => {
|
||||
const doc = createDocument('');
|
||||
const text = extractTextFromDocument(doc);
|
||||
expect(text).toBe('');
|
||||
});
|
||||
|
||||
test('should trim whitespace', () => {
|
||||
const doc = createDocument(' <p> Text with spaces </p> ');
|
||||
const text = extractTextFromDocument(doc);
|
||||
expect(text).toBe('Text with spaces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunkSection', () => {
|
||||
const bookHash = 'test-hash';
|
||||
const sectionIndex = 0;
|
||||
const chapterTitle = 'Chapter 1';
|
||||
|
||||
test('should create single chunk for short text', () => {
|
||||
const doc = createDocument('<p>Short text that is less than max chunk size.</p>');
|
||||
const chunks = chunkSection(doc, sectionIndex, chapterTitle, bookHash, 0);
|
||||
|
||||
expect(chunks.length).toBe(1);
|
||||
expect(chunks[0]!.id).toBe(`${bookHash}-${sectionIndex}-0`);
|
||||
expect(chunks[0]!.bookHash).toBe(bookHash);
|
||||
expect(chunks[0]!.sectionIndex).toBe(sectionIndex);
|
||||
expect(chunks[0]!.chapterTitle).toBe(chapterTitle);
|
||||
});
|
||||
|
||||
test('should split long text into multiple chunks', () => {
|
||||
const longText = 'Lorem ipsum dolor sit amet. '.repeat(50);
|
||||
const doc = createDocument(`<p>${longText}</p>`);
|
||||
const chunks = chunkSection(doc, sectionIndex, chapterTitle, bookHash, 0);
|
||||
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
chunks.forEach((chunk, i) => {
|
||||
expect(chunk.id).toBe(`${bookHash}-${sectionIndex}-${i}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return empty array for empty document', () => {
|
||||
const doc = createDocument('');
|
||||
const chunks = chunkSection(doc, sectionIndex, chapterTitle, bookHash, 0);
|
||||
expect(chunks).toEqual([]);
|
||||
});
|
||||
|
||||
test('should respect custom chunk options', () => {
|
||||
const longText = 'Word '.repeat(100);
|
||||
const doc = createDocument(`<p>${longText}</p>`);
|
||||
const chunks = chunkSection(doc, sectionIndex, chapterTitle, bookHash, 0, {
|
||||
maxChunkSize: 100,
|
||||
minChunkSize: 20,
|
||||
});
|
||||
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
// all chunks except last should be close to maxChunkSize
|
||||
chunks.slice(0, -1).forEach((chunk) => {
|
||||
expect(chunk.text.length).toBeLessThanOrEqual(150); // allow some flexibility for break points
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
|
||||
// mock stores and dependencies before imports
|
||||
vi.mock('@/store/settingsStore', () => {
|
||||
const mockState = {
|
||||
settings: {
|
||||
aiSettings: {
|
||||
enabled: true,
|
||||
provider: 'ollama',
|
||||
ollamaBaseUrl: 'http://127.0.0.1:11434',
|
||||
ollamaModel: 'llama3.2',
|
||||
ollamaEmbeddingModel: 'nomic-embed-text',
|
||||
spoilerProtection: true,
|
||||
maxContextChunks: 5,
|
||||
indexingMode: 'on-demand',
|
||||
},
|
||||
},
|
||||
setSettings: vi.fn(),
|
||||
saveSettings: vi.fn(),
|
||||
};
|
||||
|
||||
const fn = vi.fn(() => mockState) as unknown as {
|
||||
(): typeof mockState;
|
||||
getState: () => typeof mockState;
|
||||
setState: (partial: Partial<typeof mockState>) => void;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
destroy: () => void;
|
||||
};
|
||||
fn.getState = () => mockState;
|
||||
fn.setState = vi.fn();
|
||||
fn.subscribe = vi.fn();
|
||||
fn.destroy = vi.fn();
|
||||
|
||||
return { useSettingsStore: fn };
|
||||
});
|
||||
|
||||
import type { AISettings } from '@/services/ai/types';
|
||||
import { DEFAULT_AI_SETTINGS, GATEWAY_MODELS } from '@/services/ai/constants';
|
||||
|
||||
describe('DEFAULT_AI_SETTINGS', () => {
|
||||
test('should have enabled set to false by default', () => {
|
||||
expect(DEFAULT_AI_SETTINGS.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('should have ollama as default provider', () => {
|
||||
expect(DEFAULT_AI_SETTINGS.provider).toBe('ollama');
|
||||
});
|
||||
|
||||
test('should have valid ollama defaults', () => {
|
||||
expect(DEFAULT_AI_SETTINGS.ollamaBaseUrl).toBe('http://127.0.0.1:11434');
|
||||
expect(DEFAULT_AI_SETTINGS.ollamaModel).toBe('llama3.2');
|
||||
expect(DEFAULT_AI_SETTINGS.ollamaEmbeddingModel).toBe('nomic-embed-text');
|
||||
});
|
||||
|
||||
test('should have spoiler protection enabled by default', () => {
|
||||
expect(DEFAULT_AI_SETTINGS.spoilerProtection).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Model constants', () => {
|
||||
test('GATEWAY_MODELS should have expected models', () => {
|
||||
expect(GATEWAY_MODELS.GEMINI_FLASH_LITE).toBeDefined();
|
||||
expect(GATEWAY_MODELS.GPT_5_NANO).toBeDefined();
|
||||
expect(GATEWAY_MODELS.LLAMA_4_SCOUT).toBeDefined();
|
||||
expect(GATEWAY_MODELS.GROK_4_1_FAST).toBeDefined();
|
||||
expect(GATEWAY_MODELS.DEEPSEEK_V3_2).toBeDefined();
|
||||
expect(GATEWAY_MODELS.QWEN_3_235B).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AISettings Type', () => {
|
||||
test('should allow creating valid settings object', () => {
|
||||
const settings: AISettings = {
|
||||
enabled: true,
|
||||
provider: 'ollama',
|
||||
ollamaBaseUrl: 'http://localhost:11434',
|
||||
ollamaModel: 'mistral',
|
||||
ollamaEmbeddingModel: 'nomic-embed-text',
|
||||
spoilerProtection: false,
|
||||
maxContextChunks: 10,
|
||||
indexingMode: 'background',
|
||||
};
|
||||
|
||||
expect(settings.enabled).toBe(true);
|
||||
expect(settings.provider).toBe('ollama');
|
||||
expect(settings.indexingMode).toBe('background');
|
||||
});
|
||||
|
||||
test('should support ai-gateway provider', () => {
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
provider: 'ai-gateway',
|
||||
aiGatewayApiKey: 'test-key',
|
||||
aiGatewayModel: 'openai/gpt-5.2',
|
||||
aiGatewayEmbeddingModel: 'openai/text-embedding-3-small',
|
||||
};
|
||||
|
||||
expect(settings.provider).toBe('ai-gateway');
|
||||
expect(settings.aiGatewayApiKey).toBe('test-key');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// mock fetch for provider tests
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
|
||||
// mock logger
|
||||
vi.mock('@/services/ai/logger', () => ({
|
||||
aiLogger: {
|
||||
provider: {
|
||||
init: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// mock ai-sdk-ollama
|
||||
vi.mock('ai-sdk-ollama', () => ({
|
||||
createOllama: vi.fn(() => {
|
||||
const ollamaFn = Object.assign(vi.fn(), {
|
||||
embeddingModel: vi.fn(),
|
||||
});
|
||||
return ollamaFn;
|
||||
}),
|
||||
}));
|
||||
|
||||
import { OllamaProvider } from '@/services/ai/providers/OllamaProvider';
|
||||
import { AIGatewayProvider } from '@/services/ai/providers/AIGatewayProvider';
|
||||
import { getAIProvider } from '@/services/ai/providers';
|
||||
import type { AISettings } from '@/services/ai/types';
|
||||
import { DEFAULT_AI_SETTINGS } from '@/services/ai/constants';
|
||||
|
||||
describe('OllamaProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should create provider with default settings', () => {
|
||||
const settings: AISettings = { ...DEFAULT_AI_SETTINGS, enabled: true };
|
||||
const provider = new OllamaProvider(settings);
|
||||
|
||||
expect(provider.id).toBe('ollama');
|
||||
expect(provider.name).toBe('Ollama (Local)');
|
||||
expect(provider.requiresAuth).toBe(false);
|
||||
});
|
||||
|
||||
test('isAvailable should return true when Ollama responds', async () => {
|
||||
mockFetch.mockResolvedValueOnce({ ok: true });
|
||||
const settings: AISettings = { ...DEFAULT_AI_SETTINGS, enabled: true };
|
||||
const provider = new OllamaProvider(settings);
|
||||
|
||||
const result = await provider.isAvailable();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('isAvailable should return false when Ollama not running', async () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
||||
const settings: AISettings = { ...DEFAULT_AI_SETTINGS, enabled: true };
|
||||
const provider = new OllamaProvider(settings);
|
||||
|
||||
const result = await provider.isAvailable();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('healthCheck should verify model exists', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({ models: [{ name: 'llama3.2:latest' }, { name: 'nomic-embed:latest' }] }),
|
||||
});
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
ollamaModel: 'llama3.2',
|
||||
ollamaEmbeddingModel: 'nomic-embed',
|
||||
};
|
||||
const provider = new OllamaProvider(settings);
|
||||
|
||||
const result = await provider.healthCheck();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('healthCheck should return false if model not found', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({ models: [{ name: 'other-model' }, { name: 'nomic-embed:latest' }] }),
|
||||
});
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
ollamaModel: 'llama3.2',
|
||||
ollamaEmbeddingModel: 'nomic-embed',
|
||||
};
|
||||
const provider = new OllamaProvider(settings);
|
||||
|
||||
const result = await provider.healthCheck();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AIGatewayProvider', () => {
|
||||
test('should throw if no API key', () => {
|
||||
const settings: AISettings = { ...DEFAULT_AI_SETTINGS, enabled: true, provider: 'ai-gateway' };
|
||||
|
||||
expect(() => new AIGatewayProvider(settings)).toThrow('API key required');
|
||||
});
|
||||
|
||||
test('should create provider with API key', () => {
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
provider: 'ai-gateway',
|
||||
aiGatewayApiKey: 'test-key',
|
||||
};
|
||||
const provider = new AIGatewayProvider(settings);
|
||||
|
||||
expect(provider.id).toBe('ai-gateway');
|
||||
expect(provider.name).toBe('AI Gateway (Cloud)');
|
||||
expect(provider.requiresAuth).toBe(true);
|
||||
});
|
||||
|
||||
test('isAvailable should return true if key exists', async () => {
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
provider: 'ai-gateway',
|
||||
aiGatewayApiKey: 'test-key',
|
||||
};
|
||||
const provider = new AIGatewayProvider(settings);
|
||||
|
||||
const result = await provider.isAvailable();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('isAvailable should return false if key does not exist', async () => {
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
provider: 'ai-gateway',
|
||||
aiGatewayApiKey: '',
|
||||
};
|
||||
|
||||
// provider throws on construction if no key, so we test via getAIProvider fallback
|
||||
expect(() => new AIGatewayProvider(settings)).toThrow('API key required');
|
||||
});
|
||||
|
||||
test('healthCheck should return false if key does not exist', async () => {
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
provider: 'ai-gateway',
|
||||
aiGatewayApiKey: 'valid-key',
|
||||
};
|
||||
const provider = new AIGatewayProvider(settings);
|
||||
|
||||
// override key after construction to simulate missing key check in healthCheck
|
||||
(provider as unknown as { settings: AISettings }).settings.aiGatewayApiKey = '';
|
||||
const result = await provider.healthCheck();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAIProvider', () => {
|
||||
test('should return OllamaProvider for ollama', () => {
|
||||
const settings: AISettings = { ...DEFAULT_AI_SETTINGS, enabled: true, provider: 'ollama' };
|
||||
const provider = getAIProvider(settings);
|
||||
|
||||
expect(provider.id).toBe('ollama');
|
||||
});
|
||||
|
||||
test('should return AIGatewayProvider for ai-gateway', () => {
|
||||
const settings: AISettings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
provider: 'ai-gateway',
|
||||
aiGatewayApiKey: 'test-key',
|
||||
};
|
||||
const provider = getAIProvider(settings);
|
||||
|
||||
expect(provider.id).toBe('ai-gateway');
|
||||
});
|
||||
|
||||
test('should throw for unknown provider', () => {
|
||||
const settings = {
|
||||
...DEFAULT_AI_SETTINGS,
|
||||
enabled: true,
|
||||
provider: 'unknown' as unknown,
|
||||
} as AISettings;
|
||||
|
||||
expect(() => getAIProvider(settings)).toThrow('Unknown provider');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
import { withRetry, withTimeout, AI_TIMEOUTS, AI_RETRY_CONFIGS } from '@/services/ai/utils/retry';
|
||||
|
||||
describe('withRetry', () => {
|
||||
test('should return result on first success', async () => {
|
||||
const fn = vi.fn().mockResolvedValue('success');
|
||||
const result = await withRetry(fn);
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should retry on failure and succeed', async () => {
|
||||
const fn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('fail 1'))
|
||||
.mockRejectedValueOnce(new Error('fail 2'))
|
||||
.mockResolvedValue('success');
|
||||
|
||||
const result = await withRetry(fn, { maxRetries: 3, baseDelayMs: 1, maxDelayMs: 5 });
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should throw after max retries', async () => {
|
||||
const fn = vi.fn().mockRejectedValue(new Error('always fails'));
|
||||
|
||||
await expect(withRetry(fn, { maxRetries: 2, baseDelayMs: 1, maxDelayMs: 5 })).rejects.toThrow(
|
||||
'always fails',
|
||||
);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
|
||||
});
|
||||
|
||||
test('should not retry on AbortError', async () => {
|
||||
const abortError = new Error('Aborted');
|
||||
abortError.name = 'AbortError';
|
||||
const fn = vi.fn().mockRejectedValue(abortError);
|
||||
|
||||
await expect(withRetry(fn, { maxRetries: 3, baseDelayMs: 1, maxDelayMs: 5 })).rejects.toThrow(
|
||||
'Aborted',
|
||||
);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should call onRetry callback', async () => {
|
||||
const onRetry = vi.fn();
|
||||
const fn = vi.fn().mockRejectedValueOnce(new Error('fail')).mockResolvedValue('success');
|
||||
|
||||
await withRetry(fn, { maxRetries: 2, baseDelayMs: 1, maxDelayMs: 5, onRetry });
|
||||
|
||||
expect(onRetry).toHaveBeenCalledWith(1, expect.any(Error));
|
||||
});
|
||||
});
|
||||
|
||||
describe('withTimeout', () => {
|
||||
test('should return result before timeout', async () => {
|
||||
const promise = Promise.resolve('fast');
|
||||
const result = await withTimeout(promise, 1000);
|
||||
expect(result).toBe('fast');
|
||||
});
|
||||
|
||||
test('should throw on timeout', async () => {
|
||||
const slowPromise = new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
await expect(withTimeout(slowPromise, 10)).rejects.toThrow('Timeout after 10ms');
|
||||
});
|
||||
|
||||
test('should use custom message', async () => {
|
||||
const slowPromise = new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
await expect(withTimeout(slowPromise, 10, 'Custom timeout')).rejects.toThrow('Custom timeout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI_TIMEOUTS', () => {
|
||||
test('should have correct timeout values', () => {
|
||||
expect(AI_TIMEOUTS.EMBEDDING_SINGLE).toBe(30_000);
|
||||
expect(AI_TIMEOUTS.EMBEDDING_BATCH).toBe(120_000);
|
||||
expect(AI_TIMEOUTS.CHAT_STREAM).toBe(60_000);
|
||||
expect(AI_TIMEOUTS.HEALTH_CHECK).toBe(5_000);
|
||||
expect(AI_TIMEOUTS.OLLAMA_CONNECT).toBe(5_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI_RETRY_CONFIGS', () => {
|
||||
test('should have correct retry configs', () => {
|
||||
expect(AI_RETRY_CONFIGS.EMBEDDING.maxRetries).toBe(3);
|
||||
expect(AI_RETRY_CONFIGS.CHAT.maxRetries).toBe(2);
|
||||
expect(AI_RETRY_CONFIGS.HEALTH_CHECK.maxRetries).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -68,6 +68,7 @@ vi.mock('@/services/environment', async (importOriginal) => {
|
||||
|
||||
import { EnvProvider } from '@/context/EnvContext';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { DEFAULT_SYSTEM_SETTINGS } from '@/services/constants';
|
||||
|
||||
function renderWithProviders(ui: React.ReactNode) {
|
||||
return render(
|
||||
@@ -81,12 +82,7 @@ describe('ProofreadRulesManager', () => {
|
||||
beforeEach(() => {
|
||||
// Reset stores
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
globalViewSettings: { proofreadRules: [] },
|
||||
kosync: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
settings: DEFAULT_SYSTEM_SETTINGS,
|
||||
});
|
||||
(useReaderStore.setState as unknown as (state: unknown) => void)({ viewStates: {} });
|
||||
useSidebarStore.setState({ sideBarBookKey: null });
|
||||
@@ -101,6 +97,7 @@ describe('ProofreadRulesManager', () => {
|
||||
// Arrange: populate stores
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
globalViewSettings: {
|
||||
proofreadRules: [
|
||||
{
|
||||
@@ -127,7 +124,6 @@ describe('ProofreadRulesManager', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
kosync: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -164,8 +160,8 @@ describe('ProofreadRulesManager', () => {
|
||||
// Arrange: populate stores with a selection rule persisted in book config
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
globalViewSettings: { proofreadRules: [] },
|
||||
kosync: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -284,10 +280,10 @@ describe('ProofreadRulesManager', () => {
|
||||
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
globalViewSettings: {
|
||||
proofreadRules: [libraryRule],
|
||||
},
|
||||
kosync: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -364,8 +360,8 @@ describe('ProofreadRulesManager', () => {
|
||||
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
globalViewSettings: { proofreadRules: [] },
|
||||
kosync: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -418,8 +414,8 @@ describe('ProofreadRulesManager', () => {
|
||||
// Arrange stores
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
globalViewSettings: { proofreadRules: [] },
|
||||
kosync: { enabled: false },
|
||||
},
|
||||
});
|
||||
(useReaderStore.setState as unknown as (state: unknown) => void)({
|
||||
@@ -453,8 +449,8 @@ describe('ProofreadRulesManager', () => {
|
||||
it('shows empty state messages when no rules exist', async () => {
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
globalViewSettings: { proofreadRules: [] },
|
||||
kosync: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -509,10 +505,10 @@ describe('ProofreadRulesManager', () => {
|
||||
|
||||
(useSettingsStore.setState as unknown as (state: unknown) => void)({
|
||||
settings: {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
globalViewSettings: {
|
||||
proofreadRules: [libraryRule],
|
||||
},
|
||||
kosync: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { DocumentLoader } from '@/libs/document';
|
||||
import type { BookDoc } from '@/libs/document';
|
||||
import type { FoliateView } from '@/types/view';
|
||||
import { wrappedFoliateView } from '@/types/view';
|
||||
|
||||
// jsdom's Blob doesn't implement arrayBuffer(), polyfill it via FileReader
|
||||
if (typeof Blob.prototype.arrayBuffer !== 'function') {
|
||||
Blob.prototype.arrayBuffer = function () {
|
||||
return new Promise((res, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => res(reader.result as ArrayBuffer);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsArrayBuffer(this);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Register a stub paginator custom element so View.open() doesn't fail in jsdom
|
||||
if (!customElements.get('foliate-paginator')) {
|
||||
customElements.define(
|
||||
'foliate-paginator',
|
||||
class extends HTMLElement {
|
||||
override setAttribute() {}
|
||||
override addEventListener() {}
|
||||
open() {}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Mock the paginator module import so View doesn't try to load the real one
|
||||
vi.mock('foliate-js/paginator.js', () => ({}));
|
||||
|
||||
let book: BookDoc;
|
||||
let view: FoliateView;
|
||||
let totalSections: number;
|
||||
|
||||
const loadEPUB = async () => {
|
||||
const epubPath = resolve(__dirname, '../fixtures/sample-alice.epub');
|
||||
const buffer = readFileSync(epubPath);
|
||||
const file = new File([buffer], 'sample-alice.epub', { type: 'application/epub+zip' });
|
||||
const loader = new DocumentLoader(file);
|
||||
const { book } = await loader.open();
|
||||
return book;
|
||||
};
|
||||
|
||||
describe('getCFIProgress with real EPUB', () => {
|
||||
beforeAll(async () => {
|
||||
book = await loadEPUB();
|
||||
totalSections = book.sections!.length;
|
||||
|
||||
// Import View and create the element, wrapping it like the app does
|
||||
await import('foliate-js/view.js');
|
||||
const rawView = document.createElement('foliate-view') as FoliateView;
|
||||
view = wrappedFoliateView(rawView);
|
||||
await view.open(book);
|
||||
}, 30000);
|
||||
|
||||
it('should load the EPUB with sections', () => {
|
||||
expect(book.sections!.length).toBeGreaterThan(0);
|
||||
expect(view.book).toBe(book);
|
||||
});
|
||||
|
||||
it('should return a valid progress object with all fields', async () => {
|
||||
const sectionIndex = book.sections!.findIndex((s) => s.linear !== 'no' && s.createDocument);
|
||||
const doc = await book.sections![sectionIndex]!.createDocument();
|
||||
const body = doc.body ?? doc.documentElement;
|
||||
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
const firstTextNode = walker.nextNode();
|
||||
expect(firstTextNode).not.toBeNull();
|
||||
|
||||
const range = doc.createRange();
|
||||
range.setStart(firstTextNode!, 0);
|
||||
range.setEnd(firstTextNode!, 0);
|
||||
|
||||
const cfi = view.getCFI(sectionIndex, range);
|
||||
const result = await view.getCFIProgress(cfi);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
// fraction is the overall book progress
|
||||
expect(result!.fraction).toBeGreaterThanOrEqual(0);
|
||||
expect(result!.fraction).toBeLessThanOrEqual(1);
|
||||
// section info
|
||||
expect(result!.section.current).toBe(sectionIndex);
|
||||
expect(result!.section.total).toBe(totalSections);
|
||||
// location info
|
||||
expect(result!.location.current).toBeGreaterThanOrEqual(0);
|
||||
expect(result!.location.next).toBeGreaterThanOrEqual(result!.location.current);
|
||||
expect(result!.location.total).toBeGreaterThan(0);
|
||||
// time info
|
||||
expect(result!.time.section).toBeGreaterThanOrEqual(0);
|
||||
expect(result!.time.total).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should return the smallest fraction for a CFI at the start of a section', async () => {
|
||||
const sectionIndex = book.sections!.findIndex((s) => s.linear !== 'no' && s.createDocument);
|
||||
expect(sectionIndex).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const doc = await book.sections![sectionIndex]!.createDocument();
|
||||
const body = doc.body ?? doc.documentElement;
|
||||
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
const firstTextNode = walker.nextNode();
|
||||
expect(firstTextNode).not.toBeNull();
|
||||
|
||||
const range = doc.createRange();
|
||||
range.setStart(firstTextNode!, 0);
|
||||
range.setEnd(firstTextNode!, 0);
|
||||
|
||||
const cfi = view.getCFI(sectionIndex, range);
|
||||
const result = await view.getCFIProgress(cfi);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.section.current).toBe(sectionIndex);
|
||||
// At the start of a section, fraction should equal the section's start fraction
|
||||
// (i.e. only prior sections' sizes contribute)
|
||||
expect(result!.fraction).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should return a larger fraction for a CFI at the end of a section', async () => {
|
||||
const sectionIndex = book.sections!.findIndex((s) => s.linear !== 'no' && s.createDocument);
|
||||
const doc = await book.sections![sectionIndex]!.createDocument();
|
||||
const body = doc.body ?? doc.documentElement;
|
||||
|
||||
// Get start CFI
|
||||
const walkerStart = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
const firstTextNode = walkerStart.nextNode()!;
|
||||
const startRange = doc.createRange();
|
||||
startRange.setStart(firstTextNode, 0);
|
||||
startRange.setEnd(firstTextNode, 0);
|
||||
const startCfi = view.getCFI(sectionIndex, startRange);
|
||||
const startResult = await view.getCFIProgress(startCfi);
|
||||
|
||||
// Get end CFI
|
||||
const walkerEnd = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
let lastTextNode: Node | null = null;
|
||||
for (let node = walkerEnd.nextNode(); node; node = walkerEnd.nextNode()) {
|
||||
lastTextNode = node;
|
||||
}
|
||||
expect(lastTextNode).not.toBeNull();
|
||||
const endRange = doc.createRange();
|
||||
endRange.setStart(lastTextNode!, lastTextNode!.textContent!.length);
|
||||
endRange.setEnd(lastTextNode!, lastTextNode!.textContent!.length);
|
||||
const endCfi = view.getCFI(sectionIndex, endRange);
|
||||
const endResult = await view.getCFIProgress(endCfi);
|
||||
|
||||
expect(startResult).not.toBeNull();
|
||||
expect(endResult).not.toBeNull();
|
||||
expect(endResult!.section.current).toBe(sectionIndex);
|
||||
expect(endResult!.fraction).toBeGreaterThan(startResult!.fraction);
|
||||
});
|
||||
|
||||
it('should return a fraction between start and end for a midpoint CFI', async () => {
|
||||
const sectionIndex = book.sections!.findIndex((s) => s.linear !== 'no' && s.size > 1000);
|
||||
expect(sectionIndex).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const doc = await book.sections![sectionIndex]!.createDocument();
|
||||
const body = doc.body ?? doc.documentElement;
|
||||
|
||||
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
const textNodes: Node[] = [];
|
||||
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
expect(textNodes.length).toBeGreaterThan(0);
|
||||
|
||||
const midNodeIndex = Math.floor(textNodes.length / 2);
|
||||
const midNode = textNodes[midNodeIndex]!;
|
||||
const midOffset = Math.floor((midNode.textContent?.length ?? 0) / 2);
|
||||
|
||||
const range = doc.createRange();
|
||||
range.setStart(midNode, midOffset);
|
||||
range.setEnd(midNode, midOffset);
|
||||
|
||||
const cfi = view.getCFI(sectionIndex, range);
|
||||
const result = await view.getCFIProgress(cfi);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.section.current).toBe(sectionIndex);
|
||||
expect(result!.fraction).toBeGreaterThan(0);
|
||||
expect(result!.fraction).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should produce consistent results for the same CFI', async () => {
|
||||
const sectionIndex = book.sections!.findIndex((s) => s.linear !== 'no' && s.size > 500);
|
||||
const doc = await book.sections![sectionIndex]!.createDocument();
|
||||
const body = doc.body ?? doc.documentElement;
|
||||
|
||||
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
const node = walker.nextNode();
|
||||
expect(node).not.toBeNull();
|
||||
|
||||
const range = doc.createRange();
|
||||
const offset = Math.min(3, node!.textContent!.length);
|
||||
range.setStart(node!, offset);
|
||||
range.setEnd(node!, offset);
|
||||
|
||||
const cfi = view.getCFI(sectionIndex, range);
|
||||
|
||||
const result1 = await view.getCFIProgress(cfi);
|
||||
const result2 = await view.getCFIProgress(cfi);
|
||||
|
||||
expect(result1).not.toBeNull();
|
||||
expect(result2).not.toBeNull();
|
||||
expect(result1!.fraction).toBe(result2!.fraction);
|
||||
expect(result1!.section.current).toBe(result2!.section.current);
|
||||
expect(result1!.location.current).toBe(result2!.location.current);
|
||||
});
|
||||
|
||||
it('should return increasing fractions for successive CFIs in the same section', async () => {
|
||||
const sectionIndex = book.sections!.findIndex((s) => s.linear !== 'no' && s.size > 2000);
|
||||
expect(sectionIndex).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const doc = await book.sections![sectionIndex]!.createDocument();
|
||||
const body = doc.body ?? doc.documentElement;
|
||||
|
||||
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
const textNodes: Node[] = [];
|
||||
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
|
||||
const positions = [0, Math.floor(textNodes.length / 4), Math.floor((textNodes.length * 3) / 4)];
|
||||
const fractions: number[] = [];
|
||||
|
||||
for (const pos of positions) {
|
||||
const node = textNodes[pos]!;
|
||||
const range = doc.createRange();
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, 0);
|
||||
const cfi = view.getCFI(sectionIndex, range);
|
||||
const result = await view.getCFIProgress(cfi);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.section.current).toBe(sectionIndex);
|
||||
fractions.push(result!.fraction);
|
||||
}
|
||||
|
||||
for (let i = 1; i < fractions.length; i++) {
|
||||
expect(fractions[i]).toBeGreaterThan(fractions[i - 1]!);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return increasing fractions across different sections', async () => {
|
||||
const linearSections = book
|
||||
.sections!.map((s, i) => ({ s, i }))
|
||||
.filter(({ s }) => s.linear !== 'no' && s.size > 0);
|
||||
|
||||
expect(linearSections.length).toBeGreaterThan(1);
|
||||
|
||||
const fractions: number[] = [];
|
||||
|
||||
for (const { s, i } of linearSections.slice(0, 3)) {
|
||||
const doc = await s.createDocument();
|
||||
const body = doc.body ?? doc.documentElement;
|
||||
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
const firstTextNode = walker.nextNode();
|
||||
if (!firstTextNode) continue;
|
||||
|
||||
const range = doc.createRange();
|
||||
range.setStart(firstTextNode, 0);
|
||||
range.setEnd(firstTextNode, 0);
|
||||
|
||||
const cfi = view.getCFI(i, range);
|
||||
const result = await view.getCFIProgress(cfi);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.section.current).toBe(i);
|
||||
expect(result!.fraction).toBeGreaterThanOrEqual(0);
|
||||
expect(result!.fraction).toBeLessThanOrEqual(1);
|
||||
fractions.push(result!.fraction);
|
||||
}
|
||||
|
||||
// Fractions across sections should be increasing
|
||||
for (let i = 1; i < fractions.length; i++) {
|
||||
expect(fractions[i]).toBeGreaterThan(fractions[i - 1]!);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return null for an invalid CFI', async () => {
|
||||
const result = await view.getCFIProgress('epubcfi(/99/999!/0)');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return progress for real CFIs', async () => {
|
||||
const expectedData = [
|
||||
{ cfi: 'epubcfi(/99/999!/0)', fraction: null },
|
||||
{ cfi: 'epubcfi(/6/4!/4/2/6,/1:0,/1:13)', fraction: 0.009, page: 1 },
|
||||
{ cfi: 'epubcfi(/6/8!/4/2/2[chapter_458]/4/18,/1:0,/1:681)', fraction: 0.045, page: 5 },
|
||||
{ cfi: 'epubcfi(/6/10!/4/2/2[chapter_460]/2,/3:1,/3:18)', fraction: 0.098, page: 11 },
|
||||
{ cfi: 'epubcfi(/6/10!/4/2/2[chapter_460]/4/70,/1:0,/1:261)', fraction: 0.161, page: 19 },
|
||||
{ cfi: 'epubcfi(/6/12!/4/2/2[chapter_462]/4/22,/1:0,/1:156)', fraction: 0.179, page: 21 },
|
||||
{ cfi: 'epubcfi(/6/18!/4/2/2[chapter_468]/4/6,/1:0,/1:65)', fraction: 0.411, page: 46 },
|
||||
{ cfi: 'epubcfi(/6/18!/4/2/2[chapter_468]/4/94,/1:1,/1:30)', fraction: 0.446, page: 51 },
|
||||
{ cfi: 'epubcfi(/6/18!/4/2/2[chapter_468]/4/134,/1:0,/1:58)', fraction: 0.473, page: 54 },
|
||||
{ cfi: 'epubcfi(/6/18!/4/2/2[chapter_468]/4/170,/1:0,/1:124)', fraction: 0.491, page: 55 },
|
||||
{ cfi: 'epubcfi(/6/26!/4/2/2[chapter_476]/4/120,/1:0,/1:117)', fraction: 0.804, page: 91 },
|
||||
{ cfi: 'epubcfi(/6/30!/4/2/2[chapter_480]/4,/82/1:0,/86/1:26)', fraction: 0.955, page: 107 },
|
||||
{ cfi: 'epubcfi(/6/30!/4/2/2[chapter_480]/4/134,/1:1,/1:118)', fraction: 0.955, page: 108 },
|
||||
{ cfi: 'epubcfi(/6/30!/4/2/2[chapter_480]/4/134,/1:119,/1:358)', fraction: 0.955, page: 108 },
|
||||
];
|
||||
|
||||
for (const { cfi, fraction, page } of expectedData) {
|
||||
const result = await view.getCFIProgress(cfi);
|
||||
if (fraction === null) {
|
||||
expect(result).toBeNull();
|
||||
} else {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBeCloseTo(fraction, 1);
|
||||
expect(result!.location.current).toBe(page! - 1);
|
||||
expect(Math.abs(result!.fraction - fraction)).toBeLessThan(0.006);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { PageProgress } from 'foliate-js/progress.js';
|
||||
|
||||
const createHTML = (html: string): Document => {
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(`<!DOCTYPE html><html><body>${html}</body></html>`, 'text/html');
|
||||
};
|
||||
|
||||
const makeBook = (sections: { html: string }[]) => ({
|
||||
sections: sections.map(({ html }) => ({
|
||||
createDocument: () => Promise.resolve(createHTML(html)),
|
||||
})),
|
||||
});
|
||||
|
||||
describe('PageProgress', () => {
|
||||
describe('getProgress', () => {
|
||||
it('should return fraction 0 when anchor targets the start of a section', async () => {
|
||||
const book = makeBook([{ html: '<p>Hello world</p>' }]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: (doc: Document) => {
|
||||
const text = doc.body.querySelector('p')!.firstChild!;
|
||||
const range = doc.createRange();
|
||||
range.setStart(text, 0);
|
||||
range.setEnd(text, 0);
|
||||
return range;
|
||||
},
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-start');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBe(0);
|
||||
expect(result!.index).toBe(0);
|
||||
});
|
||||
|
||||
it('should return fraction 1 when anchor targets the end of a section', async () => {
|
||||
const book = makeBook([{ html: '<p>Hello world</p>' }]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: (doc: Document) => {
|
||||
const text = doc.body.querySelector('p')!.firstChild!;
|
||||
const range = doc.createRange();
|
||||
range.setStart(text, text.textContent!.length);
|
||||
range.setEnd(text, text.textContent!.length);
|
||||
return range;
|
||||
},
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-end');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBe(1);
|
||||
expect(result!.index).toBe(0);
|
||||
});
|
||||
|
||||
it('should return correct fraction for a midpoint in a section', async () => {
|
||||
const book = makeBook([{ html: '<p>abcdefghij</p>' }]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: (doc: Document) => {
|
||||
const text = doc.body.querySelector('p')!.firstChild!;
|
||||
const range = doc.createRange();
|
||||
range.setStart(text, 5);
|
||||
range.setEnd(text, 5);
|
||||
return range;
|
||||
},
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-mid');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBe(0.5);
|
||||
expect(result!.index).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle multiple text nodes across elements', async () => {
|
||||
const book = makeBook([{ html: '<p>aaaa</p><p>bbbb</p><p>cccc</p>' }]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: (doc: Document) => {
|
||||
// target start of second <p>, which is at offset 4 of 12 total chars
|
||||
const text = doc.body.querySelectorAll('p')[1]!.firstChild!;
|
||||
const range = doc.createRange();
|
||||
range.setStart(text, 0);
|
||||
range.setEnd(text, 0);
|
||||
return range;
|
||||
},
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-multi');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBeCloseTo(4 / 12);
|
||||
expect(result!.index).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle anchor returning an Element instead of a Range', async () => {
|
||||
const book = makeBook([{ html: '<p>first</p><p id="target">second</p><p>third</p>' }]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: (doc: Document) => doc.getElementById('target')!,
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-element');
|
||||
expect(result).not.toBeNull();
|
||||
// "first" is 5 chars, "second" starts at offset 5 of 16 total
|
||||
expect(result!.fraction).toBeCloseTo(5 / 16);
|
||||
expect(result!.index).toBe(0);
|
||||
});
|
||||
|
||||
it('should return correct index for non-zero section', async () => {
|
||||
const book = makeBook([
|
||||
{ html: '<p>chapter one</p>' },
|
||||
{ html: '<p>chapter two content</p>' },
|
||||
]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 1,
|
||||
anchor: (doc: Document) => {
|
||||
const text = doc.body.querySelector('p')!.firstChild!;
|
||||
const range = doc.createRange();
|
||||
range.setStart(text, 0);
|
||||
range.setEnd(text, 0);
|
||||
return range;
|
||||
},
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-section1');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBe(0);
|
||||
expect(result!.index).toBe(1);
|
||||
});
|
||||
|
||||
it('should return null when resolveNavigation returns no index', async () => {
|
||||
const book = makeBook([{ html: '<p>text</p>' }]);
|
||||
const resolveNavigation = (): { index: undefined; anchor: undefined } => ({
|
||||
index: undefined,
|
||||
anchor: undefined,
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('bad-cfi');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when resolveNavigation returns no anchor', async () => {
|
||||
const book = makeBook([{ html: '<p>text</p>' }]);
|
||||
const resolveNavigation = (): { index: number; anchor: undefined } => ({
|
||||
index: 0,
|
||||
anchor: undefined,
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('no-anchor');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when section has no createDocument', async () => {
|
||||
const book = { sections: [{}] };
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: () => null,
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('no-doc');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return fraction 0 for an empty document', async () => {
|
||||
const book = makeBook([{ html: '' }]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: (doc: Document) => {
|
||||
const range = doc.createRange();
|
||||
range.selectNodeContents(doc.body);
|
||||
return range;
|
||||
},
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-empty');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBe(0);
|
||||
});
|
||||
|
||||
it('should return null and log error when resolveNavigation throws', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const book = makeBook([{ html: '<p>text</p>' }]);
|
||||
const resolveNavigation = () => {
|
||||
throw new Error('bad cfi');
|
||||
};
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('invalid');
|
||||
expect(result).toBeNull();
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle nested elements correctly', async () => {
|
||||
const book = makeBook([{ html: '<div><span>ab</span><em>cd</em></div><p>ef</p>' }]);
|
||||
const resolveNavigation = () => ({
|
||||
index: 0,
|
||||
anchor: (doc: Document) => {
|
||||
// target the <em> text node "cd" at offset 0, which is at char 2 of 6
|
||||
const text = doc.body.querySelector('em')!.firstChild!;
|
||||
const range = doc.createRange();
|
||||
range.setStart(text, 0);
|
||||
range.setEnd(text, 2);
|
||||
return range;
|
||||
},
|
||||
});
|
||||
|
||||
const pp = new PageProgress(book, resolveNavigation);
|
||||
const result = await pp.getProgress('cfi-nested');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fraction).toBeCloseTo(2 / 6);
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
@@ -0,0 +1,841 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
parseAuthors,
|
||||
createBookGroups,
|
||||
createWithinGroupSorter,
|
||||
createGroupSorter,
|
||||
getGroupSortValue,
|
||||
createBookSorter,
|
||||
ensureLibrarySortByType,
|
||||
ensureLibraryGroupByType,
|
||||
findGroupById,
|
||||
getGroupDisplayName,
|
||||
} from '../../app/library/utils/libraryUtils';
|
||||
import { Book, BooksGroup } from '../../types/book';
|
||||
import { LibraryGroupByType, LibrarySortByType } from '../../types/settings';
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
|
||||
// Helper to create mock books with minimal required fields
|
||||
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('parseAuthors', () => {
|
||||
it('should return single author as array', () => {
|
||||
expect(parseAuthors('John Smith')).toEqual(['John Smith']);
|
||||
});
|
||||
|
||||
it('should split authors by comma', () => {
|
||||
expect(parseAuthors('John Smith, Jane Doe')).toEqual(['John Smith', 'Jane Doe']);
|
||||
});
|
||||
|
||||
it('should split authors by ampersand', () => {
|
||||
expect(parseAuthors('John Smith & Jane Doe')).toEqual(['John Smith', 'Jane Doe']);
|
||||
});
|
||||
|
||||
it('should split authors by "and"', () => {
|
||||
expect(parseAuthors('John Smith and Jane Doe')).toEqual(['John Smith', 'Jane Doe']);
|
||||
});
|
||||
|
||||
it('should handle mixed separators', () => {
|
||||
expect(parseAuthors('John Smith, Jane Doe & Bob Wilson')).toEqual([
|
||||
'John Smith',
|
||||
'Jane Doe',
|
||||
'Bob Wilson',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle "and" with commas', () => {
|
||||
expect(parseAuthors('John Smith, Jane Doe, and Bob Wilson')).toEqual([
|
||||
'John Smith',
|
||||
'Jane Doe',
|
||||
'Bob Wilson',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should trim whitespace from author names', () => {
|
||||
expect(parseAuthors(' John Smith , Jane Doe ')).toEqual(['John Smith', 'Jane Doe']);
|
||||
});
|
||||
|
||||
it('should return empty array for empty string', () => {
|
||||
expect(parseAuthors('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for whitespace only', () => {
|
||||
expect(parseAuthors(' ')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single author with extra spaces', () => {
|
||||
expect(parseAuthors(' John Smith ')).toEqual(['John Smith']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBookGroups', () => {
|
||||
describe('groupBy: none', () => {
|
||||
it('should return all books as flat list', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1' }),
|
||||
createMockBook({ hash: '2', title: 'Book 2' }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.None);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.every((item) => 'format' in item)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter out deleted books', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1' }),
|
||||
createMockBook({ hash: '2', title: 'Book 2', deletedAt: Date.now() }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.None);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupBy: series', () => {
|
||||
it('should group books by series name', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1', metadata: { series: 'Series A' } }),
|
||||
createMockBook({ hash: '2', title: 'Book 2', metadata: { series: 'Series A' } }),
|
||||
createMockBook({ hash: '3', title: 'Book 3', metadata: { series: 'Series B' } }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Series);
|
||||
|
||||
const groups = result.filter((item): item is BooksGroup => 'books' in item);
|
||||
expect(groups).toHaveLength(2);
|
||||
|
||||
const seriesA = groups.find((g) => g.name === 'Series A');
|
||||
expect(seriesA?.books).toHaveLength(2);
|
||||
|
||||
const seriesB = groups.find((g) => g.name === 'Series B');
|
||||
expect(seriesB?.books).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should leave books without series as ungrouped items', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1', metadata: { series: 'Series A' } }),
|
||||
createMockBook({ hash: '2', title: 'Book 2' }), // No series
|
||||
createMockBook({ hash: '3', title: 'Book 3', metadata: {} }), // Empty metadata
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Series);
|
||||
|
||||
const groups = result.filter((item): item is BooksGroup => 'books' in item);
|
||||
const ungrouped = result.filter((item): item is Book => 'format' in item);
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(ungrouped).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle empty series string as ungrouped', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1', metadata: { series: '' } }),
|
||||
createMockBook({ hash: '2', title: 'Book 2', metadata: { series: ' ' } }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Series);
|
||||
|
||||
const groups = result.filter((item): item is BooksGroup => 'books' in item);
|
||||
const ungrouped = result.filter((item): item is Book => 'format' in item);
|
||||
|
||||
expect(groups).toHaveLength(0);
|
||||
expect(ungrouped).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should set group updatedAt to most recent book', () => {
|
||||
const books = [
|
||||
createMockBook({
|
||||
hash: '1',
|
||||
title: 'Book 1',
|
||||
metadata: { series: 'Series A' },
|
||||
updatedAt: 1000,
|
||||
}),
|
||||
createMockBook({
|
||||
hash: '2',
|
||||
title: 'Book 2',
|
||||
metadata: { series: 'Series A' },
|
||||
updatedAt: 2000,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Series);
|
||||
const group = result.find((item): item is BooksGroup => 'books' in item);
|
||||
|
||||
expect(group?.updatedAt).toBe(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupBy: author', () => {
|
||||
it('should group books by author', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1', author: 'Author A' }),
|
||||
createMockBook({ hash: '2', title: 'Book 2', author: 'Author A' }),
|
||||
createMockBook({ hash: '3', title: 'Book 3', author: 'Author B' }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Author);
|
||||
|
||||
const groups = result.filter((item): item is BooksGroup => 'books' in item);
|
||||
expect(groups).toHaveLength(2);
|
||||
|
||||
const authorA = groups.find((g) => g.name === 'Author A');
|
||||
expect(authorA?.books).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should place book in multiple groups for multiple authors', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1', author: 'John Smith, Jane Doe' }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Author);
|
||||
|
||||
const groups = result.filter((item): item is BooksGroup => 'books' in item);
|
||||
expect(groups).toHaveLength(2);
|
||||
|
||||
const john = groups.find((g) => g.name === 'John Smith');
|
||||
const jane = groups.find((g) => g.name === 'Jane Doe');
|
||||
|
||||
expect(john?.books).toHaveLength(1);
|
||||
expect(jane?.books).toHaveLength(1);
|
||||
expect(john?.books[0]!.hash).toBe(jane?.books[0]!.hash);
|
||||
});
|
||||
|
||||
it('should leave books without author as ungrouped', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1', author: 'Author A' }),
|
||||
createMockBook({ hash: '2', title: 'Book 2', author: '' }),
|
||||
createMockBook({ hash: '3', title: 'Book 3', author: ' ' }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Author);
|
||||
|
||||
const groups = result.filter((item): item is BooksGroup => 'books' in item);
|
||||
const ungrouped = result.filter((item): item is Book => 'format' in item);
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(ungrouped).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupBy: group', () => {
|
||||
it('should return books as-is (group mode handled elsewhere)', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1' }),
|
||||
createMockBook({ hash: '2', title: 'Book 2' }),
|
||||
];
|
||||
|
||||
const result = createBookGroups(books, LibraryGroupByType.Group);
|
||||
|
||||
// Group mode just returns filtered books - actual grouping is in generateBookshelfItems
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWithinGroupSorter', () => {
|
||||
describe('series grouping', () => {
|
||||
it('should sort by seriesIndex ascending', () => {
|
||||
const books = [
|
||||
createMockBook({
|
||||
hash: '1',
|
||||
title: 'Book 3',
|
||||
metadata: { seriesIndex: 3 },
|
||||
}),
|
||||
createMockBook({
|
||||
hash: '2',
|
||||
title: 'Book 1',
|
||||
metadata: { seriesIndex: 1 },
|
||||
}),
|
||||
createMockBook({
|
||||
hash: '3',
|
||||
title: 'Book 2',
|
||||
metadata: { seriesIndex: 2 },
|
||||
}),
|
||||
];
|
||||
|
||||
const sorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Series,
|
||||
LibrarySortByType.Title,
|
||||
'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 place books without seriesIndex after those with index', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book A', metadata: {} }),
|
||||
createMockBook({ hash: '2', title: 'Book B', metadata: { seriesIndex: 1 } }),
|
||||
];
|
||||
|
||||
const sorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Series,
|
||||
LibrarySortByType.Title,
|
||||
'en',
|
||||
);
|
||||
const sorted = [...books].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.hash).toBe('2'); // Has index
|
||||
expect(sorted[1]!.hash).toBe('1'); // No index
|
||||
});
|
||||
|
||||
it('should sort books without seriesIndex by global sort criteria', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Zebra', metadata: {} }),
|
||||
createMockBook({ hash: '2', title: 'Apple', metadata: {} }),
|
||||
];
|
||||
|
||||
const sorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Series,
|
||||
LibrarySortByType.Title,
|
||||
'en',
|
||||
);
|
||||
const sorted = [...books].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.title).toBe('Apple');
|
||||
expect(sorted[1]!.title).toBe('Zebra');
|
||||
});
|
||||
|
||||
it('should handle decimal seriesIndex values', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 2', metadata: { seriesIndex: 2 } }),
|
||||
createMockBook({ hash: '2', title: 'Book 1.5', metadata: { seriesIndex: 1.5 } }),
|
||||
createMockBook({ hash: '3', title: 'Book 1', metadata: { seriesIndex: 1 } }),
|
||||
];
|
||||
|
||||
const sorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Series,
|
||||
LibrarySortByType.Title,
|
||||
'en',
|
||||
);
|
||||
const sorted = [...books].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.metadata?.seriesIndex).toBe(1);
|
||||
expect(sorted[1]!.metadata?.seriesIndex).toBe(1.5);
|
||||
expect(sorted[2]!.metadata?.seriesIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('author grouping', () => {
|
||||
it('should sort by global sort criteria (title)', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Zebra' }),
|
||||
createMockBook({ hash: '2', title: 'Apple' }),
|
||||
];
|
||||
|
||||
const sorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Author,
|
||||
LibrarySortByType.Title,
|
||||
'en',
|
||||
);
|
||||
const sorted = [...books].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.title).toBe('Apple');
|
||||
expect(sorted[1]!.title).toBe('Zebra');
|
||||
});
|
||||
|
||||
it('should sort by global sort criteria (updated)', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Book 1', updatedAt: 2000 }),
|
||||
createMockBook({ hash: '2', title: 'Book 2', updatedAt: 1000 }),
|
||||
];
|
||||
|
||||
const sorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Author,
|
||||
LibrarySortByType.Updated,
|
||||
'en',
|
||||
);
|
||||
const sorted = [...books].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.updatedAt).toBe(1000);
|
||||
expect(sorted[1]!.updatedAt).toBe(2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupSortValue', () => {
|
||||
const createMockGroup = (overrides: Partial<BooksGroup> = {}): BooksGroup => ({
|
||||
id: 'test-group',
|
||||
name: 'Test Group',
|
||||
displayName: 'Test Group',
|
||||
books: [],
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('should return group name for title sort', () => {
|
||||
const group = createMockGroup({ name: 'My Series' });
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Title)).toBe('My Series');
|
||||
});
|
||||
|
||||
it('should return group name for author sort', () => {
|
||||
const group = createMockGroup({ name: 'John Smith' });
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Author)).toBe('John Smith');
|
||||
});
|
||||
|
||||
it('should return group name for format sort', () => {
|
||||
const group = createMockGroup({ name: 'Test Group' });
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Format)).toBe('Test Group');
|
||||
});
|
||||
|
||||
it('should return max updatedAt for date read sort', () => {
|
||||
const group = createMockGroup({
|
||||
books: [
|
||||
createMockBook({ updatedAt: 1000 }),
|
||||
createMockBook({ updatedAt: 3000 }),
|
||||
createMockBook({ updatedAt: 2000 }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Updated)).toBe(3000);
|
||||
});
|
||||
|
||||
it('should return max createdAt for date added sort', () => {
|
||||
const group = createMockGroup({
|
||||
books: [
|
||||
createMockBook({ createdAt: 1000 }),
|
||||
createMockBook({ createdAt: 3000 }),
|
||||
createMockBook({ createdAt: 2000 }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Created)).toBe(3000);
|
||||
});
|
||||
|
||||
it('should return max published date for published sort', () => {
|
||||
const group = createMockGroup({
|
||||
books: [
|
||||
createMockBook({ metadata: { published: '2020-01-01' } }),
|
||||
createMockBook({ metadata: { published: '2023-06-15' } }),
|
||||
createMockBook({ metadata: { published: '2021-12-31' } }),
|
||||
],
|
||||
});
|
||||
|
||||
const result = getGroupSortValue(group, LibrarySortByType.Published);
|
||||
expect(result).toBe(new Date('2023-06-15').getTime());
|
||||
});
|
||||
|
||||
it('should handle missing published dates', () => {
|
||||
const group = createMockGroup({
|
||||
books: [createMockBook({ metadata: {} }), createMockBook({})],
|
||||
});
|
||||
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Published)).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty groups gracefully', () => {
|
||||
const group = createMockGroup({ books: [] });
|
||||
|
||||
// Text-based sorts return group name
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Title)).toBe('Test Group');
|
||||
// Numeric sorts return 0 for empty groups
|
||||
expect(getGroupSortValue(group, LibrarySortByType.Updated)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGroupSorter', () => {
|
||||
const createMockGroup = (overrides: Partial<BooksGroup> = {}): BooksGroup => ({
|
||||
id: 'test-group',
|
||||
name: 'Test Group',
|
||||
displayName: 'Test Group',
|
||||
books: [],
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('should sort groups alphabetically by name for title sort', () => {
|
||||
const groups = [
|
||||
createMockGroup({ name: 'Zebra Series' }),
|
||||
createMockGroup({ name: 'Apple Series' }),
|
||||
createMockGroup({ name: 'Mango Series' }),
|
||||
];
|
||||
|
||||
const sorter = createGroupSorter(LibrarySortByType.Title, 'en');
|
||||
const sorted = [...groups].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.name).toBe('Apple Series');
|
||||
expect(sorted[1]!.name).toBe('Mango Series');
|
||||
expect(sorted[2]!.name).toBe('Zebra Series');
|
||||
});
|
||||
|
||||
it('should sort groups by most recent updatedAt for date read sort', () => {
|
||||
const groups = [
|
||||
createMockGroup({
|
||||
name: 'Group A',
|
||||
books: [createMockBook({ updatedAt: 1000 })],
|
||||
}),
|
||||
createMockGroup({
|
||||
name: 'Group B',
|
||||
books: [createMockBook({ updatedAt: 3000 })],
|
||||
}),
|
||||
createMockGroup({
|
||||
name: 'Group C',
|
||||
books: [createMockBook({ updatedAt: 2000 })],
|
||||
}),
|
||||
];
|
||||
|
||||
const sorter = createGroupSorter(LibrarySortByType.Updated, 'en');
|
||||
const sorted = [...groups].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.name).toBe('Group A');
|
||||
expect(sorted[1]!.name).toBe('Group C');
|
||||
expect(sorted[2]!.name).toBe('Group B');
|
||||
});
|
||||
|
||||
it('should sort groups by most recent createdAt for date added sort', () => {
|
||||
const groups = [
|
||||
createMockGroup({
|
||||
name: 'Group A',
|
||||
books: [createMockBook({ createdAt: 3000 })],
|
||||
}),
|
||||
createMockGroup({
|
||||
name: 'Group B',
|
||||
books: [createMockBook({ createdAt: 1000 })],
|
||||
}),
|
||||
];
|
||||
|
||||
const sorter = createGroupSorter(LibrarySortByType.Created, 'en');
|
||||
const sorted = [...groups].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.name).toBe('Group B');
|
||||
expect(sorted[1]!.name).toBe('Group A');
|
||||
});
|
||||
|
||||
it('should handle groups with single book', () => {
|
||||
const groups = [
|
||||
createMockGroup({
|
||||
name: 'Group A',
|
||||
books: [createMockBook({ updatedAt: 1000 })],
|
||||
}),
|
||||
createMockGroup({
|
||||
name: 'Group B',
|
||||
books: [createMockBook({ updatedAt: 2000 })],
|
||||
}),
|
||||
];
|
||||
|
||||
const sorter = createGroupSorter(LibrarySortByType.Updated, 'en');
|
||||
const sorted = [...groups].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.name).toBe('Group A');
|
||||
expect(sorted[1]!.name).toBe('Group B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBookSorter', () => {
|
||||
it('should sort by title alphabetically', () => {
|
||||
const books = [
|
||||
createMockBook({ title: 'Zebra' }),
|
||||
createMockBook({ title: 'Apple' }),
|
||||
createMockBook({ title: 'Mango' }),
|
||||
];
|
||||
|
||||
const sorter = createBookSorter(LibrarySortByType.Title, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.title).toBe('Apple');
|
||||
expect(sorted[1]!.title).toBe('Mango');
|
||||
expect(sorted[2]!.title).toBe('Zebra');
|
||||
});
|
||||
|
||||
it('should sort by updatedAt for date read', () => {
|
||||
const books = [
|
||||
createMockBook({ title: 'Book A', updatedAt: 2000 }),
|
||||
createMockBook({ title: 'Book B', updatedAt: 1000 }),
|
||||
createMockBook({ title: 'Book C', updatedAt: 3000 }),
|
||||
];
|
||||
|
||||
const sorter = createBookSorter(LibrarySortByType.Updated, 'en');
|
||||
const sorted = [...books].sort(sorter);
|
||||
|
||||
expect(sorted[0]!.title).toBe('Book B');
|
||||
expect(sorted[1]!.title).toBe('Book A');
|
||||
expect(sorted[2]!.title).toBe('Book C');
|
||||
});
|
||||
});
|
||||
|
||||
describe('grouping and sorting integration', () => {
|
||||
it('should correctly group by series and sort groups by date read', () => {
|
||||
const books = [
|
||||
createMockBook({
|
||||
hash: '1',
|
||||
title: 'Old Series Book',
|
||||
metadata: { series: 'Old Series', seriesIndex: 1 },
|
||||
updatedAt: 1000,
|
||||
}),
|
||||
createMockBook({
|
||||
hash: '2',
|
||||
title: 'New Series Book 1',
|
||||
metadata: { series: 'New Series', seriesIndex: 1 },
|
||||
updatedAt: 3000,
|
||||
}),
|
||||
createMockBook({
|
||||
hash: '3',
|
||||
title: 'New Series Book 2',
|
||||
metadata: { series: 'New Series', seriesIndex: 2 },
|
||||
updatedAt: 2000,
|
||||
}),
|
||||
];
|
||||
|
||||
// Create groups
|
||||
const items = createBookGroups(books, LibraryGroupByType.Series);
|
||||
const groups = items.filter((item): item is BooksGroup => 'books' in item);
|
||||
|
||||
// Sort groups by updated (descending - most recent first)
|
||||
const groupSorter = createGroupSorter(LibrarySortByType.Updated, 'en');
|
||||
groups.sort((a, b) => groupSorter(a, b) * -1); // Descending
|
||||
|
||||
expect(groups[0]!.name).toBe('New Series'); // Most recent book at 3000
|
||||
expect(groups[1]!.name).toBe('Old Series'); // Most recent book at 1000
|
||||
|
||||
// Sort within groups by seriesIndex
|
||||
const withinSorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Series,
|
||||
LibrarySortByType.Updated,
|
||||
'en',
|
||||
);
|
||||
groups.forEach((group) => group.books.sort(withinSorter));
|
||||
|
||||
expect(groups[0]!.books[0]!.metadata?.seriesIndex).toBe(1);
|
||||
expect(groups[0]!.books[1]!.metadata?.seriesIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should correctly group by author and sort within groups by title', () => {
|
||||
const books = [
|
||||
createMockBook({ hash: '1', title: 'Zebra', author: 'Author A' }),
|
||||
createMockBook({ hash: '2', title: 'Apple', author: 'Author A' }),
|
||||
createMockBook({ hash: '3', title: 'Mango', author: 'Author B' }),
|
||||
];
|
||||
|
||||
// Create groups
|
||||
const items = createBookGroups(books, LibraryGroupByType.Author);
|
||||
const groups = items.filter((item): item is BooksGroup => 'books' in item);
|
||||
|
||||
// Sort groups alphabetically
|
||||
const groupSorter = createGroupSorter(LibrarySortByType.Title, 'en');
|
||||
groups.sort(groupSorter);
|
||||
|
||||
expect(groups[0]!.name).toBe('Author A');
|
||||
expect(groups[1]!.name).toBe('Author B');
|
||||
|
||||
// Sort within groups by title
|
||||
const withinSorter = createWithinGroupSorter(
|
||||
LibraryGroupByType.Author,
|
||||
LibrarySortByType.Title,
|
||||
'en',
|
||||
);
|
||||
groups.forEach((group) => group.books.sort(withinSorter));
|
||||
|
||||
expect(groups[0]!.books[0]!.title).toBe('Apple');
|
||||
expect(groups[0]!.books[1]!.title).toBe('Zebra');
|
||||
});
|
||||
|
||||
it('should handle ascending/descending sort order', () => {
|
||||
const books = [
|
||||
createMockBook({
|
||||
hash: '1',
|
||||
title: 'Series A Book',
|
||||
metadata: { series: 'Series A' },
|
||||
updatedAt: 1000,
|
||||
}),
|
||||
createMockBook({
|
||||
hash: '2',
|
||||
title: 'Series B Book',
|
||||
metadata: { series: 'Series B' },
|
||||
updatedAt: 2000,
|
||||
}),
|
||||
];
|
||||
|
||||
const items = createBookGroups(books, LibraryGroupByType.Series);
|
||||
const groups = items.filter((item): item is BooksGroup => 'books' in item);
|
||||
const groupSorter = createGroupSorter(LibrarySortByType.Updated, 'en');
|
||||
|
||||
// Ascending (oldest first)
|
||||
const ascending = [...groups].sort((a, b) => groupSorter(a, b) * 1);
|
||||
expect(ascending[0]!.name).toBe('Series A');
|
||||
|
||||
// Descending (newest first)
|
||||
const descending = [...groups].sort((a, b) => groupSorter(a, b) * -1);
|
||||
expect(descending[0]!.name).toBe('Series B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureLibrarySortByType', () => {
|
||||
it('should return valid sort type when value is valid', () => {
|
||||
expect(ensureLibrarySortByType('title', LibrarySortByType.Updated)).toBe(
|
||||
LibrarySortByType.Title,
|
||||
);
|
||||
expect(ensureLibrarySortByType('author', LibrarySortByType.Updated)).toBe(
|
||||
LibrarySortByType.Author,
|
||||
);
|
||||
expect(ensureLibrarySortByType('updated', LibrarySortByType.Title)).toBe(
|
||||
LibrarySortByType.Updated,
|
||||
);
|
||||
expect(ensureLibrarySortByType('created', LibrarySortByType.Updated)).toBe(
|
||||
LibrarySortByType.Created,
|
||||
);
|
||||
expect(ensureLibrarySortByType('format', LibrarySortByType.Updated)).toBe(
|
||||
LibrarySortByType.Format,
|
||||
);
|
||||
expect(ensureLibrarySortByType('published', LibrarySortByType.Updated)).toBe(
|
||||
LibrarySortByType.Published,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fallback when value is null', () => {
|
||||
expect(ensureLibrarySortByType(null, LibrarySortByType.Updated)).toBe(
|
||||
LibrarySortByType.Updated,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fallback when value is undefined', () => {
|
||||
expect(ensureLibrarySortByType(undefined, LibrarySortByType.Title)).toBe(
|
||||
LibrarySortByType.Title,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fallback when value is invalid', () => {
|
||||
expect(ensureLibrarySortByType('invalid', LibrarySortByType.Updated)).toBe(
|
||||
LibrarySortByType.Updated,
|
||||
);
|
||||
expect(ensureLibrarySortByType('random', LibrarySortByType.Title)).toBe(
|
||||
LibrarySortByType.Title,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fallback when value is empty string', () => {
|
||||
expect(ensureLibrarySortByType('', LibrarySortByType.Updated)).toBe(LibrarySortByType.Updated);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureLibraryGroupByType', () => {
|
||||
it('should return valid group type when value is valid', () => {
|
||||
expect(ensureLibraryGroupByType('none', LibraryGroupByType.Group)).toBe(
|
||||
LibraryGroupByType.None,
|
||||
);
|
||||
expect(ensureLibraryGroupByType('group', LibraryGroupByType.None)).toBe(
|
||||
LibraryGroupByType.Group,
|
||||
);
|
||||
expect(ensureLibraryGroupByType('series', LibraryGroupByType.Group)).toBe(
|
||||
LibraryGroupByType.Series,
|
||||
);
|
||||
expect(ensureLibraryGroupByType('author', LibraryGroupByType.Group)).toBe(
|
||||
LibraryGroupByType.Author,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fallback when value is null', () => {
|
||||
expect(ensureLibraryGroupByType(null, LibraryGroupByType.Group)).toBe(LibraryGroupByType.Group);
|
||||
});
|
||||
|
||||
it('should return fallback when value is undefined', () => {
|
||||
expect(ensureLibraryGroupByType(undefined, LibraryGroupByType.Series)).toBe(
|
||||
LibraryGroupByType.Series,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fallback when value is invalid', () => {
|
||||
expect(ensureLibraryGroupByType('invalid', LibraryGroupByType.Group)).toBe(
|
||||
LibraryGroupByType.Group,
|
||||
);
|
||||
expect(ensureLibraryGroupByType('random', LibraryGroupByType.Author)).toBe(
|
||||
LibraryGroupByType.Author,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return fallback when value is empty string', () => {
|
||||
expect(ensureLibraryGroupByType('', LibraryGroupByType.Group)).toBe(LibraryGroupByType.Group);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findGroupById', () => {
|
||||
const createMockGroup = (overrides: Partial<BooksGroup> = {}): BooksGroup => ({
|
||||
id: 'test-group',
|
||||
name: 'Test Group',
|
||||
displayName: 'Test Group',
|
||||
books: [],
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('should find group by id', () => {
|
||||
const items: (Book | BooksGroup)[] = [
|
||||
createMockBook({ hash: '1' }),
|
||||
createMockGroup({ id: 'group-1', name: 'Group 1' }),
|
||||
createMockGroup({ id: 'group-2', name: 'Group 2' }),
|
||||
];
|
||||
|
||||
const found = findGroupById(items, 'group-2');
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.name).toBe('Group 2');
|
||||
});
|
||||
|
||||
it('should return undefined when group not found', () => {
|
||||
const items: (Book | BooksGroup)[] = [
|
||||
createMockBook({ hash: '1' }),
|
||||
createMockGroup({ id: 'group-1', name: 'Group 1' }),
|
||||
];
|
||||
|
||||
const found = findGroupById(items, 'non-existent');
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not match books', () => {
|
||||
const items: (Book | BooksGroup)[] = [
|
||||
createMockBook({ hash: 'group-1' }), // Book with hash matching a group id
|
||||
createMockGroup({ id: 'group-2', name: 'Group 2' }),
|
||||
];
|
||||
|
||||
const found = findGroupById(items, 'group-1');
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupDisplayName', () => {
|
||||
const createMockGroup = (overrides: Partial<BooksGroup> = {}): BooksGroup => ({
|
||||
id: 'test-group',
|
||||
name: 'Test Group',
|
||||
displayName: 'Test Display Name',
|
||||
books: [],
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('should return displayName when available', () => {
|
||||
const items: (Book | BooksGroup)[] = [
|
||||
createMockGroup({ id: 'group-1', name: 'Name', displayName: 'Display Name' }),
|
||||
];
|
||||
|
||||
expect(getGroupDisplayName(items, 'group-1')).toBe('Display Name');
|
||||
});
|
||||
|
||||
it('should return name when displayName is empty', () => {
|
||||
const items: (Book | BooksGroup)[] = [
|
||||
createMockGroup({ id: 'group-1', name: 'Name', displayName: '' }),
|
||||
];
|
||||
|
||||
expect(getGroupDisplayName(items, 'group-1')).toBe('Name');
|
||||
});
|
||||
|
||||
it('should return undefined when group not found', () => {
|
||||
const items: (Book | BooksGroup)[] = [createMockGroup({ id: 'group-1', name: 'Name' })];
|
||||
|
||||
expect(getGroupDisplayName(items, 'non-existent')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -193,7 +193,7 @@ describe('makeSafeFilename', () => {
|
||||
});
|
||||
|
||||
it('should handle string with only unsafe characters', () => {
|
||||
expect(makeSafeFilename('<>:"|?*')).toBe('_______');
|
||||
expect(makeSafeFilename('<>:"|?*#')).toBe('________');
|
||||
});
|
||||
|
||||
it('should handle string that becomes empty after sanitization and trimming', () => {
|
||||
|
||||
@@ -0,0 +1,572 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderNoteTemplate, validateNoteTemplate, NoteTemplateData } from '../../utils/note';
|
||||
|
||||
describe('renderNoteTemplate', () => {
|
||||
const sampleData: NoteTemplateData = {
|
||||
title: 'The Great Gatsby',
|
||||
author: 'F. Scott Fitzgerald',
|
||||
exportDate: '2024-01-15',
|
||||
chapters: [
|
||||
{
|
||||
title: 'Chapter 1',
|
||||
annotations: [
|
||||
{
|
||||
text: 'In my younger and more vulnerable years',
|
||||
note: 'Opening line',
|
||||
style: 'highlight',
|
||||
color: 'yellow',
|
||||
timestamp: 1705312800000, // 2024-01-15 10:00:00 UTC
|
||||
},
|
||||
{
|
||||
text: 'So we beat on, boats against the current',
|
||||
note: '',
|
||||
style: 'underline',
|
||||
color: 'blue',
|
||||
timestamp: 1705316400000, // 2024-01-15 11:00:00 UTC
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Chapter 2',
|
||||
annotations: [
|
||||
{
|
||||
text: 'The eyes of Doctor T. J. Eckleburg',
|
||||
note: 'Symbolism',
|
||||
style: 'highlight',
|
||||
color: 'green',
|
||||
timestamp: 1705320000000, // 2024-01-15 12:00:00 UTC
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Variable substitution', () => {
|
||||
it('should substitute simple variables', () => {
|
||||
const template = 'Book: {{ title }} by {{ author }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Book: The Great Gatsby by F. Scott Fitzgerald');
|
||||
});
|
||||
|
||||
it('should handle undefined variables gracefully', () => {
|
||||
const template = '{{ nonexistent }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle nested property access', () => {
|
||||
const template = '{{ chapters[0].title }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Chapter 1');
|
||||
});
|
||||
|
||||
it('should handle deeply nested property access', () => {
|
||||
const template = '{{ chapters[0].annotations[0].text }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('In my younger and more vulnerable years');
|
||||
});
|
||||
});
|
||||
|
||||
describe('For loops', () => {
|
||||
it('should iterate over arrays', () => {
|
||||
const template = '{% for chapter in chapters %}{{ chapter.title }}\n{% endfor %}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toContain('Chapter 1');
|
||||
expect(result).toContain('Chapter 2');
|
||||
});
|
||||
|
||||
it('should handle nested for loops', () => {
|
||||
const template = `{% for chapter in chapters %}
|
||||
## {{ chapter.title }}
|
||||
{% for annotation in chapter.annotations %}
|
||||
- {{ annotation.text }}
|
||||
{% endfor %}
|
||||
{% endfor %}`;
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toContain('## Chapter 1');
|
||||
expect(result).toContain('- In my younger and more vulnerable years');
|
||||
expect(result).toContain('## Chapter 2');
|
||||
expect(result).toContain('- The eyes of Doctor T. J. Eckleburg');
|
||||
});
|
||||
|
||||
it('should handle empty arrays', () => {
|
||||
const emptyData: NoteTemplateData = {
|
||||
title: 'Empty Book',
|
||||
author: 'Nobody',
|
||||
exportDate: '2024-01-15',
|
||||
chapters: [],
|
||||
};
|
||||
const template = '{% for chapter in chapters %}{{ chapter.title }}{% endfor %}';
|
||||
const result = renderNoteTemplate(template, emptyData);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should provide loop variables (loop.index, loop.first, loop.last)', () => {
|
||||
const template = `{% for chapter in chapters %}{{ loop.index }}: {{ chapter.title }}{% if not loop.last %}, {% endif %}{% endfor %}`;
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('1: Chapter 1, 2: Chapter 2');
|
||||
});
|
||||
|
||||
it('should provide loop.index0 (0-based index)', () => {
|
||||
const template = `{% for chapter in chapters %}{{ loop.index0 }}: {{ chapter.title }}\n{% endfor %}`;
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toContain('0: Chapter 1');
|
||||
expect(result).toContain('1: Chapter 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conditionals', () => {
|
||||
it('should handle if statements', () => {
|
||||
const template = '{% if author %}Author: {{ author }}{% endif %}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Author: F. Scott Fitzgerald');
|
||||
});
|
||||
|
||||
it('should handle if-else statements', () => {
|
||||
const template = '{% if nonexistent %}Has value{% else %}No value{% endif %}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('No value');
|
||||
});
|
||||
|
||||
it('should handle if-elif-else statements', () => {
|
||||
const template =
|
||||
'{% if chapters.length > 5 %}Many{% elif chapters.length > 1 %}Some{% else %}Few{% endif %}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Some');
|
||||
});
|
||||
|
||||
it('should handle if statements with annotation note', () => {
|
||||
const template = `{% for chapter in chapters %}{% for annotation in chapter.annotations %}{% if annotation.note %}Note: {{ annotation.note }}
|
||||
{% endif %}{% endfor %}{% endfor %}`;
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toContain('Note: Opening line');
|
||||
expect(result).toContain('Note: Symbolism');
|
||||
expect(result.match(/Note:/g)?.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Date filter', () => {
|
||||
it('should format timestamp with default locale format', () => {
|
||||
const template = '{{ chapters[0].annotations[0].timestamp | date }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
// Result will be locale-dependent, just check it's not empty
|
||||
expect(result).not.toBe('');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should format timestamp with custom format %Y-%m-%d', () => {
|
||||
const template = "{{ chapters[0].annotations[0].timestamp | date('%Y-%m-%d') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('2024-01-15');
|
||||
});
|
||||
|
||||
it('should format timestamp with time format %H:%M:%S', () => {
|
||||
const template = "{{ chapters[0].annotations[0].timestamp | date('%H:%M:%S') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
// This depends on timezone, so just check the format
|
||||
expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/);
|
||||
});
|
||||
|
||||
it('should format timestamp with full datetime', () => {
|
||||
const template = "{{ chapters[0].annotations[0].timestamp | date('%Y-%m-%d %H:%M') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toMatch(/^2024-01-15 \d{2}:\d{2}$/);
|
||||
});
|
||||
|
||||
it('should handle 12-hour format with %I and %p', () => {
|
||||
const template = "{{ chapters[0].annotations[0].timestamp | date('%I:%M %p') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toMatch(/^\d{2}:\d{2} (AM|PM)$/);
|
||||
});
|
||||
|
||||
it('should handle undefined timestamp', () => {
|
||||
const dataWithoutTimestamp: NoteTemplateData = {
|
||||
...sampleData,
|
||||
chapters: [
|
||||
{
|
||||
title: 'Chapter',
|
||||
annotations: [{ text: 'Text' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const template = "{{ chapters[0].annotations[0].timestamp | date('%Y-%m-%d') }}";
|
||||
const result = renderNoteTemplate(template, dataWithoutTimestamp);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle string date input', () => {
|
||||
const template = "{{ exportDate | date('%Y-%m-%d') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('2024-01-15');
|
||||
});
|
||||
|
||||
it('should handle escaped percent signs', () => {
|
||||
const template =
|
||||
"{{ chapters[0].annotations[0].timestamp | date('100%% complete on %Y-%m-%d') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toContain('100% complete on 2024-01-15');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default filter', () => {
|
||||
it('should return value when present', () => {
|
||||
const template = "{{ title | default('Unknown') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('The Great Gatsby');
|
||||
});
|
||||
|
||||
it('should return default when value is undefined', () => {
|
||||
const template = "{{ nonexistent | default('Unknown') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Unknown');
|
||||
});
|
||||
|
||||
it('should return default when value is empty string', () => {
|
||||
const template = "{{ chapters[0].annotations[1].note | default('No note') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('No note');
|
||||
});
|
||||
});
|
||||
|
||||
describe('String filters', () => {
|
||||
it('should convert to uppercase with upper filter', () => {
|
||||
const template = '{{ title | upper }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('THE GREAT GATSBY');
|
||||
});
|
||||
|
||||
it('should convert to lowercase with lower filter', () => {
|
||||
const template = '{{ title | lower }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('the great gatsby');
|
||||
});
|
||||
|
||||
it('should capitalize with capitalize filter', () => {
|
||||
const template = '{{ "hello world" | capitalize }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('should title case with title filter', () => {
|
||||
const template = '{{ "hello world" | title }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should trim whitespace with trim filter', () => {
|
||||
const template = '{{ " spaced " | trim }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('spaced');
|
||||
});
|
||||
|
||||
it('should replace substrings with replace filter', () => {
|
||||
const template = "{{ title | replace('Great', 'Amazing') }}";
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('The Amazing Gatsby');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Truncate filter', () => {
|
||||
it('should truncate long strings', () => {
|
||||
const template = '{{ chapters[0].annotations[0].text | truncate(20) }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result.length).toBeLessThanOrEqual(23); // 20 + '...'
|
||||
expect(result).toContain('...');
|
||||
});
|
||||
|
||||
it('should not truncate short strings', () => {
|
||||
const template = '{{ title | truncate(50) }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('The Great Gatsby');
|
||||
});
|
||||
|
||||
it('should truncate at word boundaries by default', () => {
|
||||
const template = '{{ chapters[0].annotations[0].text | truncate(25) }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).not.toMatch(/\s\.\.\.$/); // Should not end with space before ...
|
||||
});
|
||||
});
|
||||
|
||||
describe('Length filter', () => {
|
||||
it('should return string length', () => {
|
||||
const template = '{{ title | length }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('16');
|
||||
});
|
||||
|
||||
it('should return array length', () => {
|
||||
const template = '{{ chapters | length }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('First and last filters', () => {
|
||||
it('should get first element of array', () => {
|
||||
const template = '{% set first_chapter = chapters | first %}{{ first_chapter.title }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Chapter 1');
|
||||
});
|
||||
|
||||
it('should get last element of array', () => {
|
||||
const template = '{% set last_chapter = chapters | last %}{{ last_chapter.title }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Chapter 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Join filter', () => {
|
||||
it('should join array elements', () => {
|
||||
// Using a loop with conditional comma separator
|
||||
const template =
|
||||
'{% for chapter in chapters %}{{ chapter.title }}{% if not loop.last %}, {% endif %}{% endfor %}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('Chapter 1, Chapter 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Newline to BR filter', () => {
|
||||
it('should convert newlines to br tags', () => {
|
||||
const dataWithNewlines: NoteTemplateData = {
|
||||
...sampleData,
|
||||
chapters: [
|
||||
{
|
||||
title: 'Chapter 1',
|
||||
annotations: [
|
||||
{
|
||||
text: 'Line 1\nLine 2\nLine 3',
|
||||
note: 'Multiple lines',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const template = '{{ chapters[0].annotations[0].text | nl2br }}';
|
||||
const result = renderNoteTemplate(template, dataWithNewlines);
|
||||
expect(result).toBe('Line 1<br>\nLine 2<br>\nLine 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Chained filters', () => {
|
||||
it('should support chaining multiple filters', () => {
|
||||
const template = '{{ title | upper | truncate(10) }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
// truncate(10) with word boundaries results in "THE GREAT..." (breaks at word boundary)
|
||||
expect(result).toBe('THE GREAT...');
|
||||
});
|
||||
|
||||
it('should support chaining with killwords option', () => {
|
||||
const template = '{{ title | upper | truncate(5, true) }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
// truncate(5, true) with killwords results in "THE G..."
|
||||
expect(result).toBe('THE G...');
|
||||
});
|
||||
|
||||
it('should chain date and string filters', () => {
|
||||
const template = '{{ exportDate | upper }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('2024-01-15');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complete template rendering', () => {
|
||||
it('should render a complete export template', () => {
|
||||
const template = `## {{ title }}
|
||||
**Author**: {{ author }}
|
||||
|
||||
**Exported**: {{ exportDate }}
|
||||
|
||||
---
|
||||
|
||||
### Highlights & Annotations
|
||||
|
||||
{% for chapter in chapters %}
|
||||
#### {{ chapter.title }}
|
||||
{% for annotation in chapter.annotations %}
|
||||
> {{ annotation.text }}
|
||||
{% if annotation.note %}
|
||||
**Note:** {{ annotation.note }}
|
||||
{% endif %}
|
||||
*Time: {{ annotation.timestamp | date('%Y-%m-%d %H:%M') }}*
|
||||
{% endfor %}
|
||||
|
||||
---
|
||||
{% endfor %}`;
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
|
||||
expect(result).toContain('## The Great Gatsby');
|
||||
expect(result).toContain('**Author**: F. Scott Fitzgerald');
|
||||
expect(result).toContain('#### Chapter 1');
|
||||
expect(result).toContain('> In my younger and more vulnerable years');
|
||||
expect(result).toContain('**Note:** Opening line');
|
||||
expect(result).toContain('#### Chapter 2');
|
||||
expect(result).toContain('> The eyes of Doctor T. J. Eckleburg');
|
||||
});
|
||||
|
||||
it('should handle template with no chapters', () => {
|
||||
const emptyData: NoteTemplateData = {
|
||||
title: 'Empty Book',
|
||||
author: 'Unknown',
|
||||
exportDate: '2024-01-15',
|
||||
chapters: [],
|
||||
};
|
||||
const template = `## {{ title }}
|
||||
{% for chapter in chapters %}
|
||||
{{ chapter.title }}
|
||||
{% endfor %}`;
|
||||
const result = renderNoteTemplate(template, emptyData);
|
||||
expect(result).toContain('## Empty Book');
|
||||
expect(result).not.toContain('Chapter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle invalid template syntax gracefully', () => {
|
||||
const template = '{{ unclosed';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toContain('[Template Error:');
|
||||
});
|
||||
|
||||
it('should handle invalid filter', () => {
|
||||
// Nunjucks throws on unknown filters
|
||||
const template = '{{ title | nonexistentfilter }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toContain('[Template Error:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Special characters and escaping', () => {
|
||||
it('should not auto-escape HTML characters', () => {
|
||||
const dataWithHtml: NoteTemplateData = {
|
||||
...sampleData,
|
||||
title: '<b>Bold</b> Title',
|
||||
};
|
||||
const template = '{{ title }}';
|
||||
const result = renderNoteTemplate(template, dataWithHtml);
|
||||
expect(result).toBe('<b>Bold</b> Title');
|
||||
});
|
||||
|
||||
it('should handle markdown special characters', () => {
|
||||
const dataWithMarkdown: NoteTemplateData = {
|
||||
...sampleData,
|
||||
chapters: [
|
||||
{
|
||||
title: '# Heading with **bold** and _italic_',
|
||||
annotations: [{ text: '> Quoted text' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const template = '{{ chapters[0].title }}\n{{ chapters[0].annotations[0].text }}';
|
||||
const result = renderNoteTemplate(template, dataWithMarkdown);
|
||||
expect(result).toContain('# Heading with **bold** and _italic_');
|
||||
expect(result).toContain('> Quoted text');
|
||||
});
|
||||
|
||||
it('should handle unicode characters', () => {
|
||||
const dataWithUnicode: NoteTemplateData = {
|
||||
...sampleData,
|
||||
title: '红楼梦',
|
||||
author: '曹雪芹',
|
||||
chapters: [
|
||||
{
|
||||
title: '第一回',
|
||||
annotations: [{ text: '满纸荒唐言,一把辛酸泪。' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const template = '{{ title }} - {{ author }}: {{ chapters[0].annotations[0].text }}';
|
||||
const result = renderNoteTemplate(template, dataWithUnicode);
|
||||
expect(result).toBe('红楼梦 - 曹雪芹: 满纸荒唐言,一把辛酸泪。');
|
||||
});
|
||||
|
||||
it('should handle emoji', () => {
|
||||
const dataWithEmoji: NoteTemplateData = {
|
||||
...sampleData,
|
||||
title: '📚 My Book',
|
||||
chapters: [
|
||||
{
|
||||
title: 'Chapter 😀',
|
||||
annotations: [{ text: 'Text with emoji 🎉' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const template = '{{ title }} - {{ chapters[0].annotations[0].text }}';
|
||||
const result = renderNoteTemplate(template, dataWithEmoji);
|
||||
expect(result).toBe('📚 My Book - Text with emoji 🎉');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Whitespace handling', () => {
|
||||
it('should trim blocks correctly', () => {
|
||||
const template = `Start
|
||||
{% if true %}
|
||||
Content
|
||||
{% endif %}
|
||||
End`;
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
// With trimBlocks and lstripBlocks enabled, whitespace should be managed
|
||||
expect(result).toContain('Start');
|
||||
expect(result).toContain('Content');
|
||||
expect(result).toContain('End');
|
||||
});
|
||||
|
||||
it('should preserve intentional whitespace in content', () => {
|
||||
const template = '{{ title }} {{ author }}';
|
||||
const result = renderNoteTemplate(template, sampleData);
|
||||
expect(result).toBe('The Great Gatsby F. Scott Fitzgerald');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateNoteTemplate', () => {
|
||||
it('should return valid for correct template', () => {
|
||||
const template = '{{ title }} by {{ author }}';
|
||||
const result = validateNoteTemplate(template);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return valid for complex template', () => {
|
||||
const template = `{% for chapter in chapters %}
|
||||
{{ chapter.title }}
|
||||
{% for annotation in chapter.annotations %}
|
||||
{{ annotation.text }}
|
||||
{% endfor %}
|
||||
{% endfor %}`;
|
||||
const result = validateNoteTemplate(template);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should return invalid for unclosed variable', () => {
|
||||
const template = '{{ unclosed';
|
||||
const result = validateNoteTemplate(template);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return invalid for unclosed block', () => {
|
||||
const template = '{% if true %}content';
|
||||
const result = validateNoteTemplate(template);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return invalid for unclosed for loop', () => {
|
||||
const template = '{% for item in items %}content';
|
||||
const result = validateNoteTemplate(template);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return valid for empty template', () => {
|
||||
const template = '';
|
||||
const result = validateNoteTemplate(template);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('should return valid for plain text without template syntax', () => {
|
||||
const template = 'Just plain text without any template syntax';
|
||||
const result = validateNoteTemplate(template);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ParagraphIterator } from '@/utils/paragraph';
|
||||
|
||||
const createDoc = (body: string): Document =>
|
||||
new DOMParser().parseFromString(`<html><body>${body}</body></html>`, 'text/html');
|
||||
|
||||
describe('ParagraphIterator', () => {
|
||||
it('skips whitespace-only paragraphs', async () => {
|
||||
const doc = createDoc(`
|
||||
<p>First</p>
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
<p>\u200b</p>
|
||||
<p><span>\u00a0</span></p>
|
||||
<p><br /></p>
|
||||
<p>Second</p>
|
||||
`);
|
||||
|
||||
const iterator = await ParagraphIterator.createAsync(doc, 1000);
|
||||
|
||||
expect(iterator.length).toBe(2);
|
||||
expect(iterator.first()?.toString()).toContain('First');
|
||||
expect(iterator.next()?.toString()).toContain('Second');
|
||||
});
|
||||
|
||||
it('keeps paragraphs with non-text content', async () => {
|
||||
const doc = createDoc(`
|
||||
<p>Intro</p>
|
||||
<p><img src="cover.png" alt="" /></p>
|
||||
<p>Outro</p>
|
||||
`);
|
||||
|
||||
const iterator = await ParagraphIterator.createAsync(doc, 1000);
|
||||
|
||||
expect(iterator.length).toBe(3);
|
||||
|
||||
iterator.first();
|
||||
expect(iterator.current()?.toString()).toContain('Intro');
|
||||
|
||||
const imageRange = iterator.next();
|
||||
const fragment = imageRange?.cloneContents();
|
||||
expect(fragment?.querySelector('img')).not.toBeNull();
|
||||
|
||||
expect(iterator.next()?.toString()).toContain('Outro');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { estimateTTSTime } from '@/utils/ttsTime';
|
||||
import { BookProgress } from '@/types/book';
|
||||
|
||||
const createProgress = (values: {
|
||||
sectionMin: number;
|
||||
totalMin: number;
|
||||
pageCurrent: number;
|
||||
pageTotal: number;
|
||||
}) => {
|
||||
return {
|
||||
timeinfo: {
|
||||
section: values.sectionMin,
|
||||
total: values.totalMin,
|
||||
},
|
||||
pageinfo: {
|
||||
current: values.pageCurrent,
|
||||
total: values.pageTotal,
|
||||
},
|
||||
} as BookProgress;
|
||||
};
|
||||
|
||||
describe('estimateTTSTime', () => {
|
||||
it('estimates chapter and book remaining time at 1x', () => {
|
||||
const progress = createProgress({
|
||||
sectionMin: 10,
|
||||
totalMin: 60,
|
||||
pageCurrent: 49,
|
||||
pageTotal: 100,
|
||||
});
|
||||
|
||||
const result = estimateTTSTime(progress, 1, 1_700_000_000_000);
|
||||
|
||||
expect(result.chapterRemainingSec).toBe(600);
|
||||
expect(result.bookRemainingSec).toBe(3600);
|
||||
expect(result.finishAtTimestamp).toBe(1_700_003_600_000);
|
||||
});
|
||||
|
||||
it('scales estimates by playback rate', () => {
|
||||
const progress = createProgress({
|
||||
sectionMin: 12,
|
||||
totalMin: 90,
|
||||
pageCurrent: 44,
|
||||
pageTotal: 90,
|
||||
});
|
||||
|
||||
const result = estimateTTSTime(progress, 1.5, 1000);
|
||||
|
||||
expect(result.chapterRemainingSec).toBe(480);
|
||||
expect(result.bookRemainingSec).toBe(3600);
|
||||
expect(result.finishAtTimestamp).toBe(3_601_000);
|
||||
});
|
||||
|
||||
it('falls back safely for missing progress', () => {
|
||||
const result = estimateTTSTime(null, 2, 1000);
|
||||
|
||||
expect(result.chapterRemainingSec).toBeNull();
|
||||
expect(result.bookRemainingSec).toBeNull();
|
||||
expect(result.finishAtTimestamp).toBeNull();
|
||||
});
|
||||
|
||||
it('uses book remaining to compute finish time', () => {
|
||||
const progress = createProgress({
|
||||
sectionMin: 1,
|
||||
totalMin: 2,
|
||||
pageCurrent: 99,
|
||||
pageTotal: 100,
|
||||
});
|
||||
|
||||
const result = estimateTTSTime(progress, 1, 1000);
|
||||
|
||||
expect(result.bookRemainingSec).toBe(120);
|
||||
expect(result.finishAtTimestamp).toBe(121000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { TxtToEpubConverter } from '@/utils/txt';
|
||||
|
||||
type TestChapter = {
|
||||
title: string;
|
||||
content: string;
|
||||
isVolume: boolean;
|
||||
};
|
||||
|
||||
type TestMetadata = {
|
||||
bookTitle: string;
|
||||
author: string;
|
||||
language: string;
|
||||
identifier: string;
|
||||
};
|
||||
|
||||
type TxtConverterPrivateAPI = {
|
||||
detectEncoding(buffer: ArrayBuffer): string | undefined;
|
||||
createEpub(chapters: TestChapter[], metadata: TestMetadata): Promise<Blob>;
|
||||
};
|
||||
|
||||
type TxtConverterFlowPrivateAPI = TxtConverterPrivateAPI & {
|
||||
convert(options: { file: File; author?: string; language?: string }): Promise<{
|
||||
chapterCount: number;
|
||||
}>;
|
||||
extractChapters(
|
||||
txtContent: string,
|
||||
metadata: TestMetadata,
|
||||
option: { linesBetweenSegments: number; fallbackParagraphsPerChapter: number },
|
||||
): TestChapter[];
|
||||
probeChapterCount(
|
||||
txtContent: string,
|
||||
metadata: TestMetadata,
|
||||
option: { linesBetweenSegments: number; fallbackParagraphsPerChapter: number },
|
||||
): number;
|
||||
iterateSegmentsFromTextChunks(
|
||||
chunks: Iterable<string>,
|
||||
linesBetweenSegments: number,
|
||||
): Generator<string>;
|
||||
detectEncodingFromFile(file: File): Promise<string | undefined>;
|
||||
extractChaptersFromFileBySegments(
|
||||
file: File,
|
||||
encoding: string,
|
||||
metadata: TestMetadata,
|
||||
option: { linesBetweenSegments: number; fallbackParagraphsPerChapter: number },
|
||||
): Promise<TestChapter[]>;
|
||||
probeChapterCountFromFileBySegments(
|
||||
file: File,
|
||||
encoding: string,
|
||||
metadata: TestMetadata,
|
||||
option: { linesBetweenSegments: number; fallbackParagraphsPerChapter: number },
|
||||
): Promise<number>;
|
||||
};
|
||||
|
||||
const getBufferSize = (input?: BufferSource): number => {
|
||||
if (!input) return 0;
|
||||
return input instanceof ArrayBuffer ? input.byteLength : input.byteLength;
|
||||
};
|
||||
|
||||
describe('TxtToEpubConverter', () => {
|
||||
it('convert should choose 8 -> 7 when probe detects multiple chapters', async () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterFlowPrivateAPI;
|
||||
const calls: number[] = [];
|
||||
|
||||
converter.detectEncoding = () => 'utf-8';
|
||||
converter.createEpub = async () => new Blob();
|
||||
converter.extractChapters = (_, __, option) => {
|
||||
calls.push(option.linesBetweenSegments);
|
||||
if (option.linesBetweenSegments === 8) {
|
||||
return [{ title: 'Only', content: 'c', isVolume: false }];
|
||||
}
|
||||
if (option.linesBetweenSegments === 7) {
|
||||
return [
|
||||
{ title: 'A', content: 'a', isVolume: false },
|
||||
{ title: 'B', content: 'b', isVolume: false },
|
||||
];
|
||||
}
|
||||
return [{ title: 'Fallback', content: 'f', isVolume: false }];
|
||||
};
|
||||
converter.probeChapterCount = (_, __, option) => {
|
||||
calls.push(option.linesBetweenSegments);
|
||||
return 2;
|
||||
};
|
||||
|
||||
const file = new File(['dummy content'], 'sample.txt');
|
||||
const result = await converter.convert({ file });
|
||||
|
||||
expect(calls).toEqual([8, 7, 7]);
|
||||
expect(result.chapterCount).toBe(2);
|
||||
});
|
||||
|
||||
it('convert should choose 8 -> 6 when probe does not detect multiple chapters', async () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterFlowPrivateAPI;
|
||||
const calls: number[] = [];
|
||||
|
||||
converter.detectEncoding = () => 'utf-8';
|
||||
converter.createEpub = async () => new Blob();
|
||||
converter.extractChapters = (_, __, option) => {
|
||||
calls.push(option.linesBetweenSegments);
|
||||
if (option.linesBetweenSegments === 8) {
|
||||
return [{ title: 'Only', content: 'c', isVolume: false }];
|
||||
}
|
||||
if (option.linesBetweenSegments === 6) {
|
||||
return [
|
||||
{ title: 'A', content: 'a', isVolume: false },
|
||||
{ title: 'B', content: 'b', isVolume: false },
|
||||
];
|
||||
}
|
||||
return [{ title: 'Single', content: 's', isVolume: false }];
|
||||
};
|
||||
converter.probeChapterCount = (_, __, option) => {
|
||||
calls.push(option.linesBetweenSegments);
|
||||
return 1;
|
||||
};
|
||||
|
||||
const file = new File(['dummy content'], 'sample.txt');
|
||||
const result = await converter.convert({ file });
|
||||
|
||||
expect(calls).toEqual([8, 7, 6]);
|
||||
expect(result.chapterCount).toBe(2);
|
||||
});
|
||||
|
||||
it('detectEncoding should probe UTF-8 with sampled buffers only', () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterPrivateAPI;
|
||||
const fullSize = 220 * 1024;
|
||||
const buffer = new TextEncoder().encode('a'.repeat(fullSize)).buffer;
|
||||
|
||||
const OriginalTextDecoder = globalThis.TextDecoder;
|
||||
const decodeSizes: number[] = [];
|
||||
|
||||
class RecordingTextDecoder extends OriginalTextDecoder {
|
||||
override decode(input?: BufferSource, options?: TextDecodeOptions): string {
|
||||
decodeSizes.push(getBufferSize(input));
|
||||
return super.decode(input, options);
|
||||
}
|
||||
}
|
||||
|
||||
(globalThis as { TextDecoder: typeof TextDecoder }).TextDecoder =
|
||||
RecordingTextDecoder as typeof TextDecoder;
|
||||
try {
|
||||
expect(converter.detectEncoding(buffer)).toBe('utf-8');
|
||||
} finally {
|
||||
(globalThis as { TextDecoder: typeof TextDecoder }).TextDecoder = OriginalTextDecoder;
|
||||
}
|
||||
|
||||
expect(Math.max(...decodeSizes)).toBeLessThanOrEqual(64 * 1024);
|
||||
expect(decodeSizes).toContain(8192);
|
||||
expect(decodeSizes).not.toContain(fullSize);
|
||||
});
|
||||
|
||||
it('createEpub should use metadata language for chapter lang attributes', async () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterPrivateAPI;
|
||||
const chapters: TestChapter[] = [
|
||||
{
|
||||
title: 'Chapter 1',
|
||||
content: '<h2>Chapter 1</h2><p>Hello world</p>',
|
||||
isVolume: false,
|
||||
},
|
||||
];
|
||||
const metadata: TestMetadata = {
|
||||
bookTitle: 'Sample Book',
|
||||
author: 'Sample Author',
|
||||
language: 'zh',
|
||||
identifier: 'sample-id',
|
||||
};
|
||||
|
||||
const blob = await converter.createEpub(chapters, metadata);
|
||||
const { ZipReader, BlobReader, TextWriter } = await import('@zip.js/zip.js');
|
||||
const reader = new ZipReader(new BlobReader(blob));
|
||||
try {
|
||||
const entries = await reader.getEntries();
|
||||
const chapterEntry = entries.find((entry) => entry.filename === 'OEBPS/chapter1.xhtml');
|
||||
expect(chapterEntry).toBeDefined();
|
||||
const chapterContent = await chapterEntry!.getData(new TextWriter());
|
||||
expect(chapterContent).toContain('lang="zh"');
|
||||
expect(chapterContent).toContain('xml:lang="zh"');
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('iterateSegmentsFromTextChunks should split by 8 newlines across chunk boundaries', () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterFlowPrivateAPI;
|
||||
const chunks = ['Segment A\n\n\n\n', '\n\n\n\nSegment B'];
|
||||
|
||||
const segments = Array.from(converter.iterateSegmentsFromTextChunks(chunks, 8));
|
||||
|
||||
expect(segments).toEqual(['Segment A', 'Segment B']);
|
||||
});
|
||||
|
||||
it('convert should use chunked path for large files without calling file.arrayBuffer', async () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterFlowPrivateAPI;
|
||||
const calls: number[] = [];
|
||||
let arrayBufferCalled = false;
|
||||
const backingBlob = new Blob(['Header line\n\n\n\n\n\n\n\nChapter content']);
|
||||
|
||||
const largeFile = {
|
||||
name: 'large.txt',
|
||||
size: 9 * 1024 * 1024,
|
||||
slice: (start?: number, end?: number) => backingBlob.slice(start, end),
|
||||
stream: () => backingBlob.stream(),
|
||||
arrayBuffer: async () => {
|
||||
arrayBufferCalled = true;
|
||||
throw new Error('large path should not call file.arrayBuffer');
|
||||
},
|
||||
} as unknown as File;
|
||||
|
||||
converter.detectEncodingFromFile = async () => 'utf-8';
|
||||
converter.createEpub = async () => new Blob();
|
||||
converter.extractChaptersFromFileBySegments = async (_, __, ___, option) => {
|
||||
calls.push(option.linesBetweenSegments);
|
||||
if (option.linesBetweenSegments === 8) {
|
||||
return [{ title: 'Only', content: 'c', isVolume: false }];
|
||||
}
|
||||
if (option.linesBetweenSegments === 7) {
|
||||
return [
|
||||
{ title: 'A', content: 'a', isVolume: false },
|
||||
{ title: 'B', content: 'b', isVolume: false },
|
||||
];
|
||||
}
|
||||
return [{ title: 'Fallback', content: 'f', isVolume: false }];
|
||||
};
|
||||
converter.probeChapterCountFromFileBySegments = async (_, __, ___, option) => {
|
||||
calls.push(option.linesBetweenSegments);
|
||||
return 2;
|
||||
};
|
||||
|
||||
const result = await converter.convert({ file: largeFile });
|
||||
|
||||
expect(arrayBufferCalled).toBe(false);
|
||||
expect(calls).toEqual([8, 7, 7]);
|
||||
expect(result.chapterCount).toBe(2);
|
||||
});
|
||||
|
||||
it('convert large file should execute real chunked extraction without file.arrayBuffer', async () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterFlowPrivateAPI;
|
||||
let arrayBufferCalled = false;
|
||||
const backingBlob = new Blob(['Segment A\n\n\n\n\n\n\n\nSegment B']);
|
||||
|
||||
const largeFile = {
|
||||
name: 'large.txt',
|
||||
size: 9 * 1024 * 1024,
|
||||
slice: (start?: number, end?: number) => backingBlob.slice(start, end),
|
||||
stream: () => backingBlob.stream(),
|
||||
arrayBuffer: async () => {
|
||||
arrayBufferCalled = true;
|
||||
throw new Error('large path should not call file.arrayBuffer');
|
||||
},
|
||||
} as unknown as File;
|
||||
|
||||
converter.createEpub = async () => new Blob();
|
||||
|
||||
const result = await converter.convert({ file: largeFile });
|
||||
|
||||
expect(arrayBufferCalled).toBe(false);
|
||||
expect(result.chapterCount).toBe(2);
|
||||
});
|
||||
|
||||
it('iterateSegmentsFromFile should cancel stream on early return', async () => {
|
||||
const converter = new TxtToEpubConverter() as unknown as TxtConverterFlowPrivateAPI & {
|
||||
iterateSegmentsFromFile(
|
||||
file: File,
|
||||
encoding: string,
|
||||
linesBetweenSegments: number,
|
||||
): AsyncGenerator<string>;
|
||||
};
|
||||
const encoder = new TextEncoder();
|
||||
let cancelled = false;
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode('Segment A\n\n\n\n\n\n\n\nSegment B'));
|
||||
},
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
});
|
||||
|
||||
const file = {
|
||||
stream: () => stream,
|
||||
} as unknown as File;
|
||||
|
||||
const iterator = converter.iterateSegmentsFromFile(file, 'utf-8', 8);
|
||||
const first = await iterator.next();
|
||||
expect(first.value).toBe('Segment A');
|
||||
await iterator.return(undefined);
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
import { streamText, createGateway } from 'ai';
|
||||
import type { ModelMessage } from 'ai';
|
||||
|
||||
export async function POST(req: Request): Promise<Response> {
|
||||
try {
|
||||
const { user, token } = await validateUserAndToken(req.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return Response.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { messages, system, apiKey, model } = await req.json();
|
||||
|
||||
if (!messages || !Array.isArray(messages)) {
|
||||
return new Response(JSON.stringify({ error: 'Messages required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const gatewayApiKey = apiKey || process.env['AI_GATEWAY_API_KEY'];
|
||||
if (!gatewayApiKey) {
|
||||
return new Response(JSON.stringify({ error: 'API key required' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const gateway = createGateway({ apiKey: gatewayApiKey });
|
||||
const languageModel = gateway(model || 'google/gemini-2.5-flash-lite');
|
||||
|
||||
const result = streamText({
|
||||
model: languageModel,
|
||||
system: system || 'You are a helpful assistant.',
|
||||
messages: messages as ModelMessage[],
|
||||
});
|
||||
|
||||
return result.toTextStreamResponse();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return new Response(JSON.stringify({ error: `Chat failed: ${errorMessage}` }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { embed, embedMany, createGateway } from 'ai';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
|
||||
export async function POST(req: Request): Promise<Response> {
|
||||
try {
|
||||
const { user, token } = await validateUserAndToken(req.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { texts, single, apiKey } = await req.json();
|
||||
|
||||
if (!texts || !Array.isArray(texts) || texts.length === 0) {
|
||||
return NextResponse.json({ error: 'Texts array required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const gatewayApiKey = apiKey || process.env['AI_GATEWAY_API_KEY'];
|
||||
if (!gatewayApiKey) {
|
||||
return NextResponse.json({ error: 'API key required' }, { status: 401 });
|
||||
}
|
||||
|
||||
const gateway = createGateway({ apiKey: gatewayApiKey });
|
||||
const model = gateway.embeddingModel(
|
||||
process.env['AI_GATEWAY_EMBEDDING_MODEL'] || 'openai/text-embedding-3-small',
|
||||
);
|
||||
|
||||
if (single) {
|
||||
const { embedding } = await embed({ model, value: texts[0] });
|
||||
return NextResponse.json({ embedding });
|
||||
} else {
|
||||
const { embeddings } = await embedMany({ model, values: texts });
|
||||
return NextResponse.json({ embeddings });
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return NextResponse.json({ error: `Embedding failed: ${errorMessage}` }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -134,11 +134,25 @@ async function handleRequest(request: NextRequest, method: 'GET' | 'HEAD') {
|
||||
const buf = await response.arrayBuffer();
|
||||
const length = buf.byteLength;
|
||||
console.log(`[OPDS Proxy] Success: ${url} (${length} bytes)`);
|
||||
const excludedHeaders = new Set([
|
||||
'content-encoding',
|
||||
'content-length',
|
||||
'transfer-encoding',
|
||||
'connection',
|
||||
'keep-alive',
|
||||
]);
|
||||
|
||||
const proxyHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of response.headers.entries()) {
|
||||
if (!excludedHeaders.has(key.toLowerCase())) {
|
||||
proxyHeaders[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return new NextResponse(buf, {
|
||||
status: 200,
|
||||
headers: {
|
||||
...Object.fromEntries(response.headers.entries()),
|
||||
...proxyHeaders,
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': length.toString(),
|
||||
'Cache-Control': 'public, max-age=300',
|
||||
|
||||
@@ -231,7 +231,12 @@ export default function AuthPage() {
|
||||
settings.keepLogin = false;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
router.back();
|
||||
const redirectTo = new URLSearchParams(window.location.search).get('redirect');
|
||||
if (redirectTo) {
|
||||
router.push(redirectTo);
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
const getAuthLocalization = () => {
|
||||
@@ -411,7 +416,7 @@ export default function AuthPage() {
|
||||
Icon={FaDiscord}
|
||||
label={_('Sign in with {{provider}}', { provider: 'Discord' })}
|
||||
/>
|
||||
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
|
||||
<hr aria-hidden='true' className='border-base-300 my-3 mt-6 w-64 border-t' />
|
||||
<div className='w-full'>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { ViewTransitions } from 'next-view-transitions';
|
||||
import { EnvProvider } from '@/context/EnvContext';
|
||||
import Providers from '@/components/Providers';
|
||||
|
||||
@@ -72,9 +73,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<meta name='twitter:image' content={previewImage} />
|
||||
</head>
|
||||
<body>
|
||||
<EnvProvider>
|
||||
<Providers>{children}</Providers>
|
||||
</EnvProvider>
|
||||
<ViewTransitions>
|
||||
<EnvProvider>
|
||||
<Providers>{children}</Providers>
|
||||
</EnvProvider>
|
||||
</ViewTransitions>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { navigateToLogin } from '@/utils/nav';
|
||||
@@ -45,6 +46,7 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
|
||||
return (
|
||||
@@ -116,13 +118,16 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
</h4>
|
||||
)}
|
||||
<div
|
||||
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
|
||||
className={clsx(
|
||||
'flex items-center',
|
||||
book.progress || book.readingStatus ? 'justify-between' : 'justify-end',
|
||||
)}
|
||||
style={{
|
||||
height: `${iconSize15}px`,
|
||||
minHeight: `${iconSize15}px`,
|
||||
}}
|
||||
>
|
||||
{book.progress && <ReadingProgress book={book} />}
|
||||
{(book.progress || book.readingStatus) && <ReadingProgress book={book} />}
|
||||
<div className='flex items-center justify-center gap-x-2'>
|
||||
{!appService?.isMobile && (
|
||||
<button
|
||||
@@ -155,6 +160,7 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
) : (
|
||||
(!book.uploadedAt || (book.uploadedAt && !book.downloadedAt)) && (
|
||||
<button
|
||||
aria-label={!book.uploadedAt ? _('Upload Book') : _('Download Book')}
|
||||
className='show-cloud-button -m-2 p-2'
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={() => {
|
||||
@@ -169,7 +175,9 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!book.uploadedAt && <LiaCloudUploadAltSolid size={iconSize15} />}
|
||||
{!book.uploadedAt && settings.autoUpload && (
|
||||
<LiaCloudUploadAltSolid size={iconSize15} />
|
||||
)}
|
||||
{book.uploadedAt && !book.downloadedAt && (
|
||||
<LiaCloudDownloadAltSolid size={iconSize15} />
|
||||
)}
|
||||
|
||||
@@ -3,8 +3,13 @@ import * as React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { Book } from '@/types/book';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { Book, BooksGroup, ReadingStatus } from '@/types/book';
|
||||
import {
|
||||
LibraryCoverFitType,
|
||||
LibraryGroupByType,
|
||||
LibrarySortByType,
|
||||
LibraryViewModeType,
|
||||
} from '@/types/settings';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useAutoFocus } from '@/hooks/useAutoFocus';
|
||||
@@ -13,8 +18,18 @@ import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav';
|
||||
import { createBookFilter, createBookSorter } from '../utils/libraryUtils';
|
||||
import { formatTitle } from '@/utils/book';
|
||||
import {
|
||||
createBookFilter,
|
||||
createBookGroups,
|
||||
createBookSorter,
|
||||
createGroupSorter,
|
||||
createWithinGroupSorter,
|
||||
ensureLibraryGroupByType,
|
||||
ensureLibrarySortByType,
|
||||
getBookSortValue,
|
||||
getGroupSortValue,
|
||||
compareSortValues,
|
||||
} from '../utils/libraryUtils';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
@@ -23,6 +38,7 @@ import ModalPortal from '@/components/ModalPortal';
|
||||
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
|
||||
import SelectModeActions from './SelectModeActions';
|
||||
import GroupingModal from './GroupingModal';
|
||||
import SetStatusAlert from './SetStatusAlert';
|
||||
|
||||
interface BookshelfProps {
|
||||
libraryBooks: Book[];
|
||||
@@ -38,6 +54,7 @@ interface BookshelfProps {
|
||||
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleSetSelectMode: (selectMode: boolean) => void;
|
||||
handleShowDetailsBook: (book: Book) => void;
|
||||
handleLibraryNavigation: (targetGroup: string) => void;
|
||||
handlePushLibrary: () => Promise<void>;
|
||||
booksTransferProgress: { [key: string]: number | null };
|
||||
}
|
||||
@@ -53,27 +70,30 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
handleBookDelete,
|
||||
handleSetSelectMode,
|
||||
handleShowDetailsBook,
|
||||
handleLibraryNavigation,
|
||||
handlePushLibrary,
|
||||
booksTransferProgress,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { appService } = useEnv();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
|
||||
const groupId = searchParams?.get('group') || '';
|
||||
const queryTerm = searchParams?.get('q') || null;
|
||||
const viewMode = searchParams?.get('view') || settings.libraryViewMode;
|
||||
const sortBy = searchParams?.get('sort') || settings.librarySortBy;
|
||||
const sortBy = ensureLibrarySortByType(searchParams?.get('sort'), settings.librarySortBy);
|
||||
const sortOrder = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
|
||||
const groupBy = ensureLibraryGroupByType(searchParams?.get('groupBy'), settings.libraryGroupBy);
|
||||
const coverFit = searchParams?.get('cover') || settings.libraryCoverFit;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
|
||||
const [bookIdsToDelete, setBookIdsToDelete] = useState<string[]>([]);
|
||||
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
|
||||
const [showStatusAlert, setShowStatusAlert] = useState(false);
|
||||
const [showGroupingModal, setShowGroupingModal] = useState(false);
|
||||
const [importBookUrl] = useState(searchParams?.get('url') || '');
|
||||
|
||||
@@ -82,7 +102,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
const autofocusRef = useAutoFocus<HTMLDivElement>();
|
||||
|
||||
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
|
||||
const { setCurrentBookshelf, setLibrary, updateBooks } = useLibraryStore();
|
||||
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
|
||||
const { getGroupName } = useLibraryStore();
|
||||
|
||||
@@ -100,8 +120,9 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (params.get('sort') === 'updated') params.delete('sort');
|
||||
if (params.get('sort') === LibrarySortByType.Updated) params.delete('sort');
|
||||
if (params.get('order') === 'desc') params.delete('order');
|
||||
if (params.get('groupBy') === LibraryGroupByType.Group) params.delete('groupBy');
|
||||
if (params.get('cover') === 'crop') params.delete('cover');
|
||||
if (params.get('view') === 'grid') params.delete('view');
|
||||
|
||||
@@ -121,13 +142,33 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}, [libraryBooks, queryTerm]);
|
||||
|
||||
const currentBookshelfItems = useMemo(() => {
|
||||
const groupName = getGroupName(groupId) || '';
|
||||
if (groupId && !groupName) {
|
||||
return [];
|
||||
if (groupBy === LibraryGroupByType.Group) {
|
||||
// Use existing generateBookshelfItems for group mode
|
||||
const groupName = getGroupName(groupId) || '';
|
||||
if (groupId && !groupName) {
|
||||
return [];
|
||||
}
|
||||
return generateBookshelfItems(filteredBooks, groupName);
|
||||
} else {
|
||||
// Use new createBookGroups for series/author/none modes
|
||||
const allItems = createBookGroups(filteredBooks, groupBy);
|
||||
|
||||
// If navigating into a specific group, show only that group's books
|
||||
if (groupId) {
|
||||
const targetGroup = allItems.find(
|
||||
(item): item is BooksGroup => 'books' in item && item.id === groupId,
|
||||
);
|
||||
if (targetGroup) {
|
||||
// Return the books from the target group as individual items
|
||||
return targetGroup.books;
|
||||
}
|
||||
// Group not found, return empty
|
||||
return [];
|
||||
}
|
||||
|
||||
return allItems;
|
||||
}
|
||||
const items = generateBookshelfItems(filteredBooks, groupName);
|
||||
return items;
|
||||
}, [filteredBooks, groupId, getGroupName]);
|
||||
}, [filteredBooks, groupBy, groupId, getGroupName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupId && currentBookshelfItems.length === 0) {
|
||||
@@ -138,24 +179,60 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}, [searchParams, groupId, currentBookshelfItems.length, updateUrlParams]);
|
||||
|
||||
const sortedBookshelfItems = useMemo(() => {
|
||||
const bookSorter = createBookSorter(sortBy, uiLanguage);
|
||||
const sortOrderMultiplier = sortOrder === 'asc' ? 1 : -1;
|
||||
return currentBookshelfItems.sort((a, b) => {
|
||||
if (sortBy === 'updated') {
|
||||
return (
|
||||
(new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()) * sortOrderMultiplier
|
||||
);
|
||||
} else if ('name' in a || 'name' in b) {
|
||||
const aName = 'name' in a ? a.name : formatTitle(a.title);
|
||||
const bName = 'name' in b ? b.name : formatTitle(b.title);
|
||||
return aName.localeCompare(bName, uiLanguage || navigator.language) * sortOrderMultiplier;
|
||||
} else if (!('name' in a || 'name' in b)) {
|
||||
return bookSorter(a, b) * sortOrderMultiplier;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Separate into ungrouped books and groups
|
||||
const ungroupedBooks = currentBookshelfItems.filter((item): item is Book => 'format' in item);
|
||||
const groups = currentBookshelfItems.filter((item): item is BooksGroup => 'books' in item);
|
||||
|
||||
// Sort books within each group
|
||||
const withinGroupSorter = createWithinGroupSorter(groupBy, sortBy, uiLanguage);
|
||||
groups.forEach((group) => {
|
||||
group.books.sort((a, b) => withinGroupSorter(a, b) * sortOrderMultiplier);
|
||||
});
|
||||
}, [sortOrder, sortBy, uiLanguage, currentBookshelfItems]);
|
||||
|
||||
// Sort ungrouped books - use within-group sorter if we're inside a group
|
||||
// (for series, this ensures books are sorted by series index)
|
||||
const bookSorter = createBookSorter(sortBy, uiLanguage);
|
||||
if (groupId && groupBy !== LibraryGroupByType.Group && groupBy !== LibraryGroupByType.None) {
|
||||
ungroupedBooks.sort((a, b) => withinGroupSorter(a, b) * sortOrderMultiplier);
|
||||
} else {
|
||||
ungroupedBooks.sort((a, b) => bookSorter(a, b) * sortOrderMultiplier);
|
||||
}
|
||||
|
||||
// Merge groups and ungrouped books, then sort them together
|
||||
const allItems: (Book | BooksGroup)[] = [...groups, ...ungroupedBooks];
|
||||
const groupSorter = createGroupSorter(sortBy, uiLanguage);
|
||||
|
||||
allItems.sort((a, b) => {
|
||||
const isAGroup = 'books' in a;
|
||||
const isBGroup = 'books' in b;
|
||||
|
||||
// If both are groups, use group sorter
|
||||
if (isAGroup && isBGroup) {
|
||||
return groupSorter(a, b) * sortOrderMultiplier;
|
||||
}
|
||||
|
||||
// If both are books, use book sorter
|
||||
if (!isAGroup && !isBGroup) {
|
||||
return bookSorter(a, b) * sortOrderMultiplier;
|
||||
}
|
||||
|
||||
// One is a group, one is a book - compare their sort values
|
||||
if (isAGroup && !isBGroup) {
|
||||
const groupValue = getGroupSortValue(a, sortBy);
|
||||
const bookValue = getBookSortValue(b, sortBy);
|
||||
return compareSortValues(groupValue, bookValue, uiLanguage) * sortOrderMultiplier;
|
||||
} else if (!isAGroup && isBGroup) {
|
||||
const bookValue = getBookSortValue(a, sortBy);
|
||||
const groupValue = getGroupSortValue(b, sortBy);
|
||||
return compareSortValues(bookValue, groupValue, uiLanguage) * sortOrderMultiplier;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return allItems;
|
||||
}, [sortOrder, sortBy, groupBy, groupId, uiLanguage, currentBookshelfItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isImportingBook.current) return;
|
||||
@@ -247,6 +324,39 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
setShowGroupingModal(true);
|
||||
};
|
||||
|
||||
const showStatusSelection = () => {
|
||||
setShowSelectModeActions(false);
|
||||
setShowStatusAlert(true);
|
||||
};
|
||||
|
||||
const updateBooksStatus = async (status: ReadingStatus | undefined) => {
|
||||
const selectedIds = getSelectedBooks();
|
||||
const booksToUpdate: Book[] = [];
|
||||
|
||||
for (const id of selectedIds) {
|
||||
const book = filteredBooks.find((b) => b.hash === id);
|
||||
if (book) {
|
||||
booksToUpdate.push({ ...book, readingStatus: status, updatedAt: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
if (booksToUpdate.length > 0) {
|
||||
await updateBooks(envConfig, booksToUpdate);
|
||||
}
|
||||
|
||||
setSelectedBooks([]);
|
||||
setShowStatusAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
};
|
||||
|
||||
const handleUpdateReadingStatus = useCallback(
|
||||
async (book: Book, status: ReadingStatus | undefined) => {
|
||||
const updatedBook = { ...book, readingStatus: status, updatedAt: Date.now() };
|
||||
await updateBooks(envConfig, [updatedBook]);
|
||||
},
|
||||
[envConfig, updateBooks],
|
||||
);
|
||||
|
||||
const handleDeleteBooksIntent = (event: CustomEvent) => {
|
||||
const { ids } = event.detail;
|
||||
setBookIdsToDelete(ids);
|
||||
@@ -318,6 +428,8 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
handleBookDelete={handleBookDelete}
|
||||
handleSetSelectMode={handleSetSelectMode}
|
||||
handleShowDetailsBook={handleShowDetailsBook}
|
||||
handleLibraryNavigation={handleLibraryNavigation}
|
||||
handleUpdateReadingStatus={handleUpdateReadingStatus}
|
||||
transferProgress={
|
||||
'hash' in item ? booksTransferProgress[(item as Book).hash] || null : null
|
||||
}
|
||||
@@ -325,7 +437,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
))}
|
||||
{viewMode === 'grid' && currentBookshelfItems.length > 0 && (
|
||||
<div
|
||||
className={clsx('mx-0 my-2 sm:mx-4 sm:my-4')}
|
||||
className={clsx('bookshelf-import-item mx-0 my-2 sm:mx-4 sm:my-4')}
|
||||
style={
|
||||
coverFit === 'fit' && viewMode === 'grid'
|
||||
? {
|
||||
@@ -363,6 +475,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
onOpen={openSelectedBooks}
|
||||
onGroup={groupSelectedBooks}
|
||||
onDetails={openBookDetails}
|
||||
onStatus={showStatusSelection}
|
||||
onDelete={deleteSelectedBooks}
|
||||
onCancel={() => handleSetSelectMode(false)}
|
||||
/>
|
||||
@@ -405,6 +518,17 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showStatusAlert && (
|
||||
<SetStatusAlert
|
||||
selectedCount={getSelectedBooks().length}
|
||||
safeAreaBottom={safeAreaInsets?.bottom || 0}
|
||||
onCancel={() => {
|
||||
setShowStatusAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
}}
|
||||
onUpdateStatus={updateBooksStatus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { navigateToReader, showReaderWindow } from '@/utils/nav';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
@@ -15,7 +15,7 @@ import { throttle } from '@/utils/throttle';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { Book, BooksGroup, ReadingStatus } from '@/types/book';
|
||||
import { md5Fingerprint } from '@/utils/md5';
|
||||
import BookItem from './BookItem';
|
||||
import GroupItem from './GroupItem';
|
||||
@@ -24,51 +24,60 @@ export const generateBookshelfItems = (
|
||||
books: Book[],
|
||||
parentGroupName: string,
|
||||
): (Book | BooksGroup)[] => {
|
||||
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
|
||||
if (book.deletedAt) return acc;
|
||||
if (parentGroupName && (!book.groupName || !book.groupName.startsWith(parentGroupName)))
|
||||
return acc;
|
||||
book.groupId = book.groupId || BOOK_UNGROUPED_ID;
|
||||
book.groupName = book.groupName || BOOK_UNGROUPED_NAME;
|
||||
const slashIndex = book.groupName.indexOf('/', parentGroupName.length + 1);
|
||||
const leafGroupName = book.groupName.substring(
|
||||
parentGroupName ? parentGroupName.length + 1 : 0,
|
||||
slashIndex > 0 ? slashIndex : undefined,
|
||||
);
|
||||
const fullGroupName = parentGroupName
|
||||
? `${parentGroupName}${leafGroupName ? `/${leafGroupName}` : ''}`
|
||||
: leafGroupName;
|
||||
const groupIndex = acc.findIndex(
|
||||
(group) =>
|
||||
group.name === fullGroupName ||
|
||||
(parentGroupName && group.name === parentGroupName) ||
|
||||
(leafGroupName === BOOK_UNGROUPED_NAME && group.name === BOOK_UNGROUPED_NAME),
|
||||
);
|
||||
const booksGroup = acc[groupIndex];
|
||||
if (booksGroup) {
|
||||
booksGroup.books.push(book);
|
||||
booksGroup.updatedAt = Math.max(booksGroup.updatedAt, book.updatedAt);
|
||||
const groupsMap = new Map<string, BooksGroup>();
|
||||
|
||||
for (const book of books) {
|
||||
if (book.deletedAt) continue;
|
||||
|
||||
const groupName = book.groupName || BOOK_UNGROUPED_NAME;
|
||||
if (
|
||||
parentGroupName &&
|
||||
groupName !== parentGroupName &&
|
||||
!groupName.startsWith(parentGroupName + '/')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativePath = parentGroupName ? groupName.slice(parentGroupName.length + 1) : groupName;
|
||||
// Get the immediate child group name (or empty if book is directly in parent)
|
||||
const slashIndex = relativePath.indexOf('/');
|
||||
const immediateChild = slashIndex > 0 ? relativePath.slice(0, slashIndex) : relativePath;
|
||||
// Determine if this book belongs directly to the parent group
|
||||
const isDirectChild =
|
||||
groupName === parentGroupName || (groupName === BOOK_UNGROUPED_NAME && !parentGroupName);
|
||||
// Build the full group name for this level
|
||||
const fullGroupName = isDirectChild
|
||||
? BOOK_UNGROUPED_NAME
|
||||
: parentGroupName
|
||||
? `${parentGroupName}/${immediateChild}`
|
||||
: immediateChild;
|
||||
|
||||
const mapKey = fullGroupName;
|
||||
const existingGroup = groupsMap.get(mapKey);
|
||||
if (existingGroup) {
|
||||
existingGroup.books.push(book);
|
||||
existingGroup.updatedAt = Math.max(existingGroup.updatedAt, book.updatedAt);
|
||||
} else {
|
||||
const groupName = fullGroupName;
|
||||
acc.push({
|
||||
id: groupName === parentGroupName ? BOOK_UNGROUPED_ID : md5Fingerprint(groupName),
|
||||
name: groupName === parentGroupName ? BOOK_UNGROUPED_NAME : groupName,
|
||||
displayName: leafGroupName,
|
||||
groupsMap.set(mapKey, {
|
||||
id: isDirectChild ? BOOK_UNGROUPED_ID : md5Fingerprint(fullGroupName),
|
||||
name: fullGroupName,
|
||||
displayName: isDirectChild ? BOOK_UNGROUPED_NAME : immediateChild,
|
||||
books: [book],
|
||||
updatedAt: book.updatedAt,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
groups.forEach((group) => {
|
||||
}
|
||||
|
||||
for (const group of groupsMap.values()) {
|
||||
group.books.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
});
|
||||
const ungroupedBooks: Book[] =
|
||||
groups.find((group) => group.name === BOOK_UNGROUPED_NAME || group.name === parentGroupName)
|
||||
?.books || [];
|
||||
const groupedBooks: BooksGroup[] = groups.filter(
|
||||
(group) => group.name !== BOOK_UNGROUPED_NAME && group.name !== parentGroupName,
|
||||
}
|
||||
|
||||
const ungroupedGroup = groupsMap.get(BOOK_UNGROUPED_NAME);
|
||||
const ungroupedBooks = ungroupedGroup?.books || [];
|
||||
const groupedBooks = Array.from(groupsMap.values()).filter(
|
||||
(group) => group.name !== BOOK_UNGROUPED_NAME,
|
||||
);
|
||||
|
||||
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
};
|
||||
|
||||
@@ -90,6 +99,8 @@ interface BookshelfItemProps {
|
||||
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleSetSelectMode: (selectMode: boolean) => void;
|
||||
handleShowDetailsBook: (book: Book) => void;
|
||||
handleLibraryNavigation: (targetGroup: string) => void;
|
||||
handleUpdateReadingStatus: (book: Book, status: ReadingStatus | undefined) => void;
|
||||
}
|
||||
|
||||
const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
@@ -106,10 +117,11 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
handleBookDownload,
|
||||
handleSetSelectMode,
|
||||
handleShowDetailsBook,
|
||||
handleLibraryNavigation,
|
||||
handleUpdateReadingStatus,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useTransitionRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { updateBook } = useLibraryStore();
|
||||
@@ -168,15 +180,11 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
if (isSelectMode) {
|
||||
toggleSelection(group.id);
|
||||
} else {
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('group', group.id);
|
||||
setTimeout(() => {
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
}, 0);
|
||||
handleLibraryNavigation(group.id);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isSelectMode, searchParams],
|
||||
[isSelectMode, handleLibraryNavigation],
|
||||
);
|
||||
|
||||
const bookContextMenuHandler = async (book: Book) => {
|
||||
@@ -201,6 +209,24 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
handleGroupBooks();
|
||||
},
|
||||
});
|
||||
const markAsFinishedMenuItem = await MenuItem.new({
|
||||
text: _('Mark as Finished'),
|
||||
action: async () => {
|
||||
handleUpdateReadingStatus(book, 'finished');
|
||||
},
|
||||
});
|
||||
const markAsUnreadMenuItem = await MenuItem.new({
|
||||
text: _('Mark as Unread'),
|
||||
action: async () => {
|
||||
handleUpdateReadingStatus(book, 'unread');
|
||||
},
|
||||
});
|
||||
const clearStatusMenuItem = await MenuItem.new({
|
||||
text: _('Clear Status'),
|
||||
action: async () => {
|
||||
handleUpdateReadingStatus(book, undefined);
|
||||
},
|
||||
});
|
||||
const showBookInFinderMenuItem = await MenuItem.new({
|
||||
text: _(fileRevealLabel),
|
||||
action: async () => {
|
||||
@@ -235,6 +261,15 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const menu = await Menu.new();
|
||||
menu.append(selectBookMenuItem);
|
||||
menu.append(groupBooksMenuItem);
|
||||
if (book.readingStatus === 'finished') {
|
||||
menu.append(markAsUnreadMenuItem);
|
||||
} else {
|
||||
menu.append(markAsFinishedMenuItem);
|
||||
}
|
||||
// show "Clear Status" option when book has an explicit status set
|
||||
if (book.readingStatus === 'finished' || book.readingStatus === 'unread') {
|
||||
menu.append(clearStatusMenuItem);
|
||||
}
|
||||
menu.append(showBookDetailsMenuItem);
|
||||
menu.append(showBookInFinderMenuItem);
|
||||
if (book.uploadedAt && !book.downloadedAt) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { MdArrowBack } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import { LibraryGroupByType } from '@/types/settings';
|
||||
|
||||
interface GroupHeaderProps {
|
||||
groupBy: LibraryGroupByType;
|
||||
groupName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header component displayed when viewing books inside a series or author group.
|
||||
* Shows the group type, group name, and a back button to return to the main bookshelf.
|
||||
*/
|
||||
const GroupHeader: React.FC<GroupHeaderProps> = ({ groupBy, groupName }) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const iconSize = useResponsiveSize(20);
|
||||
|
||||
const handleBack = () => {
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.delete('group');
|
||||
navigateToLibrary(router, params.toString());
|
||||
};
|
||||
|
||||
// Get localized label for the group type
|
||||
const getGroupTypeLabel = (): string => {
|
||||
switch (groupBy) {
|
||||
case LibraryGroupByType.Series:
|
||||
return _('Series');
|
||||
case LibraryGroupByType.Author:
|
||||
return _('Author');
|
||||
default:
|
||||
return _('Group');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-2 px-4 py-2'>
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className='btn btn-ghost btn-sm h-8 min-h-8 px-2'
|
||||
aria-label={_('Back to library')}
|
||||
>
|
||||
<MdArrowBack size={iconSize} />
|
||||
</button>
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<span className='text-neutral-content text-sm'>{getGroupTypeLabel()}:</span>
|
||||
<span className='truncate text-base font-medium'>{groupName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupHeader;
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { MdCheckCircle, MdCheckCircleOutline, MdChevronRight, MdChevronLeft } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { BooksGroup } from '@/types/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
@@ -15,6 +16,7 @@ interface GroupItemProps {
|
||||
}
|
||||
|
||||
const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupSelected }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -147,6 +149,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
|
||||
<div className='absolute left-[-0.5px] top-0 h-full w-12'>
|
||||
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-r to-transparent'></div>
|
||||
<button
|
||||
aria-label={_('Scroll left')}
|
||||
onClick={handleLeftArrowClick}
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
@@ -168,6 +171,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
|
||||
<div className='absolute right-[-0.5px] top-0 h-full w-12'>
|
||||
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-l to-transparent'></div>
|
||||
<button
|
||||
aria-label={_('Scroll right')}
|
||||
onClick={handleRightArrowClick}
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { IoFileTray } from 'react-icons/io5';
|
||||
import { MdRssFeed } from 'react-icons/md';
|
||||
|
||||
import { IoFileTray } from 'react-icons/io5';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import Menu from '@/components/Menu';
|
||||
|
||||
@@ -20,6 +20,7 @@ const ImportMenu: React.FC<ImportMenuProps> = ({
|
||||
onOpenCatalogManager,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
|
||||
const handleImportFromFiles = () => {
|
||||
onImportBooksFromFiles();
|
||||
@@ -38,7 +39,7 @@ const ImportMenu: React.FC<ImportMenuProps> = ({
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={clsx('dropdown-content bg-base-100 rounded-box z-[1] mt-3 p-2 shadow')}
|
||||
className={clsx('dropdown-content bg-base-100 rounded-box !relative z-[1] mt-3 p-2 shadow')}
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
<MenuItem
|
||||
@@ -54,7 +55,7 @@ const ImportMenu: React.FC<ImportMenuProps> = ({
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Online Library')}
|
||||
label={appService?.isOnlineCatalogsAccessible ? _('Online Library') : _('OPDS Catalogs')}
|
||||
Icon={<MdRssFeed className='h-5 w-5' />}
|
||||
onClick={handleOpenCatalogManager}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import ViewMenu from './ViewMenu';
|
||||
interface LibraryHeaderProps {
|
||||
isSelectMode: boolean;
|
||||
isSelectAll: boolean;
|
||||
onPullLibrary: () => void;
|
||||
onImportBooksFromFiles: () => void;
|
||||
onImportBooksFromDirectory?: () => void;
|
||||
onOpenCatalogManager: () => void;
|
||||
@@ -36,6 +37,7 @@ interface LibraryHeaderProps {
|
||||
const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
isSelectMode,
|
||||
isSelectAll,
|
||||
onPullLibrary,
|
||||
onImportBooksFromFiles,
|
||||
onImportBooksFromDirectory,
|
||||
onOpenCatalogManager,
|
||||
@@ -101,7 +103,9 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
style={{
|
||||
marginTop: appService?.hasSafeAreaInset
|
||||
? `max(${insets.top}px, ${systemUIVisible ? statusBarHeight : 0}px)`
|
||||
: '0px',
|
||||
: appService?.hasTrafficLight
|
||||
? '-2px'
|
||||
: '0px',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
|
||||
@@ -149,7 +153,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<Dropdown
|
||||
label={_('Import Books')}
|
||||
className={clsx(
|
||||
'exclude-title-bar-mousedown dropdown-bottom dropdown-center flex h-6 cursor-pointer justify-center',
|
||||
'exclude-title-bar-mousedown dropdown-bottom dropdown-center cursor-pointer',
|
||||
)}
|
||||
buttonClassName='p-0 h-6 min-h-6 w-6 flex touch-target items-center justify-center !bg-transparent'
|
||||
toggleButton={<PiPlus role='none' className='m-0.5 h-5 w-5' />}
|
||||
@@ -209,7 +213,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu role='none' size={iconSize18} />}
|
||||
>
|
||||
<SettingsMenu />
|
||||
<SettingsMenu onPullLibrary={onPullLibrary} />
|
||||
</Dropdown>
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
|
||||
@@ -327,7 +327,7 @@ export const MigrateDataWindow = () => {
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'folder-menu dropdown-content dropdown-center no-triangle',
|
||||
'folder-menu dropdown-content no-triangle left-0',
|
||||
'border-base-300 !bg-base-200 z-20 mt-1 max-w-[90vw] shadow-2xl',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { clsx } from 'clsx';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { CatalogManager } from '@/app/opds/components/CatelogManager';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Dialog from '@/components/Dialog';
|
||||
@@ -9,10 +10,11 @@ interface CatalogDialogProps {
|
||||
|
||||
export function CatalogDialog({ onClose }: CatalogDialogProps) {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={_('Online Library')}
|
||||
title={appService?.isOnlineCatalogsAccessible ? _('Online Library') : _('OPDS Catalogs')}
|
||||
onClose={onClose}
|
||||
bgClassName={'sm:!bg-black/75'}
|
||||
boxClassName='sm:min-w-[520px] sm:w-3/4 sm:h-[85%] sm:!max-w-screen-sm'
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type React from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import type { Book } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { SHOW_UNREAD_STATUS_BADGE } from '@/services/constants';
|
||||
import StatusBadge from './StatusBadge';
|
||||
|
||||
interface ReadingProgressProps {
|
||||
book: Book;
|
||||
@@ -19,10 +22,31 @@ const getProgressPercentage = (book: Book) => {
|
||||
|
||||
const ReadingProgress: React.FC<ReadingProgressProps> = memo(
|
||||
({ book }) => {
|
||||
const _ = useTranslation();
|
||||
const progressPercentage = useMemo(() => getProgressPercentage(book), [book]);
|
||||
|
||||
if (book.readingStatus === 'finished') {
|
||||
return (
|
||||
<div className='flex justify-start'>
|
||||
<StatusBadge status={book.readingStatus}>{_('Finished')}</StatusBadge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (book.readingStatus === 'unread') {
|
||||
if (SHOW_UNREAD_STATUS_BADGE) {
|
||||
return (
|
||||
<div className='flex justify-start'>
|
||||
<StatusBadge status={book.readingStatus}>{_('Unread')}</StatusBadge>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div className='flex justify-start'></div>;
|
||||
}
|
||||
}
|
||||
|
||||
if (progressPercentage === null || Number.isNaN(progressPercentage)) {
|
||||
return null;
|
||||
return <div className='flex justify-start'></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -38,7 +62,8 @@ const ReadingProgress: React.FC<ReadingProgressProps> = memo(
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.book.hash === nextProps.book.hash &&
|
||||
prevProps.book.updatedAt === nextProps.book.updatedAt
|
||||
prevProps.book.updatedAt === nextProps.book.updatedAt &&
|
||||
prevProps.book.readingStatus === nextProps.book.readingStatus
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
|
||||
import {
|
||||
MdDelete,
|
||||
MdOpenInNew,
|
||||
MdOutlineCancel,
|
||||
MdInfoOutline,
|
||||
MdCheckCircleOutline,
|
||||
} from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -11,6 +17,7 @@ interface SelectModeActionsProps {
|
||||
onOpen: () => void;
|
||||
onGroup: () => void;
|
||||
onDetails: () => void;
|
||||
onStatus: () => void;
|
||||
onDelete: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
@@ -21,6 +28,7 @@ const SelectModeActions: React.FC<SelectModeActionsProps> = ({
|
||||
onOpen,
|
||||
onGroup,
|
||||
onDetails,
|
||||
onStatus,
|
||||
onDelete,
|
||||
onCancel,
|
||||
}) => {
|
||||
@@ -41,9 +49,11 @@ const SelectModeActions: React.FC<SelectModeActionsProps> = ({
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'text-base-content flex items-center justify-center text-xs shadow-lg',
|
||||
'text-base-content text-xs shadow-lg',
|
||||
'not-eink:bg-base-300 eink:bg-base-100 eink:border eink:border-base-content',
|
||||
'mx-auto w-fit space-x-6 rounded-lg p-4',
|
||||
'mx-auto w-fit rounded-lg p-4',
|
||||
'flex items-center justify-center space-x-6',
|
||||
'max-[350px]:grid max-[350px]:grid-cols-3 max-[350px]:gap-x-10 max-[350px]:gap-y-2 max-[350px]:space-x-0',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -66,6 +76,16 @@ const SelectModeActions: React.FC<SelectModeActionsProps> = ({
|
||||
<LuFolderPlus />
|
||||
<div>{_('Group')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onStatus}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(!hasSelection || !hasValidBooks) && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdCheckCircleOutline />
|
||||
<div>{_('Status')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDetails}
|
||||
className={clsx(
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { PiX } from 'react-icons/pi';
|
||||
import { ReadingStatus } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
|
||||
|
||||
interface SetStatusAlertProps {
|
||||
selectedCount: number;
|
||||
safeAreaBottom: number;
|
||||
onCancel: () => void;
|
||||
onUpdateStatus: (status: ReadingStatus | undefined) => void;
|
||||
}
|
||||
|
||||
const SetStatusAlert: React.FC<SetStatusAlertProps> = ({
|
||||
selectedCount,
|
||||
safeAreaBottom,
|
||||
onCancel,
|
||||
onUpdateStatus,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const divRef = useKeyDownActions({ onCancel });
|
||||
|
||||
const statusButtons = [
|
||||
{
|
||||
label: _('Mark as Unread'),
|
||||
status: 'unread' as ReadingStatus,
|
||||
className:
|
||||
'not-eink:bg-amber-500/15 not-eink:text-amber-600 dark:not-eink:text-amber-400 not-eink:border-amber-500/20 eink-bordered',
|
||||
},
|
||||
{
|
||||
label: _('Mark as Finished'),
|
||||
status: 'finished' as ReadingStatus,
|
||||
className:
|
||||
'not-eink:bg-success/15 not-eink:text-success not-eink:border-success/20 eink-bordered',
|
||||
},
|
||||
{
|
||||
label: _('Clear Status'),
|
||||
status: undefined,
|
||||
className:
|
||||
'not-eink:bg-base-300 not-eink:text-base-content not-eink:border-base-content/10 eink-bordered',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={clsx('status-alert fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
|
||||
style={{
|
||||
paddingBottom: `${safeAreaBottom + 16}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-auto max-w-[90vw] flex-col gap-3',
|
||||
'border-base-content/10 bg-base-200/95 rounded-2xl border p-4',
|
||||
'shadow-lg backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
{/* Header with close button for small screens */}
|
||||
<div className='relative flex items-center justify-center'>
|
||||
<div className='text-center text-sm font-medium'>
|
||||
{_('Set status for {{count}} book(s)', { count: selectedCount })}
|
||||
</div>
|
||||
<button
|
||||
className={clsx(
|
||||
'absolute right-0 flex items-center justify-center',
|
||||
'rounded-full p-1.5 transition-colors',
|
||||
'hover:bg-base-content/10',
|
||||
'sm:hidden',
|
||||
)}
|
||||
onClick={onCancel}
|
||||
aria-label={_('Cancel')}
|
||||
>
|
||||
<PiX className='size-5' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center justify-center gap-2'>
|
||||
{statusButtons.map(({ label, status, className }) => (
|
||||
<button
|
||||
key={label}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 rounded-full border px-4 py-2',
|
||||
'shadow-sm transition-all duration-200 ease-out active:scale-[0.97]',
|
||||
className,
|
||||
)}
|
||||
onClick={() => onUpdateStatus(status)}
|
||||
>
|
||||
<span className='text-sm font-medium'>{label}</span>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className={clsx(
|
||||
'hidden items-center gap-2 rounded-full border px-4 py-2',
|
||||
'not-eink:bg-base-300 not-eink:text-base-content not-eink:border-base-content/10 not-eink:shadow-sm',
|
||||
'eink-bordered',
|
||||
'transition-all duration-200 ease-out active:scale-[0.97]',
|
||||
'sm:flex',
|
||||
)}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<span className='text-sm font-medium'>{_('Cancel')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetStatusAlert;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user