Compare commits

...

118 Commits

Author SHA1 Message Date
Huang Xin 83f0d135c5 release: version 0.9.99 (#3190) 2026-02-06 18:51:03 +01:00
Huang Xin 1ed84a3256 feat(tts): navigation in TTS mode with back to current button, closes #3100 (#3189) 2026-02-06 18:26:14 +01:00
Huang Xin 9d6394fe2b fix(layout): fixed page navigation buttons have higher z-index than TTS control button (#3184) 2026-02-06 11:00:56 +01:00
Huang Xin fe9603ffb8 fix(a11y): updating progress info when navigating with screen readers (#3182)
Also add some missing button labels for screen readers
2026-02-06 08:43:03 +01:00
boludo00 681e065ac4 fix(rsvp): fix position restoration and keyboard handling (#3150)
* feat: add RSVP speed reading feature

Implements Rapid Serial Visual Presentation (RSVP) speed reading mode:

- Display words one at a time with ORP (Optimal Recognition Point) highlighting
- Adjustable WPM speed (100-1000) with keyboard/button controls
- Punctuation pause settings (25-200ms)
- Progress tracking with chapter navigation
- Context panel showing surrounding text when paused
- Keyboard shortcuts (Space, Escape, arrows) and touch gestures
- Chapter selector for quick navigation
- Respects current theme colors
- Persists settings (WPM, pause duration, position) per book

New files:
- services/rsvp/RSVPController.ts - Main controller with playback logic
- services/rsvp/types.ts - TypeScript interfaces
- components/rsvp/RSVPOverlay.tsx - Full-screen RSVP reader UI
- components/rsvp/RSVPControl.tsx - Integration component
- components/rsvp/RSVPStartDialog.tsx - Start position selection

Closes #3111

* test(rsvp): add Playwright e2e tests for RSVP feature

- Set up Playwright test infrastructure with config
- Add comprehensive e2e tests for RSVP speed reading:
  - Opening RSVP from View menu
  - Start dialog options
  - Play/pause toggle
  - Speed adjustment
  - Skip navigation
  - Keyboard shortcuts
  - Progress bar
  - Chapter navigation
  - Accessibility tests
- Add test data attributes and ARIA labels to RSVPOverlay
- Add test scripts to package.json

* refactor: remove test files, unsure of use case for now

* chore: remove Playwright e2e test scripts from package.json

- Deleted e2e test scripts related to Playwright from package.json as they are no longer needed.
- Removed Playwright as a dev dependency to streamline the project.

* chore: sync pnpm-lock.yaml after removing @playwright/test

* fix: lint errors and timezone-aware date formatting

* fix(rsvp): restore correct position when exiting RSVP mode

Fix bug where exiting RSVP after changing speed would navigate to wrong
position (several pages backwards). The issue was that Range objects
become stale if the document re-renders, and the position recovery used
global word index instead of per-document index.

- Add docWordIndex to track word position within each document
- Add docTotalWords to RsvpStopPosition for accurate recovery
- Validate Range before using it, recreate from current DOM if stale
- Use same word extraction logic as RSVPController for consistency

* fix(rsvp): prevent arrow keys from changing chapter dropdown

Arrow keys during RSVP mode were inadvertently navigating the chapter
dropdown instead of controlling RSVP speed. Fixed by using capture phase
for keyboard events and stopping propagation to prevent native elements
from receiving the events.

* chore: remove playwright-mcp screenshots from repo

* chore: update gitignore

* fix(i18n): wrap all RSVP overlay strings with translation function

Restore missing translation wrappers for all user-facing strings:
- Close RSVP button labels and tooltips
- Select Chapter dropdown
- WPM display
- Context panel header
- Ready state text
- Chapter Progress label
- Words count and time remaining
- Reading progress slider
- Pause/punctuation label
- Skip back/forward buttons
- Play/Pause button
- Speed control buttons

* fix(rsvp): use CFI-based position tracking for accurate page positioning

- Add CFI generation for each extracted word during RSVP processing
- Implement CFI path and character offset comparison for precise position matching
- Update startFromCurrentPosition to use CFI matching instead of word index
- Add extractFullDocPathWithOffset to handle CFI character offsets
- Simplify RSVPControl by removing unused helper functions
- Remove word index from RsvpPosition in favor of CFI-only tracking

* fix(ViewMenu): re-enable Speed Reading Mode in production environment

* refactor(ViewMenu, RSVPController): clean up code formatting and improve readability

* refactor(ViewMenu): remove unused IoSpeedometer import

* refactor(RSVPController): streamline CFI handling and improve section comparison

- Removed redundant logic and re-using CFI util functions for CFI operations

* refactor(RsvpPosition): remove legacy wordIndex field for cleaner type definition
2026-02-05 18:19:13 +01:00
Huang Xin f64fc5723e fix(a11y): double tap to focus on current paragraph and update page info with TalkBack, closes #2276 (#3179) 2026-02-05 18:18:45 +01:00
Huang Xin a9a1dc8e70 fix(layout): fixed regression of vertical alignment, closes #3012 (#3178) 2026-02-05 17:46:47 +01:00
Huang Xin b1a1e35790 fix(layout): move sync options inside of account section in settings menu (#3176) 2026-02-05 10:08:17 +01:00
Huang Xin 3c538c3746 feat(sync): add manual books sync in the main menu (#3175) 2026-02-05 08:26:33 +01:00
Huang Xin 9834bd57cf feat(toc): add page number for nested TOC items, closes #2953 (#3174) 2026-02-05 08:19:18 +01:00
Huang Xin b89171a4d8 compat(iOS): disable native CompressionStream/DecompressionStream API in zip.js for iOS 15.x (#3170) 2026-02-04 13:18:36 +01:00
Huang Xin 92116e7455 fix(bookshelf): merge groups and ungrouped books then sort them together (#3169) 2026-02-04 12:32:29 +01:00
Huang Xin 8dfab2c963 feat(a11y): always show page navigation buttons in reader page for screen readers, closes #3036 (#3167) 2026-02-04 10:07:37 +01:00
Huang Xin 9f261f12e9 fix(kosync): don't show conflict prompt when progress diff is less than 0.01% (usually from the same device) (#3166)
This should close #3137.
2026-02-04 08:28:15 +01:00
Huang Xin e74615ac1d feat(export): add an option to export annotations as plain text, closes #3147 (#3165) 2026-02-04 07:50:40 +01:00
Huang Xin 3b350d6945 fix(layout): responsive select mode actions on smaller screens (#3164) 2026-02-04 07:04:29 +01:00
Huang Xin 9e0e3fde7d fix(layout): fix aligment of options in settings menu, closes #3151 (#3163) 2026-02-04 06:42:14 +01:00
Adam Charron 52c49ddef1 Add enhanced grouping and sorting functionality to the library view (#3146)
* feat(library): implement grouping of books by series or author

- Added functionality to group books in the library by series or author.
- Introduced new utility functions for creating book groups and parsing author strings.
- Updated the LibraryPageContent to track the current group for navigation.
- Created a new GroupHeader component to display the current group context.
- Enhanced the Bookshelf component to support displaying grouped books.
- Updated settings to include grouping options in the view menu.

New files:
- src/__tests__/utils/libraryUtils.test.ts - Unit tests for new utility functions.
- src/app/library/components/GroupHeader.tsx - Component for displaying group context.
- src/app/library/utils/libraryUtils.ts - Utility functions for grouping and parsing authors.

* Use group by and sort by constants, and split sort/group by menus into their own elements

* Format code with autoformatter

* Remove any casting from tests

* Translate missing strings

* refactor: cleaner layout with collapsible view menu options

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-02-03 14:51:50 +01:00
Huang Xin e3d52891fb fix(a11y): add missing aria labels and fix iOS safe insets (#3149) 2026-02-03 12:50:24 +01:00
Huang Xin 9cd88fe839 fix(progress): show correct progress info for subsections in FB2, closes #3136 (#3145) 2026-02-02 17:53:26 +01:00
Huang Xin fca3917a12 fix(toc): prevent unintentional scrolling in the TOC, closes #3124 (#3144) 2026-02-02 14:28:11 +01:00
Adam Charron c90de6967b Change setup command for vendors in CONTRIBUTING.md (#3139)
Updated instructions for setting up dependencies in the project.
2026-02-02 04:41:54 +01:00
boludo00 bbbd378f9b feat: rsvp speed reading (#3121)
* feat: add RSVP speed reading feature

Implements Rapid Serial Visual Presentation (RSVP) speed reading mode:

- Display words one at a time with ORP (Optimal Recognition Point) highlighting
- Adjustable WPM speed (100-1000) with keyboard/button controls
- Punctuation pause settings (25-200ms)
- Progress tracking with chapter navigation
- Context panel showing surrounding text when paused
- Keyboard shortcuts (Space, Escape, arrows) and touch gestures
- Chapter selector for quick navigation
- Respects current theme colors
- Persists settings (WPM, pause duration, position) per book

New files:
- services/rsvp/RSVPController.ts - Main controller with playback logic
- services/rsvp/types.ts - TypeScript interfaces
- components/rsvp/RSVPOverlay.tsx - Full-screen RSVP reader UI
- components/rsvp/RSVPControl.tsx - Integration component
- components/rsvp/RSVPStartDialog.tsx - Start position selection

Closes #3111

* fix(rsvp): use portal to fix overlay stacking context issue

- Render RSVP overlay and start dialog via React Portal to document.body
- This ensures the overlay appears above all other content regardless of
  parent component CSS transforms or stacking contexts
- Add fallback colors for theme values to ensure solid background

* fix(rsvp): improve UX with progress sync, sentence highlight, and better dialogs

- Fix start dialog transparency by using solid opaque background with proper
  fallback colors for both light and dark modes
- Increase context words from 50 to 100 words before/after current word
- Add progress sync on RSVP exit - navigates reader to the last word position
- Add temporary bright green sentence underline on exit (5 second duration)
  to easily locate where reading left off
- Helper function to expand word range to full sentence boundaries

* fix(rsvp): store full BookNote for proper annotation removal

The addAnnotation API requires the full BookNote object (including CFI)
when removing annotations, not just the ID. Changed tempHighlightIdRef
to tempHighlightRef to store the complete BookNote object.

* test(rsvp): add Playwright e2e tests for RSVP feature

- Set up Playwright test infrastructure with config
- Add comprehensive e2e tests for RSVP speed reading:
  - Opening RSVP from View menu
  - Start dialog options
  - Play/pause toggle
  - Speed adjustment
  - Skip navigation
  - Keyboard shortcuts
  - Progress bar
  - Chapter navigation
  - Accessibility tests
- Add test data attributes and ARIA labels to RSVPOverlay
- Add test scripts to package.json

* fix(rsvp): clarify start dialog option labels

Update the RSVP start dialog to use clearer language:
- "From Beginning" → "From Chapter Start" (since it starts from chapter beginning)
- "From Current Position" → "From Current Page" (starts from visible page)

* fix(rsvp): use correct theme colors from themeCode

The RSVP components were using incorrect palette key names (camelCase
like `base100` instead of hyphenated like `base-100`), causing the
fallback colors to always be used instead of the reader's actual theme.

Fix by using themeCode.bg, themeCode.fg, and themeCode.primary directly,
which are already resolved from the palette with correct keys.

* fix(rsvp): use theme accent color for sentence underline and persist until page change

- Change underline color from hardcoded green (#22c55e) to theme accent
  color (themeCode.primary), matching the ORP focal point highlight
- Remove 5-second timeout that auto-removed the underline
- Add cleanup on page navigation, new RSVP session start, and unmount
- Add removeRsvpHighlight helper function for consistent cleanup

* fix(rsvp): transition to next chapter when reaching end of section

When RSVP reached the end of a chapter, it would restart the current
chapter instead of moving to the next one. This happened because RSVP
extracts ALL words from the current section via renderer.getContents(),
so when words run out, the entire chapter is done.

- Always use view.renderer.nextSection() when RSVP exhausts words
- This moves to the next chapter instead of staying in the current one

* refactor: remove test files, unsure of use case for now

* chore: remove Playwright e2e test scripts from package.json

- Deleted e2e test scripts related to Playwright from package.json as they are no longer needed.
- Removed Playwright as a dev dependency to streamline the project.

* fix(rsvp): ensure CFI retrieval occurs before navigation

- Updated comments to clarify the necessity of obtaining CFI for both the navigation and sentence highlight before invoking the goTo() method, as it may re-render the document and invalidate Range objects.
- Introduced a new variable for sentence text to enhance readability and maintainability of the code.

* chore: sync pnpm-lock.yaml after removing @playwright/test

* style: format RSVP components

* fix: lint errors and timezone-aware date formatting

* i18n: support CJK text and add translations

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-02-01 18:22:24 +01:00
Huang Xin 9f7147f8f8 fix(layout): fix z index for dropdown (#3135) 2026-02-01 16:36:03 +01:00
Huang Xin 8a468a6d1f fix(opds): more robust parsing for authors in metadata, closes #3120 (#3134) 2026-02-01 15:57:10 +01:00
Huang Xin c848a319ff fix(layout): fix centered dropdown menu (#3133) 2026-02-01 15:52:51 +01:00
Huang Xin 5cc80db438 feat(gesture): support two finger swipe left/right to paginate on trackpad, closes #3127 (#3132) 2026-02-01 09:37:13 +01:00
Huang Xin 94930baa1b feat(sync): more intuitive pull down to sync gesture and toast info, closes #3128 (#3131) 2026-02-01 09:23:24 +01:00
Huang Xin 1294ef90c1 fix(macOS): fix traffic lights position and visibility on macOS, closes #3129 (#3130) 2026-02-01 05:38:40 +01:00
Huang Xin 570598520f fix(a11y): fix accessibility in dropdown menus for TalkBack, closes #3035 (#3126) 2026-01-31 19:05:56 +01:00
Huang Xin 7b4fc91994 fix(layout): fixed layout of book reading progress and reader footer bar (#3125) 2026-01-31 18:16:17 +01:00
Huang Xin fb387c2384 fix(layout): fixed layout of book reading progress and reader footer bar (#3117) 2026-01-30 06:54:20 +01:00
Mohammed Efaz 4ce1ebe477 feat: paragraph by paragraph reading mode (#3096)
* feat: add index

* feat: add bottom nav bar

* feat: add paragraph iterator

* feat: add para mode shortcut

* feat: add paragraph control into foliateviewer

* feat: add paragraph mode toggle to view menu

* feat: add paragraph bar for navigation controls

* feat: add paragraph control wrapper component

* feat: add pargraph overlay for the focused display

* feat: integrate paragraph mode into keyboard navigation

* feat: add paragraph mode state management hook

* feat: add paragraph mode type definition

* feat: add default paragraph mode config

* fix: replace previous storage system with sytem one and fix sync issues

* fix: format
2026-01-30 05:24:23 +01:00
Huang Xin c1460f4b85 fix(css): mix blend only inline images, closes #3112 (#3116) 2026-01-30 05:17:16 +01:00
Huang Xin bc94f3f790 refactor: responsive SetStatusAlert on smaller screens (#3110) 2026-01-29 15:53:20 +01:00
Huang Xin c8c761b017 fix(sync): escape unsafe character in filename (#3109) 2026-01-29 15:13:02 +01:00
Huang Xin bdb25999c9 fix(sync): more robust books sync for stale library, closes #3099 (#3108) 2026-01-29 14:05:58 +01:00
Huang Xin c58e172a54 fix(bookshelf): don't show badge or progress for unread book, closes #3103 (#3107) 2026-01-29 12:48:21 +01:00
Huang Xin 160efcd37b chore: bump next.js to the latest version (#3106) 2026-01-29 12:29:17 +01:00
Huang Xin d7470f4139 fix(iOS): disable online catalogs on iOS from appstore channel (#3102) 2026-01-29 05:52:59 +01:00
Mohammed Efaz 01b4e3530d feat: toggle finished manually (#3091) 2026-01-28 06:26:57 +01:00
Huang Xin 09c62d442f fix(css): no default mix blend mode for hr, closes #3086 (#3093) 2026-01-27 15:03:17 +01:00
Huang Xin d62ad60ce8 chore: bump opennextjs and wrangler to the latest versions (#3092) 2026-01-27 14:57:28 +01:00
Jair Goh 8cd34c8aaa feat(assistant): Add embedding model option to AIPanel (#3090)
* Add embedding model option to AIPanel

* Minor code reformatting

* Edit tests to account for embedding model

* Minor reformatting
2026-01-27 14:28:18 +01:00
Mohammed Efaz a52d9e3a2b feat: add fuzzy search for searching across all settings (#3085)
* feat: enable linking to settings items

* feat: integrate command palette with global state and styles

* feat: add command pallete ui and fuzzy search

* feat: add command palette with global state and styles

* feat: add command registry and search dependencies

* fix: open command palette with shortcuts in reader page

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-01-26 18:32:04 +01:00
Huang Xin 64a1ea531a fix(bookshelf): fixed conflicts of group names with common prefix (#3084) 2026-01-26 13:38:01 +01:00
Mohammed Efaz 19f9f5ea24 feat: auto position reader ruler to top of page and some fixes (#3075)
* fix(settings): ensure latest config is used when saving view settings

* fix: reader ruler appearing before other book reading contents

* feat(reader): dispatch reader-closing event during book close

* feat(reader) add auto reposition to top when changing pages and position persistence on book close

* fix(reader): add rtl prop to ReadingRuler component

* fix(reader): handle vertical ltr writing mode in ruler auto positioning

* chore: revert redundant changes to settings.ts

* refactor: remove redundant reader-closing

* refactor: use store progress for ruler positioning with throttled saving
2026-01-26 08:38:06 +01:00
Huang Xin ffc51e75de feat(ruler): support vertical ruler and transparent ruler (#3076) 2026-01-25 20:15:47 +01:00
Huang Xin 2100071991 feat(eink): support color E-ink mode to display more colors, closes #3037 (#3074) 2026-01-25 17:44:41 +01:00
Huang Xin 563d3478ba feat(gamepad): support gamepad input to paginate, closes #2432 (#3073) 2026-01-25 16:32:05 +01:00
Huang Xin 2c54e9ae2f feat(updater): in-app updater for AppImage (#3072) 2026-01-25 14:45:37 +01:00
Huang Xin c2eb2e2fcc doc: update donation link via Stripe (#3071) 2026-01-25 11:24:10 +01:00
Huang Xin e592f40429 layout: don't show upload icon if auto upload is disabled for cleaner UI (#3068) 2026-01-25 09:22:30 +01:00
Huang Xin 4bd7cfe57c chore: fix dockerfile (#3067) 2026-01-25 08:51:40 +01:00
Mohammed Efaz c31ece6742 feat: more highlight colours (#3062)
* feat(highlight): extend types and constants for custom hex colours

* feat(highlight): update colour to support hex strings

* refactor(annotator): update component to accept hex colours

* feat(highlight): add colour picker with max 4 custom colors

* feat(settings): add custom colour editor with limit

* refactor: custom highlight colors can only be modified in settings

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-01-25 07:52:29 +01:00
Mohammed Efaz aba9e87fc1 feat: line highlight to guide reading (reading ruler) (#3063)
* feat: add increment decrement numbers for opacity

* feat: add reading ruler colours

* feat: add reading ruler config to book config

* feat: reading Ruler settings panel (in layout section)

* feat: draggable reading ruler

* feat: reading ruler in reader view
2026-01-25 06:48:13 +01:00
Huang Xin 2c4df601d8 refactor: rename components/ui to components/primitives (#3064)
Move Radix/shadcn-based UI primitives into a dedicated
`components/primitives` directory to better reflect
their low-level, composable nature.
Imports remain ergonomic via an alias to `components/ui`.
App-level components should continue to use PascalCase filenames.
2026-01-25 06:41:18 +01:00
Huang Xin 8bfc90a5b0 chore: fix nunjucks bundling (#3061) 2026-01-24 18:20:15 +01:00
Mohammed Efaz 8bea05478a feat(ai): AI reading assistant phase 2.1 (minor UI/UX updates) (#3059)
* refactor(ai): remove chat icons from conversation list items

* feat(ai): add scroll-to-bottom button with animations and dynamic spacer

* fix(ai): align connection status icon with text in settings

* feat(ai): scroll to bottom on chat panel open and improve scroll behavior

* feat(ai): add loading overlay with blur transition for chat history

* feat(ai): pass loading state through component chain for history loading
2026-01-24 17:57:00 +01:00
Huang Xin 42fee90a27 feat(annotation): export annotations with style and color fields in annotation template, closes #1734 (#3060) 2026-01-24 17:44:40 +01:00
Huang Xin 45e57c3943 chore: update wrangler and next config (#3058) 2026-01-24 14:36:04 +01:00
Huang Xin 51b0b8483f chore: less build concurrency on cloudflare to get rid of build container OOM (#3056) 2026-01-24 13:27:11 +01:00
Huang Xin cbf7a501b7 chore: pin pnpm version for vercel and cloudflare deployment (#3055) 2026-01-24 12:35:03 +01:00
Huang Xin edfcb75ba5 chore: refresh pnpm lockfile (#3054) 2026-01-24 12:08:52 +01:00
Huang Xin ce76843ccc chore: fix patched dependencies (#3053) 2026-01-24 11:52:28 +01:00
Mohammed Efaz 5bbc5ceccc feat(ai): AI reading assistant phase 2 (#3023)
* feat(ai): add dependencies

* chore: bump zod version to default version

* feat(ai): define types and model constants

* feat(ai): ollama provider for local LLM

* feat(ai): implement openrouter provider for cloud models

* feat(settings): register ai settings panel in global dialog

* refactor(ai): expose provider factory and service layer entry point

* test(ai): add unit tests for the providers

* test(ai): add unit tests for the providers

* feat(ai): settings panel for ai configurations

* refactor(ai): rewrite aipanel with autosave and greyed out disabled state

* fix: remove unused onClose prop from aipanel

* test(ai): update mock data

* refactor(ai): remove models

* refactor: use centralised defaults in system defaults

* chore(ai): remove comments

* fix(ai): merge default ai settings on load to prevent undefined values

* refactor(ai): rewrite settings panel with autosave and model input

* feat(ai): add ai tab with simplified highlighting

* feat(sidebar): render AIAssistant for ai tab

* feat(ai): add chat UI

* feat(ai); add chat service with RAG context

* feat(ai): temp debug logger

* feat(ai): add RAG service

* feat(ai): add text chunking utility

* feat(ai): add structured method

* feat(ai): add chatstructured method

* feat(ai): add rag types nd structured output schema

* feat(ai): add aistore, indexdb, bm25

* fix: update lock file

* feat(ai): update types for AI SDK v5

* feat(ai): add placeholder gateway model constants

* refactor(ai): update OllamaProvider for AI SDK

* feat(ai): add native gateway provider

* refactor(ai): update provider exports

* refactor(ai): use streamText from AI SDK

* refactor(ai): use embed from AI sdk

* refactor(ai): update provider factory exports

* feat(ai): add AI Elements and shadcn components

* config: add shadcn component config

* deps: add AI SDK and AI Elements dependencies

* config: add ai packages to transpilePackages

* refactor(ai): remove OpenRouterProvider and old tests

* feat(ai):add assistant-ui components

* feat(ai): add TauriChatAdapter for assistant-ui runtime

* refactor(ai): remove ai-elements components

* dep(ai): install assistant-ui and update next config

* chore(ai): export adapters from service index

* feat(ui): enhance ui components for assistant integration

* feat(settings): migrate ai settings to gateway

* feat(sidebar): integrate assistant-ui

* feat: add ai settings toggle to sidebar content

* feat: conditionally show ai tab in sidebar navigation

* feat: update ai model constants for cheaper options

* feat: add gateway provider with proxied embedding

* feat: add timeouts to ollama provider health checks

* feat: add retry logic to rag service embeddings

* feat: add error recovery to ai store

* feat: add ai feature tests

* feat: add ai api endpoints

* feat: add proxied gateway embedding provider

* feat: add ai runtime utilities

* feat: add ai retry utilities

* feat: add tauri env example template

* feat: add web env example template

* chore: add env

* feat(ai): update models and pricing, remove GLM-4.7-FlashX

* feat(ai): improve system prompt with official headings and no numeric citations

* feat(ai): optimize system prompt for tauri chat

* feat(ui): refine ai chat UI and relocate sources

* feat(ui): update ai settings panel with model pricing and custom model support

* feat(ai): add custom model support to ai settings

* test(ai): update constants tests for removed model

* feat(api): implement ai chat proxy route

* feat(api): implement ai embedding proxy route

* feat(ai): implement ai gateway health check and proxy logic

* feat(ai): simplify proxied embedding provider

* feat(ui): improve markdown text rendering

* feat(ui): add input group component

* test(ai): update ai provider tests

* feat(ai): add pageNumber to text chunk schema

* feat(ai): implement page-based chunking with 1500 char formula

* feat(ai): bump db to v2 and add store reset migration

* feat(ai): transition rag pipeline to page level spoiler filtering

* feat(ai): overhaul readest persona and antijailbreak prompt

* feat(ai): update tauri adapter for page tracking and persona

* chore(ai): export aiStore and logger from core index

* feat(reader): integrate page tracking and manual index reset

* feat(ui): add re-index action and reset logic to chat

* chore: sync pnpm lockfile with ai dependencies

* feat(utils): add browser-safe file utilities for web builds

* refactor(utils): use dynamic tauri fs import to prevent web crashes

* refactor(services): defer osType call to init() for web compatibility

* refactor(services): import RemoteFile from file.web

* refactor(services): import ClosableFile from file.web

* fix(libs): cast Entry to any for getData access

* fix(annotator): cast Overlayer to any for bubble access

* refactor(ai): replace SparklesIcon with BookOpenIcon for index prompt

* test(ai): add pageNumber to TextChunk mocks

* test(ai): fix chunkSection signature in tests

* chore: update files

* fix(ai): prevent useLocalRuntime crash when adapter is null

* refactor: optimize annotator overlay drawing

* feat: stabilize AI assistant runtime and adapter

* refactor: improve document zip loader type safety

* feat: update tauri chat adapter for dynamic options

* fix: restore architecture comments and refine platform properties

* build: update lockfile with assistant-ui patch

* fix(library): patch @assistant-ui/react for runtime initialization

* build: update dependencies in readest-app

* build: update root dependencies and patch configuration

* fix(ai): patch @assistant-ui/react for thread deletion and runtime init

* fix(ai): update assistant-ui patch with dist guards and deletion fallback

* build: sync lockfile with assistant-ui patch updates

* chore(env): update .gitignore by removing .env files from it

* chore(env): update .gitignore by adding .env.local

* chore(env): update .gitignore by adding .env*.local

* fix: restore static osType import

* chore: sync submodules with upstream/main

* refactor: remove redundant file.web module and revert import

* chore: update pnpm-lock.yaml

* refactor: revert guards

* refactor; remove deprecated codes and extract prompts.ts

* refactor(ai): remove unused ragservice exports

* refactor: remove unused ollama and embedding models

* refactor: remove unused type

* test: remove test for the now deleted constants

* refactor: remove unused export

* style: fix ui component formatting

* style: fix core and style file formatting

* test: fix broken ai provider import

* fix: typescript error

* fix: add eslint disable command

* fix(deps): remove unused ai sdk provider util after v6 ai sdk migration

* fix(patch): add lookbehind regex patch

* feat(dep): upgrade vercel ai sdk to v6 and ai-sdk-ollama to v3

* chore: update lockfile for vercel ai sdk v6

* refactor(ai): remove EmbeddingModel generic for ai sdk v6

* refactor(ai): remove EmbeddingModel generic for ai sdk v6

* test(ai): update mock to use embeddingModel

* fix(patch): add lookbehind regex patch for email autolinks in markdown

* refactor(ai): use ai sdk v6 syntax

* fix: prettier formatting

* chore: revert cargo.lock

* fix(ai): update proxied embedding model to v3 spec

* feat(ai): add aiconversation types for chat persistence

* feat(ai): add conversation/message indexeddb and crud operations

* feat(ai): create aiChatStore zustand store for chat state management

* feat(notebook): add notebookactivetab state for Notes/AI

* refactor(ai): refine conversation and message types for persistence

* feat(types): add notebookActiveTab to ReadSettings type

* chore: update deps

* feat: add notebookactive tab default value

* feat: add hook for ai chat

* feat: update left side panel with history/chat icon

* feat: integrate ChatHistoryView into sidebar content

* feat: create UI for managing AI chat history

* feat: implement persistent history with assistant-ui adapter

* feat: create tab navigation component for notes and AI

* feat: add tab navigation and AI assistant view

* feat: update header to display active tab title

* fix: formatting

* feat: remove title and update new chat button

* fix: formatting

* fix: revert tooltip and styling

* feat: implement cross-platform ask dialog bridge

* feat(ai): preserve history during ui clear & use native dialogs

* fix: align notebook navigation height with sidebar tabs

* fix(ai): add missing dependency to handleDeleteConversation hook

* docs: update PROJECT.md with session highlights

* chore: delete projectmd

* chore: update package.json and lock file

* chore: update package.json

* chore: remove patch

* chore: upgrade react types to 19 and show ai features only in development mode for now

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-01-24 11:38:48 +01:00
Huang Xin 224acd68b1 fix(sanitizer): add XHTML11 doc type to recognize nbsp entity, closes #3024 (#3043) 2026-01-23 17:49:06 +01:00
Huang Xin 6c0f1b8b5f fix(annotator): fix instant action on Android (#3042) 2026-01-23 17:18:51 +01:00
Huang Xin 481d8198e9 fix(tts): set playback rate after play only on Linux (#3040) 2026-01-23 12:14:13 +01:00
Huang Xin b1153ba051 fix(sync): correctly update last sync timestamp when the bookshelf has no books yet (#3039) 2026-01-23 04:39:22 +01:00
Huang Xin aad532bfdd fix(sync): hotfix for the initial race condition for books sync (#3038) 2026-01-23 04:32:03 +01:00
Huang Xin a71347c897 fix(layout): more responsive layout on smaller screens, closes #3029 (#3034) 2026-01-22 17:32:34 +01:00
Huang Xin 034f41ca10 fix(shortcuts): prevent system search bar from showing with ctrl+f, closes #3013 (#3033) 2026-01-22 15:59:21 +01:00
Huang Xin 539ad8dea2 fix(layout): correctly constrain the maximum width of fixed-layout tables, closes #3028 and closes #3017 (#3032) 2026-01-22 14:56:35 +01:00
Huang Xin 48920a87bf chore(opds): disable popular online opds catalogs in certain regions on App Store (#3031) 2026-01-22 11:42:16 +01:00
xijibomi-coffee d1d0d2d59c chore: enforce prettier, ignore submodules and vendor files (#3018)
* chore: enforce prettier, ignore submodules and vendor files

* chore: add format check in CI

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-01-21 14:53:02 +01:00
xijibomi-coffee ea811c90c6 feat(ui): switch typography to Inter (#3009) 2026-01-21 13:49:37 +01:00
Jon Volkmar 5cd5b65e83 fix: Conditionally set Cache-Control header to prevent caching in development environments. (#3010) 2026-01-21 03:18:08 +01:00
Huang Xin ea24b5a97a fix(account): redirect to auth page if unauth user is in user page (#3008) 2026-01-20 21:55:53 +01:00
Huang Xin 06d620db83 chore: fixed signing of the portable version for Windows (#3007) 2026-01-21 03:23:39 +08:00
Huang Xin 0c25b85a8a release: version 0.9.98 (#3006) 2026-01-20 18:54:06 +01:00
Huang Xin c536450ab0 fix(fonts): host fonts in Readest CDN for more stable web fonts distribution (#3005) 2026-01-20 18:44:22 +01:00
Huang Xin c83e380c5a chore(doc): use png resources for sponsors logo (#3003) 2026-01-20 07:47:28 +01:00
Huang Xin 894a7551aa chore(doc): update sponsors info (#3002) 2026-01-20 07:42:06 +01:00
xijibomi-coffee f875ba88ac perf: use native walkdir for recursive imports from directory (#2993)
* fix(perf): replace JS recursion with native Rust walkdir for imports

* fix: implement security scope check in rust recursive scanner

* refactor and format code

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-01-20 06:22:04 +01:00
Huang Xin d9a6cffe78 feat(discord): support show reading status with Discord Rich Presence, closes #1538 (#2998) 2026-01-19 17:42:19 +01:00
Huang Xin 1704736bc8 feat(account): support social login with Discord account (#2995) 2026-01-19 05:22:17 +01:00
Huang Xin 6ce9a263ee fix(a11y): add missing aria labels for some buttons on TTS panel (#2994) 2026-01-19 04:29:02 +01:00
Huang Xin 038eca4267 fix(eink): resolved an issue where the dropdown menu would occasionally fail to expand on some Eink devices (#2991) 2026-01-18 19:57:09 +01:00
Huang Xin f0722ec0fe feat(bookshelf): show one line of book description in list view of the bookshelf, closes #2955 (#2990) 2026-01-18 17:16:54 +01:00
Huang Xin fd299a61a7 feat(notes): export notes with custom template, closes #1734 (#2989) 2026-01-18 16:56:32 +01:00
Huang Xin 36b7386e30 fix(portable): in-app update the portable version app, closes #2983 (#2988) 2026-01-18 11:20:15 +01:00
Huang Xin 2670b0dc0b fix(linux): use new AppImage format on Linux, closes #190 (#2985)
See https://github.com/tauri-apps/tauri/pull/12491
2026-01-18 09:50:51 +01:00
Huang Xin c44705e269 perf(pdf): pre-render next page for PDFs (#2984)
This should close #1911 and #13.
2026-01-18 08:58:00 +01:00
Huang Xin 7e618d456e chore: bump pdf.js to the latest version (#2982) 2026-01-17 13:45:34 +01:00
Huang Xin b28ac99a9e feat(pdf): support panning on PDFs (#2981)
This also closes #2978 and closes #2875.
2026-01-17 09:11:33 +01:00
Huang Xin 2d7d6b08a9 compat(opds): parse attachment filename from download requests, closes #2969 (#2976) 2026-01-16 17:13:30 +01:00
Huang Xin b1419f9c01 chore: bump tauri to latest dev (#2975) 2026-01-16 14:52:22 +01:00
Huang Xin 32ea42a835 fix: remove non-breaking space in book title (#2974) 2026-01-16 12:02:41 +01:00
Huang Xin 6083de3293 fix(import): read permissions of nested directories when importing books, closes #2954 (#2972) 2026-01-16 08:04:07 +01:00
Huang Xin 1c9cfa49b3 fix(flathub): use custom dbus id for single instance on Linux (#2971) 2026-01-16 05:59:25 +01:00
Huang Xin f85d6d4293 fix(eink): avoid scrolling if animation is turned off or in eink mode, closes #2957 (#2967) 2026-01-15 17:37:38 +01:00
Huang Xin 017d0b0b39 fix(tts): fix setting playback rate on Linux, closes #2950 (#2966) 2026-01-15 17:29:26 +01:00
Huang Xin 3146ae48a7 fix(proofread): support replace text with space (#2965) 2026-01-15 14:10:05 +01:00
Huang Xin 7f26e45ae7 feat(sync): cloud deletion in transfer queue (#2964) 2026-01-15 13:50:24 +01:00
Huang Xin 7d97826e4b feat(tts): replace words for TTS, closes #2057 (#2952) 2026-01-14 16:42:11 +01:00
Huang Xin 9c9c79176d feat(bookshelf): add an option to sort books by publish date, closes #2925 (#2949) 2026-01-14 13:40:07 +01:00
Huang Xin ed476a4fce fix(ios): fix wakelock on iOS, closes #2453 (#2948) 2026-01-14 13:06:16 +01:00
Huang Xin 44b4f7995b fix(android): fix annotator on Android, closes #2927 (#2946) 2026-01-14 11:56:38 +01:00
Huang Xin da5e3a0bd3 perf(fonts): cache first for font cache (#2945) 2026-01-14 07:34:00 +01:00
Huang Xin ba4678cc23 fix(fonts): fix CORS access error 403 of deno.dev with Origin: tauri://localhost (#2944) 2026-01-14 05:35:27 +01:00
Huang Xin e5eb3014b4 chore(unittest): test makeSafeFilename (#2943) 2026-01-14 04:08:30 +01:00
Huang Xin ed77b0bc7f fix(android): only dismiss unpinned sidebar and notebook with back key, closes #2920 (#2939) 2026-01-13 15:42:03 +01:00
Huang Xin 434a44e62c feat(proofread): add an option for whole word replacement, closes #2934 (#2938) 2026-01-13 15:35:57 +01:00
Huang Xin aaee04c290 fix(opds): probe filename when downloading PDFs and CBZ files, closes #2921 (#2932) 2026-01-12 17:55:50 +01:00
Huang Xin 48e2bfa82c fix(android): avoid occasional crash on start on Android (#2931) 2026-01-12 16:27:25 +01:00
Huang Xin c04f19ffb4 feat: add support for exporting book files in book details dialog, closes #2919 (#2930) 2026-01-12 16:26:32 +01:00
Huang Xin 4a624e3902 fix(settings): avoid stale viewSettings after saving, closes #2912 (#2916) 2026-01-11 16:47:36 +01:00
319 changed files with 33633 additions and 7092 deletions
+33
View File
@@ -0,0 +1,33 @@
# Dependencies
node_modules
**/node_modules
# Rust build artifacts
target
**/target
# Git
.git
.gitignore
# Build outputs
dist
**/dist
.next
**/.next
out
**/out
# IDE
.idea
.vscode
*.swp
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
+1 -1
View File
@@ -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
+4 -2
View File
@@ -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: |
+47 -2
View File
@@ -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
@@ -286,10 +284,15 @@ jobs:
echo "Uploading updated latest.json to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber
- name: Override tauri-cli with custom AppImage format (Linux)
if: matrix.config.release == 'linux'
run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
- uses: tauri-apps/tauri-action@v0
if: matrix.config.release != 'android'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: 'true'
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -316,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
@@ -348,6 +353,46 @@ jobs:
echo "Uploading $bin_file to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file --clobber
echo "Signing portable binary"
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
- name: download and update latest.json for Windows portable release
if: matrix.config.os == 'windows-latest'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
curl -sL https://github.com/readest/readest/releases/latest/download/latest.json -o latest.json
version=${{ needs.get-release.outputs.release_version }}
arch=${{ matrix.config.arch }}
if [ "$arch" = "x86_64" ]; then
bin_file="Readest_${version}_x64-portable.exe"
platform_key="windows-x86_64-portable"
elif [ "$arch" = "aarch64" ]; then
bin_file="Readest_${version}_arm64-portable.exe"
platform_key="windows-aarch64-portable"
else
echo "Unknown architecture: $arch"
exit 1
fi
portable_url="https://github.com/readest/readest/releases/download/${{ needs.get-release.outputs.release_tag }}/$bin_file"
portable_sig=$(cat $bin_file.sig)
jq --arg url "$portable_url" \
--arg sig "$portable_sig" \
--arg key "$platform_key" \
'.platforms[$key] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json
echo "Uploading updated latest.json to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber
upload-to-r2:
needs: [get-release, build-tauri]
uses: ./.github/workflows/upload-to-r2.yml
+1
View File
@@ -45,3 +45,4 @@ fastlane/report.xml
# nix
result*
.playwright-mcp/
+34 -1
View File
@@ -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
+16 -1
View File
@@ -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
View File
@@ -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
Generated
+576 -557
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -4,6 +4,8 @@ members = [
"packages/tauri/crates/tauri",
"packages/tauri/crates/tauri-utils",
"packages/tauri/crates/tauri-build",
"packages/tauri-plugins/plugins/deep-link",
"packages/tauri-plugins/plugins/single-instance"
]
resolver = "2"
@@ -19,6 +21,7 @@ schemars = "0.8"
serde_json = "1"
thiserror = "2"
glob = "0.3"
zbus = "5.9"
dunce = "1"
url = "2"
@@ -35,3 +38,5 @@ rust-version = "1.77.2"
tauri = { path = "packages/tauri/crates/tauri" }
tauri-utils = { path = "packages/tauri/crates/tauri-utils" }
tauri-build = { path = "packages/tauri/crates/tauri-build" }
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
tauri-plugin-single-instance = { path = "packages/tauri-plugins/plugins/single-instance" }
+1 -19
View File
@@ -5,31 +5,13 @@ 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
WORKDIR /app
RUN pnpm install
RUN pnpm --filter @readest/readest-app setup-pdfjs
RUN pnpm --filter @readest/readest-app setup-vendors
WORKDIR /app/apps/readest-app
+3 -3
View File
@@ -262,13 +262,13 @@ 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 [contribute 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 LambdaTest" href="https://www.lambdatest.com/?utm_source=readest&utm_medium=sponsor" target="_blank">
<img src="https://www.lambdatest.com/blue-logo.png" style="vertical-align: middle;" width="250" />
<a title="Browser testing via TestMu AI" href="https://www.testmu.ai?utm_source=readest&utm_medium=sponsor" 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>
+2
View File
@@ -28,3 +28,5 @@ S3_ACCESS_KEY_ID=PLACE_HOLDER
S3_SECRET_ACCESS_KEY=PLACE_HOLDER
S3_BUCKET_NAME=PLACE_HOLDER
S3_REGION=PLACE_HOLDER
TEMP_STORAGE_PUBLIC_BUCKET_NAME=PLACE_HOLDER
+3 -1
View File
@@ -1 +1,3 @@
NEXT_PUBLIC_APP_PLATFORM=tauri
NEXT_PUBLIC_APP_PLATFORM=tauri
DBUS_ID=com.bilingify.readest
+2
View File
@@ -0,0 +1,2 @@
NEXT_PUBLIC_APP_PLATFORM=tauri
AI_GATEWAY_API_KEY=your_key_here
+3
View File
@@ -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
+22
View File
@@ -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
```
+35 -13
View File
@@ -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',
},
],
},
+52 -18
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.97",
"version": "0.9.99",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -14,13 +14,17 @@
"lint": "eslint .",
"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.mjs,pdf.d.mts}\" ./public/vendor/pdfjs",
"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",
"copy-pdfjs-fonts": "cpx \"../../packages/foliate-js/node_modules/pdfjs-dist/{cmaps,standard_fonts}/*\" ./public/vendor/pdfjs",
"copy-flatten-pdfjs-annotation-layer-css": "npx postcss \"../../packages/foliate-js/vendor/pdfjs/annotation_layer_builder.css\" --no-map -u postcss-nested > ./public/vendor/pdfjs/annotation_layer_builder.css",
"copy-flatten-pdfjs-text-layer-css": "npx postcss \"../../packages/foliate-js/vendor/pdfjs/text_layer_builder.css\" --no-map -u postcss-nested > ./public/vendor/pdfjs/text_layer_builder.css",
"copy-flatten-pdfjs-css": "pnpm copy-flatten-pdfjs-annotation-layer-css && pnpm copy-flatten-pdfjs-text-layer-css",
"copy-pdfjs": "pnpm copy-pdfjs-js && pnpm copy-pdfjs-fonts && pnpm copy-flatten-pdfjs-css",
"copy-pdfjs": "pnpm copy-pdfjs-js && pnpm copy-pdfjs-wasm && pnpm copy-pdfjs-fonts && pnpm copy-flatten-pdfjs-css",
"copy-simplecc": "cpx \"../../packages/simplecc-wasm/dist/web/*\" ./public/vendor/simplecc",
"setup-pdfjs": "pnpm prepare-public-vendor && pnpm copy-pdfjs",
"setup-simplecc": "pnpm prepare-public-vendor && pnpm copy-simplecc",
@@ -51,10 +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",
@@ -63,28 +82,34 @@
"@supabase/supabase-js": "^2.76.1",
"@tauri-apps/api": "2.9.1",
"@tauri-apps/plugin-cli": "^2.4.1",
"@tauri-apps/plugin-deep-link": "^2.4.5",
"@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-deep-link": "^2.4.6",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-haptics": "^2.3.2",
"@tauri-apps/plugin-http": "^2.5.4",
"@tauri-apps/plugin-log": "^2.7.1",
"@tauri-apps/plugin-opener": "^2.5.2",
"@tauri-apps/plugin-http": "^2.5.6",
"@tauri-apps/plugin-log": "^2.8.0",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "~2.3.3",
"@tauri-apps/plugin-shell": "~2.3.4",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-websocket": "~2.4.1",
"@zip.js/zip.js": "^2.7.53",
"@tauri-apps/plugin-websocket": "~2.4.2",
"@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",
@@ -96,8 +121,12 @@
"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",
"nunjucks": "^3.2.4",
"overlayscrollbars": "^2.11.4",
"overlayscrollbars-react": "^0.5.6",
"posthog-js": "^1.246.0",
@@ -109,14 +138,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",
@@ -126,10 +158,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",
@@ -159,6 +193,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.

After

Width:  |  Height:  |  Size: 52 KiB

@@ -172,9 +172,6 @@
"Token": "الرمز",
"Your OTP token": "رمز OTP الذي وصلك",
"Verify token": "التحقق من الرمز",
"Sign in with Google": "تسجيل الدخول عبر Google",
"Sign in with Apple": "تسجيل الدخول عبر Apple",
"Sign in with GitHub": "تسجيل الدخول عبر GitHub",
"Account": "الحساب",
"Failed to delete user. Please try again later.": "فشل حذف المستخدم. يرجى المحاولة مرة أخرى لاحقًا.",
"Community Support": "دعم المجتمع",
@@ -356,8 +353,8 @@
"{{count}} pages left in chapter_many": "تبقّت {{count}} صفحة في هذا الفصل",
"{{count}} pages left in chapter_other": "تبقّى {{count}} صفحة في هذا الفصل",
"Show Remaining Pages": "عرض الصفحات المتبقية",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "إدارة الاشتراك",
"Coming Soon": "قريبًا",
@@ -875,6 +872,212 @@
"Show Results": "عرض النتائج",
"Clear search": "مسح البحث",
"Clear search history": "مسح سجل البحث",
"Quick action disabled": "الإجراء السريع معطل",
"Tap to Toggle Footer": "انقر لتبديل التذييل"
"Tap to Toggle Footer": "انقر لتبديل التذييل",
"Exported successfully": "تم التصدير بنجاح",
"Book exported successfully.": "تم تصدير الكتاب بنجاح.",
"Failed to export the book.": "فشل تصدير الكتاب.",
"Export Book": "تصدير الكتاب",
"Whole word:": "كلمة كاملة:",
"Error": "خطأ",
"Unable to load the article. Try searching directly on {{link}}.": "تعذر تحميل المقال. حاول البحث مباشرة على {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "تعذر تحميل الكلمة. حاول البحث مباشرة على {{link}}.",
"Date Published": "تاريخ النشر",
"Only for TTS:": "فقط لـ TTS:",
"Uploaded": "تم الرفع",
"Downloaded": "تم التنزيل",
"Deleted": "تم الحذف",
"Note:": "ملاحظة:",
"Time:": "الوقت:",
"Format Options": "خيارات التنسيق",
"Export Date": "تاريخ التصدير",
"Chapter Titles": "عناوين الفصول",
"Chapter Separator": "فاصل الفصول",
"Highlights": "التمييزات",
"Note Date": "تاريخ الملاحظة",
"Advanced": "متقدم",
"Hide": "إخفاء",
"Show": "إظهار",
"Use Custom Template": "استخدام قالب مخصص",
"Export Template": "قالب التصدير",
"Template Syntax:": "بناء جملة القالب:",
"Insert value": "إدراج قيمة",
"Format date (locale)": "تنسيق التاريخ (محلي)",
"Format date (custom)": "تنسيق التاريخ (مخصص)",
"Conditional": "شرطي",
"Loop": "حلقة",
"Available Variables:": "المتغيرات المتاحة:",
"Book title": "عنوان الكتاب",
"Book author": "مؤلف الكتاب",
"Export date": "تاريخ التصدير",
"Array of chapters": "مصفوفة الفصول",
"Chapter title": "عنوان الفصل",
"Array of annotations": "مصفوفة التعليقات",
"Highlighted text": "النص المميز",
"Annotation note": "ملاحظة التعليق",
"Date Format Tokens:": "رموز تنسيق التاريخ:",
"Year (4 digits)": "السنة (4 أرقام)",
"Month (01-12)": "الشهر (01-12)",
"Day (01-31)": "اليوم (01-31)",
"Hour (00-23)": "الساعة (00-23)",
"Minute (00-59)": "الدقيقة (00-59)",
"Second (00-59)": "الثانية (00-59)",
"Show Source": "إظهار المصدر",
"No content to preview": "لا يوجد محتوى للمعاينة",
"Export": "تصدير",
"Set Timeout": "تعيين المهلة",
"Select Voice": "اختر الصوت",
"Toggle Sticky Bottom TTS Bar": "تبديل شريط TTS الثابت",
"Display what I'm reading on Discord": "عرض ما أقرأه على Discord",
"Show on Discord": "عرض على Discord",
"Instant {{action}}": "{{action}} فوري",
"Instant {{action}} Disabled": "تم تعطيل {{action}} فوري",
"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": "اختر الفصل",
"{{number}} WPM": "{{number}} كلمة في الدقيقة",
"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": "العودة إلى موقع القراءة الآلية"
}
@@ -24,9 +24,6 @@
"Token": "টোকেন",
"Your OTP token": "আপনার ওটিপি টোকেন",
"Verify token": "টোকেন যাচাই করুন",
"Sign in with Google": "গুগল দিয়ে সাইন ইন",
"Sign in with Apple": "অ্যাপল দিয়ে সাইন ইন",
"Sign in with GitHub": "গিটহাব দিয়ে সাইন ইন",
"New Password": "নতুন পাসওয়ার্ড",
"Your new password": "আপনার নতুন পাসওয়ার্ড",
"Update password": "পাসওয়ার্ড আপডেট করুন",
@@ -488,8 +485,8 @@
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"Source Han Serif CN": "Source Han Serif CN",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Google Books": "গুগল বুকস",
"Open Library": "ওপেন লাইব্রেরি",
@@ -835,6 +832,204 @@
"Show Results": "ফলাফল দেখুন",
"Clear search": "অনুসন্ধান সাফ করুন",
"Clear search history": "অনুসন্ধান ইতিহাস সাফ করুন",
"Quick action disabled": "দ্রুত কার্যক্রম অক্ষম",
"Tap to Toggle Footer": "ফুটার টগল করতে আলতো চাপুন"
"Tap to Toggle Footer": "ফুটার টগল করতে আলতো চাপুন",
"Exported successfully": "সফলভাবে রপ্তানি হয়েছে",
"Book exported successfully.": "বইটি সফলভাবে রপ্তানি হয়েছে।",
"Failed to export the book.": "বই রপ্তানি করতে ব্যর্থ।",
"Export Book": "বই রপ্তানি করুন",
"Whole word:": "সম্পূর্ণ শব্দ:",
"Error": "ত্রুটি",
"Unable to load the article. Try searching directly on {{link}}.": "নিবন্ধ লোড করতে অক্ষম। সরাসরি {{link}} এ অনুসন্ধান করার চেষ্টা করুন।",
"Unable to load the word. Try searching directly on {{link}}.": "শব্দ লোড করতে অক্ষম। সরাসরি {{link}} এ অনুসন্ধান করার চেষ্টা করুন।",
"Date Published": "প্রকাশনার তারিখ",
"Only for TTS:": "শুধুমাত্র TTS-এর জন্য:",
"Uploaded": "আপলোড হয়েছে",
"Downloaded": "ডাউনলোড হয়েছে",
"Deleted": "মুছে ফেলা হয়েছে",
"Note:": "নোট:",
"Time:": "সময়:",
"Format Options": "বিন্যাস বিকল্প",
"Export Date": "রপ্তানি তারিখ",
"Chapter Titles": "অধ্যায়ের শিরোনাম",
"Chapter Separator": "অধ্যায় বিভাজক",
"Highlights": "হাইলাইট",
"Note Date": "নোট তারিখ",
"Advanced": "উন্নত",
"Hide": "লুকান",
"Show": "দেখান",
"Use Custom Template": "কাস্টম টেমপ্লেট ব্যবহার করুন",
"Export Template": "রপ্তানি টেমপ্লেট",
"Template Syntax:": "টেমপ্লেট সিনট্যাক্স:",
"Insert value": "মান সন্নিবেশ করুন",
"Format date (locale)": "তারিখ বিন্যাস (স্থানীয়)",
"Format date (custom)": "তারিখ বিন্যাস (কাস্টম)",
"Conditional": "শর্তসাপেক্ষ",
"Loop": "লুপ",
"Available Variables:": "উপলব্ধ ভেরিয়েবল:",
"Book title": "বইয়ের শিরোনাম",
"Book author": "বইয়ের লেখক",
"Export date": "রপ্তানি তারিখ",
"Array of chapters": "অধ্যায়ের তালিকা",
"Chapter title": "অধ্যায়ের শিরোনাম",
"Array of annotations": "টীকাগুলির তালিকা",
"Highlighted text": "হাইলাইট করা পাঠ্য",
"Annotation note": "টীকা নোট",
"Date Format Tokens:": "তারিখ বিন্যাস টোকেন:",
"Year (4 digits)": "বছর (4 সংখ্যা)",
"Month (01-12)": "মাস (01-12)",
"Day (01-31)": "দিন (01-31)",
"Hour (00-23)": "ঘণ্টা (00-23)",
"Minute (00-59)": "মিনিট (00-59)",
"Second (00-59)": "সেকেন্ড (00-59)",
"Show Source": "উৎস দেখান",
"No content to preview": "প্রিভিউ করার জন্য কোনো কন্টেন্ট নেই",
"Export": "রপ্তানি",
"Set Timeout": "টাইমআউট সেট করুন",
"Select Voice": "ভয়েস নির্বাচন করুন",
"Toggle Sticky Bottom TTS Bar": "স্থির TTS বার টগল করুন",
"Display what I'm reading on Discord": "Discord-এ পড়ছি যা দেখান",
"Show on Discord": "Discord-এ দেখান",
"Instant {{action}}": "তাৎক্ষণিক {{action}}",
"Instant {{action}} Disabled": "তাৎক্ষণিক {{action}} নিষ্ক্রিয়",
"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": "অধ্যায় নির্বাচন করুন",
"{{number}} WPM": "{{number}} WPM",
"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": "টিটিএস অবস্থানে ফিরে যান"
}
@@ -173,9 +173,6 @@
"Token": "བཀོལ་ཐོགས།",
"Your OTP token": "ཁྱེད་ཀྱི་ OTP བཀོལ་ཐོགས།",
"Verify token": "བཀོལ་ཐོགས་བདེན་སྦྱོང་།",
"Sign in with Google": "Google བེད་སྤྱད་ནས་ནང་བསྐྱོད།",
"Sign in with Apple": "Apple བེད་སྤྱད་ནས་ནང་བསྐྱོད།",
"Sign in with GitHub": "GitHub བེད་སྤྱད་ནས་ནང་བསྐྱོད།",
"Account": "རྩིས་ཐོ།",
"Failed to delete user. Please try again later.": "སྤྱོད་མཁན་བསུབ་མ་ཐུབ། ཅུང་ཙམ་སྒུག་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས།",
"Community Support": "སྡེ་ཁུལ་གྱི་རྒྱབ་སྐྱོར།",
@@ -336,8 +333,8 @@
"Reset Settings": "སྒྲིག་བཀོད་བསྐྱར་སྒྲིག",
"{{count}} pages left in chapter_other": "ལེའུ་འདིར་ད་དུང་ {{count}} ཤོག་ལྷེ་ལྷག་ཡོད།",
"Show Remaining Pages": "ལྷག་མའི་ཤོག་ལྷེའི་གྲངས་ཀ་མངོན་པ།",
"Source Han Serif CN VF": "སི་ཡོན་སུང་ཐི།",
"Huiwen-mincho": "ཧུའེ་ཝུན་མིང་ཁྲའོ་ཐི།",
"Source Han Serif CN": "སི་ཡོན་སུང་ཐི།",
"Huiwen-MinchoGBK": "ཧུའེ་ཝུན་མིང་ཁྲའོ་ཐི།",
"KingHwa_OldSong": "ཅིང་ཧྭ་ལའོ་སུང་ཐི།",
"Manage Subscription": "མཚམས་སྦྱོར་དོ་དམ།",
"Coming Soon": "ཉེ་བར་སྤྱོད་འགོ་འཛུགས་རྒྱུ།",
@@ -825,6 +822,202 @@
"Show Results": "འབྲས་བུ་སྟོན།",
"Clear search": "འཚོལ་བཤེར་གཙང་སེལ།",
"Clear search history": "འཚོལ་བཤེར་ལོ་རྒྱུས་གཙང་སེལ།",
"Quick action disabled": "མྱུར་མགྱོགས་བྱ་འགུལ་བཀག་འགོག",
"Tap to Toggle Footer": "ཞབས་མཇུག་སྒོ་འབྱེད་བྱེད་པར་གནོན།"
"Tap to Toggle Footer": "ཞབས་མཇུག་སྒོ་འབྱེད་བྱེད་པར་གནོན།",
"Exported successfully": "ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
"Book exported successfully.": "དཔེ་དེབ་ཕྱིར་འདོན་ལེགས་གྲུབ་བྱུང་།",
"Failed to export the book.": "དཔེ་དེབ་ཕྱིར་འདོན་མི་ཐུབ།",
"Export Book": "དཔེ་དེབ་ཕྱིར་འདོན།",
"Whole word:": "ཚིག་གྲུབ་ཆ་ཚང་:",
"Error": "ནོར་འཁྲུལ།",
"Unable to load the article. Try searching directly on {{link}}.": "རྩོམ་ཡིག་མངོན་མི་ཐུབ། {{link}} ཐད་ཀར་འཚོལ་བ་བྱོས།",
"Unable to load the word. Try searching directly on {{link}}.": "ཚིག་མངོན་མི་ཐུབ། {{link}} ཐད་ཀར་འཚོལ་བ་བྱོས།",
"Date Published": "པར་སྐྲུན་ཚེས་གྲངས།",
"Only for TTS:": "TTS ལ་ཁོ་ན།:",
"Uploaded": "ཡར་སྐྱེལ་བྱས་ཟིན།",
"Downloaded": "མར་ལེན་བྱས་ཟིན།",
"Deleted": "བསུབས་ཟིན།",
"Note:": "གསལ་བཤད།:",
"Time:": "དུས་ཚོད།:",
"Format Options": "རྣམ་གཞག་གདམ་ག",
"Export Date": "ཕྱིར་འདོན་ཚེས་གྲངས།",
"Chapter Titles": "ལེའུའི་མཚན།",
"Chapter Separator": "ལེའུའི་དབར་མཚམས།",
"Highlights": "གཙོ་གནད།",
"Note Date": "གསལ་བཤད་ཚེས་གྲངས།",
"Advanced": "མཐོ་རིམ།",
"Hide": "སྦས་པ།",
"Show": "མངོན་པ།",
"Use Custom Template": "རང་བཟོའི་དཔེ་གཞི་བེད་སྤྱོད།",
"Export Template": "ཕྱིར་འདོན་དཔེ་གཞི།",
"Template Syntax:": "དཔེ་གཞིའི་སྒྲིག་གཞི།:",
"Insert value": "གྲངས་ཐང་བཙུགས་པ།",
"Format date (locale)": "ཚེས་གྲངས་རྣམ་གཞག (ས་གནས།)",
"Format date (custom)": "ཚེས་གྲངས་རྣམ་གཞག (རང་བཟོ།)",
"Conditional": "དམིགས་བསལ།",
"Loop": "འཁོར་ལོ།",
"Available Variables:": "བེད་སྤྱད་རུང་བའི་འགྱུར་ཅན།:",
"Book title": "དེབ་མཚན།",
"Book author": "དེབ་རྩོམ་པ་པོ།",
"Export date": "ཕྱིར་འདོན་ཚེས་གྲངས།",
"Array of chapters": "ལེའུའི་ལེབ་ངོས།",
"Chapter title": "ལེའུའི་མཚན།",
"Array of annotations": "གསལ་བཤད་ལེབ་ངོས།",
"Highlighted text": "གཙོ་གནད་ཡིག་རྐྱང་།",
"Annotation note": "གསལ་བཤད་ཟིན་བྲིས།",
"Date Format Tokens:": "ཚེས་གྲངས་རྣམ་གཞག་རྟགས།:",
"Year (4 digits)": "ལོ། (གྲངས་ཐང་4)",
"Month (01-12)": "ཟླ། (01-12)",
"Day (01-31)": "ཚེས། (01-31)",
"Hour (00-23)": "ཆུ་ཚོད། (00-23)",
"Minute (00-59)": "སྐར་མ། (00-59)",
"Second (00-59)": "སྐར་ཆ། (00-59)",
"Show Source": "འབྱུང་ཁུངས་མངོན་པ།",
"No content to preview": "སྔོན་ལྟ་བྱེད་པའི་དོན་རྐྱེན་མེད།",
"Export": "ཕྱིར་འདོན།",
"Set Timeout": "དུས་ཚོད་སྒྲིག་པ།",
"Select Voice": "སྐད་གདངས་འདེམས།",
"Toggle Sticky Bottom TTS Bar": "TTS སྡོམ་ཐིག་བརྗེ་བ།",
"Display what I'm reading on Discord": "Discord ཐོག་ཀློག་བཞིན་པའི་དཔེ་ཆ་སྟོན།",
"Show on Discord": "Discord ཐོག་སྟོན།",
"Instant {{action}}": "འཕྲལ་མར་{{action}}",
"Instant {{action}} Disabled": "འཕྲལ་མར་{{action}}་ལྕོགས་མིན་བཟོས།",
"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": "ལེའུ་འདེམས་པ།",
"{{number}} WPM": "{{number}} WPM",
"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 གནས་སར་ཕྱིར་ལོག་པ།"
}
@@ -172,9 +172,6 @@
"Token": "Token",
"Your OTP token": "Ihr OTP-Token",
"Verify token": "Token überprüfen",
"Sign in with Google": "Mit Google anmelden",
"Sign in with Apple": "Mit Apple anmelden",
"Sign in with GitHub": "Mit GitHub anmelden",
"Account": "Konto",
"Failed to delete user. Please try again later.": "Benutzer konnte nicht gelöscht werden. Bitte versuchen Sie es später erneut.",
"Community Support": "Community-Support",
@@ -340,8 +337,8 @@
"{{count}} pages left in chapter_one": "{{count}} Seite verbleibend im Kapitel",
"{{count}} pages left in chapter_other": "{{count}} Seiten verbleibend im Kapitel",
"Show Remaining Pages": "Verbleibende Seiten anzeigen",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Abo verwalten",
"Coming Soon": "Demnächst verfügbar",
@@ -835,6 +832,204 @@
"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"
"Tap to Toggle Footer": "Tippen, um Fußzeile umzuschalten",
"Exported successfully": "Erfolgreich exportiert",
"Book exported successfully.": "Buch erfolgreich exportiert.",
"Failed to export the book.": "Buch konnte nicht exportiert werden.",
"Export Book": "Buch exportieren",
"Whole word:": "Ganzes Wort:",
"Error": "Fehler",
"Unable to load the article. Try searching directly on {{link}}.": "Artikel kann nicht geladen werden. Versuchen Sie die direkte Suche auf {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Wort kann nicht geladen werden. Versuchen Sie die direkte Suche auf {{link}}.",
"Date Published": "Veröffentlichungsdatum",
"Only for TTS:": "Nur für TTS:",
"Uploaded": "Hochgeladen",
"Downloaded": "Heruntergeladen",
"Deleted": "Gelöscht",
"Note:": "Notiz:",
"Time:": "Zeit:",
"Format Options": "Formatoptionen",
"Export Date": "Exportdatum",
"Chapter Titles": "Kapiteltitel",
"Chapter Separator": "Kapiteltrennzeichen",
"Highlights": "Markierungen",
"Note Date": "Notizdatum",
"Advanced": "Erweitert",
"Hide": "Ausblenden",
"Show": "Anzeigen",
"Use Custom Template": "Benutzerdefinierte Vorlage verwenden",
"Export Template": "Exportvorlage",
"Template Syntax:": "Vorlagensyntax:",
"Insert value": "Wert einfügen",
"Format date (locale)": "Datum formatieren (Gebietsschema)",
"Format date (custom)": "Datum formatieren (benutzerdefiniert)",
"Conditional": "Bedingung",
"Loop": "Schleife",
"Available Variables:": "Verfügbare Variablen:",
"Book title": "Buchtitel",
"Book author": "Buchautor",
"Export date": "Exportdatum",
"Array of chapters": "Array von Kapiteln",
"Chapter title": "Kapiteltitel",
"Array of annotations": "Array von Anmerkungen",
"Highlighted text": "Markierter Text",
"Annotation note": "Anmerkungsnotiz",
"Date Format Tokens:": "Datumsformat-Token:",
"Year (4 digits)": "Jahr (4 Ziffern)",
"Month (01-12)": "Monat (01-12)",
"Day (01-31)": "Tag (01-31)",
"Hour (00-23)": "Stunde (00-23)",
"Minute (00-59)": "Minute (00-59)",
"Second (00-59)": "Sekunde (00-59)",
"Show Source": "Quelle anzeigen",
"No content to preview": "Kein Inhalt zur Vorschau",
"Export": "Exportieren",
"Set Timeout": "Zeitlimit festlegen",
"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",
"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",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Κωδικός επαλήθευσης",
"Your OTP token": "Ο κωδικός OTP σας",
"Verify token": "Επαλήθευση κωδικού",
"Sign in with Google": "Σύνδεση με Google",
"Sign in with Apple": "Σύνδεση με Apple",
"Sign in with GitHub": "Σύνδεση με GitHub",
"Account": "Λογαριασμός",
"Failed to delete user. Please try again later.": "Αποτυχία διαγραφής χρήστη. Παρακαλώ δοκιμάστε ξανά αργότερα.",
"Community Support": "Υποστήριξη κοινότητας",
@@ -341,8 +338,8 @@
"{{count}} pages left in chapter_one": "Μένει {{count}} σελίδα",
"{{count}} pages left in chapter_other": "Μένουν {{count}} σελίδες",
"Show Remaining Pages": "Εμφάνιση υπολοίπων",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Διαχείριση συνδρομής",
"Coming Soon": "Έρχεται σύντομα",
@@ -835,6 +832,204 @@
"Show Results": "Εμφάνιση αποτελεσμάτων",
"Clear search": "Εκκαθάριση αναζήτησης",
"Clear search history": "Εκκαθάριση ιστορικού αναζήτησης",
"Quick action disabled": "Η γρήγορη ενέργεια απενεργοποιήθηκε",
"Tap to Toggle Footer": "Πατήστε για εναλλαγή υποσέλιδου"
"Tap to Toggle Footer": "Πατήστε για εναλλαγή υποσέλιδου",
"Exported successfully": "Εξαγωγή επιτυχής",
"Book exported successfully.": "Το βιβλίο εξήχθη επιτυχώς.",
"Failed to export the book.": "Αποτυχία εξαγωγής του βιβλίου.",
"Export Book": "Εξαγωγή βιβλίου",
"Whole word:": "Ολόκληρη λέξη:",
"Error": "Σφάλμα",
"Unable to load the article. Try searching directly on {{link}}.": "Αδυναμία φόρτωσης του άρθρου. Δοκιμάστε να αναζητήσετε απευθείας στο {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Αδυναμία φόρτωσης της λέξης. Δοκιμάστε να αναζητήσετε απευθείας στο {{link}}.",
"Date Published": "Ημερομηνία δημοσίευσης",
"Only for TTS:": "Μόνο για TTS:",
"Uploaded": "Μεταφορτώθηκε",
"Downloaded": "Λήφθηκε",
"Deleted": "Διαγράφηκε",
"Note:": "Σημείωση:",
"Time:": "Ώρα:",
"Format Options": "Επιλογές μορφοποίησης",
"Export Date": "Ημερομηνία εξαγωγής",
"Chapter Titles": "Τίτλοι κεφαλαίων",
"Chapter Separator": "Διαχωριστικό κεφαλαίων",
"Highlights": "Επισημάνσεις",
"Note Date": "Ημερομηνία σημείωσης",
"Advanced": "Για προχωρημένους",
"Hide": "Απόκρυψη",
"Show": "Εμφάνιση",
"Use Custom Template": "Χρήση προσαρμοσμένου προτύπου",
"Export Template": "Πρότυπο εξαγωγής",
"Template Syntax:": "Σύνταξη προτύπου:",
"Insert value": "Εισαγωγή τιμής",
"Format date (locale)": "Μορφοποίηση ημερομηνίας (τοπική)",
"Format date (custom)": "Μορφοποίηση ημερομηνίας (προσαρμοσμένη)",
"Conditional": "Υπό όρους",
"Loop": "Βρόχος",
"Available Variables:": "Διαθέσιμες μεταβλητές:",
"Book title": "Τίτλος βιβλίου",
"Book author": "Συγγραφέας βιβλίου",
"Export date": "Ημερομηνία εξαγωγής",
"Array of chapters": "Πίνακας κεφαλαίων",
"Chapter title": "Τίτλος κεφαλαίου",
"Array of annotations": "Πίνακας σχολιασμών",
"Highlighted text": "Επισημασμένο κείμενο",
"Annotation note": "Σημείωση σχολιασμού",
"Date Format Tokens:": "Σύμβολα μορφοποίησης ημερομηνίας:",
"Year (4 digits)": "Έτος (4 ψηφία)",
"Month (01-12)": "Μήνας (01-12)",
"Day (01-31)": "Ημέρα (01-31)",
"Hour (00-23)": "Ώρα (00-23)",
"Minute (00-59)": "Λεπτό (00-59)",
"Second (00-59)": "Δευτερόλεπτο (00-59)",
"Show Source": "Εμφάνιση πηγής",
"No content to preview": "Δεν υπάρχει περιεχόμενο για προεπισκόπηση",
"Export": "Εξαγωγή",
"Set Timeout": "Ορισμός χρονικού ορίου",
"Select Voice": "Επιλογή φωνής",
"Toggle Sticky Bottom TTS Bar": "Εναλλαγή καρφιτσωμένης μπάρας TTS",
"Display what I'm reading on Discord": "Εμφάνιση του βιβλίου που διαβάζω στο Discord",
"Show on Discord": "Εμφάνιση στο Discord",
"Instant {{action}}": "Άμεση {{action}}",
"Instant {{action}} Disabled": "Άμεση {{action}} απενεργοποιημένη",
"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": "Επιλογή Κεφαλαίου",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -1,8 +1,8 @@
{
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Are you sure to delete {{count}} selected book(s)?_one": "Are you sure to delete {{count}} selected book?",
"Are you sure to delete {{count}} selected book(s)?_other": "Are you sure to delete {{count}} selected books?",
@@ -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"
}
@@ -200,9 +200,6 @@
"Token": "Código de verificación",
"Your OTP token": "Tu código OTP",
"Verify token": "Verificar código",
"Sign in with Google": "Iniciar sesión con Google",
"Sign in with Apple": "Iniciar sesión con Apple",
"Sign in with GitHub": "Iniciar sesión con GitHub",
"Account": "Cuenta",
"Failed to delete user. Please try again later.": "Error al eliminar usuario. Por favor, inténtelo de nuevo más tarde.",
"Community Support": "Soporte comunitario",
@@ -372,8 +369,8 @@
"{{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",
"Show Remaining Pages": "Mostrar resto",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gestionar suscripción",
"Coming Soon": "Próximamente",
@@ -845,6 +842,206 @@
"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"
"Tap to Toggle Footer": "Toca para alternar pie de página",
"Exported successfully": "Exportado con éxito",
"Book exported successfully.": "Libro exportado con éxito.",
"Failed to export the book.": "Error al exportar el libro.",
"Export Book": "Exportar libro",
"Whole word:": "Palabra completa:",
"Error": "Error",
"Unable to load the article. Try searching directly on {{link}}.": "No se puede cargar el artículo. Intenta buscar directamente en {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "No se puede cargar la palabra. Intenta buscar directamente en {{link}}.",
"Date Published": "Fecha de publicación",
"Only for TTS:": "Solo para TTS:",
"Uploaded": "Subido",
"Downloaded": "Descargado",
"Deleted": "Eliminado",
"Note:": "Nota:",
"Time:": "Hora:",
"Format Options": "Opciones de formato",
"Export Date": "Fecha de exportación",
"Chapter Titles": "Títulos de capítulos",
"Chapter Separator": "Separador de capítulos",
"Highlights": "Resaltados",
"Note Date": "Fecha de nota",
"Advanced": "Avanzado",
"Hide": "Ocultar",
"Show": "Mostrar",
"Use Custom Template": "Usar plantilla personalizada",
"Export Template": "Plantilla de exportación",
"Template Syntax:": "Sintaxis de plantilla:",
"Insert value": "Insertar valor",
"Format date (locale)": "Formatear fecha (regional)",
"Format date (custom)": "Formatear fecha (personalizado)",
"Conditional": "Condicional",
"Loop": "Bucle",
"Available Variables:": "Variables disponibles:",
"Book title": "Título del libro",
"Book author": "Autor del libro",
"Export date": "Fecha de exportación",
"Array of chapters": "Lista de capítulos",
"Chapter title": "Título del capítulo",
"Array of annotations": "Lista de anotaciones",
"Highlighted text": "Texto resaltado",
"Annotation note": "Nota de anotación",
"Date Format Tokens:": "Tokens de formato de fecha:",
"Year (4 digits)": "Año (4 dígitos)",
"Month (01-12)": "Mes (01-12)",
"Day (01-31)": "Día (01-31)",
"Hour (00-23)": "Hora (00-23)",
"Minute (00-59)": "Minuto (00-59)",
"Second (00-59)": "Segundo (00-59)",
"Show Source": "Mostrar fuente",
"No content to preview": "Sin contenido para previsualizar",
"Export": "Exportar",
"Set Timeout": "Establecer tiempo límite",
"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",
"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",
"{{number}} WPM": "{{number}} PPM",
"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"
}
@@ -172,9 +172,6 @@
"Token": "توکن",
"Your OTP token": "کد یک‌بارمصرف شما",
"Verify token": "تأیید کد",
"Sign in with Google": "ورود با گوگل",
"Sign in with Apple": "ورود با اپل",
"Sign in with GitHub": "ورود با گیت‌هاب",
"Account": "حساب",
"Failed to delete user. Please try again later.": "حذف کاربر ناموفق بود. لطفاً بعداً دوباره تلاش کنید.",
"Community Support": "پشتیبانی جامعه",
@@ -340,8 +337,8 @@
"{{count}} pages left in chapter_one": "{{count}} صفحه تا انتهای فصل",
"{{count}} pages left in chapter_other": "{{count}} صفحه تا انتهای فصل",
"Show Remaining Pages": "نمایش صفحات باقی‌مانده",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"Source Han Serif CN": "Source Han Serif CN",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Manage Subscription": "مدیریت اشتراک",
"Coming Soon": "به‌زودی",
@@ -835,6 +832,204 @@
"Show Results": "نمایش نتایج",
"Clear search": "پاک کردن جستجو",
"Clear search history": "پاک کردن تاریخچه جستجو",
"Quick action disabled": "عملکرد سریع غیرفعال است",
"Tap to Toggle Footer": "برای تغییر پاورقی ضربه بزنید"
"Tap to Toggle Footer": "برای تغییر پاورقی ضربه بزنید",
"Exported successfully": "صادرات موفق",
"Book exported successfully.": "کتاب با موفقیت صادر شد.",
"Failed to export the book.": "صادر کردن کتاب ناموفق بود.",
"Export Book": "صادر کردن کتاب",
"Whole word:": "کلمه کامل:",
"Error": "خطا",
"Unable to load the article. Try searching directly on {{link}}.": "بارگذاری مقاله امکان‌پذیر نیست. سعی کنید مستقیماً در {{link}} جستجو کنید.",
"Unable to load the word. Try searching directly on {{link}}.": "بارگذاری کلمه امکان‌پذیر نیست. سعی کنید مستقیماً در {{link}} جستجو کنید.",
"Date Published": "تاریخ انتشار",
"Only for TTS:": "فقط برای TTS:",
"Uploaded": "بارگذاری شد",
"Downloaded": "دانلود شد",
"Deleted": "حذف شد",
"Note:": "یادداشت:",
"Time:": "زمان:",
"Format Options": "گزینه‌های قالب",
"Export Date": "تاریخ خروجی",
"Chapter Titles": "عناوین فصل",
"Chapter Separator": "جداکننده فصل",
"Highlights": "برجسته‌ها",
"Note Date": "تاریخ یادداشت",
"Advanced": "پیشرفته",
"Hide": "پنهان کردن",
"Show": "نمایش",
"Use Custom Template": "استفاده از الگوی سفارشی",
"Export Template": "الگوی خروجی",
"Template Syntax:": "نحو الگو:",
"Insert value": "درج مقدار",
"Format date (locale)": "قالب‌بندی تاریخ (محلی)",
"Format date (custom)": "قالب‌بندی تاریخ (سفارشی)",
"Conditional": "شرطی",
"Loop": "حلقه",
"Available Variables:": "متغیرهای موجود:",
"Book title": "عنوان کتاب",
"Book author": "نویسنده کتاب",
"Export date": "تاریخ خروجی",
"Array of chapters": "آرایه فصل‌ها",
"Chapter title": "عنوان فصل",
"Array of annotations": "آرایه یادداشت‌ها",
"Highlighted text": "متن برجسته‌شده",
"Annotation note": "یادداشت حاشیه",
"Date Format Tokens:": "نشانه‌های قالب تاریخ:",
"Year (4 digits)": "سال (۴ رقم)",
"Month (01-12)": "ماه (۰۱-۱۲)",
"Day (01-31)": "روز (۰۱-۳۱)",
"Hour (00-23)": "ساعت (۰۰-۲۳)",
"Minute (00-59)": "دقیقه (۰۰-۵۹)",
"Second (00-59)": "ثانیه (۰۰-۵۹)",
"Show Source": "نمایش منبع",
"No content to preview": "محتوایی برای پیش‌نمایش وجود ندارد",
"Export": "خروجی",
"Set Timeout": "تنظیم مهلت زمانی",
"Select Voice": "انتخاب صدا",
"Toggle Sticky Bottom TTS Bar": "تغییر نوار TTS ثابت",
"Display what I'm reading on Discord": "نمایش کتاب در حال خواندن در Discord",
"Show on Discord": "نمایش در Discord",
"Instant {{action}}": "{{action}} فوری",
"Instant {{action}} Disabled": "{{action}} فوری غیرفعال شد",
"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": "انتخاب فصل",
"{{number}} WPM": "{{number}} کلمه در دقیقه",
"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"
}
@@ -172,9 +172,6 @@
"Token": "Code de vérification",
"Your OTP token": "Votre code OTP",
"Verify token": "Vérifier le code",
"Sign in with Google": "Se connecter avec Google",
"Sign in with Apple": "Se connecter avec Apple",
"Sign in with GitHub": "Se connecter avec GitHub",
"Account": "Compte",
"Failed to delete user. Please try again later.": "Échec de la suppression de l'utilisateur. Veuillez réessayer plus tard.",
"Community Support": "Support communautaire",
@@ -344,8 +341,8 @@
"{{count}} pages left in chapter_many": "{{count}} pages restantes dans le chapitre",
"{{count}} pages left in chapter_other": "{{count}} pages restantes dans le chapitre",
"Show Remaining Pages": "Voir restantes",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gérer labonnement",
"Coming Soon": "Bientôt disponible",
@@ -845,6 +842,206 @@
"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"
"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.",
"Failed to export the book.": "Échec de l'exportation du livre.",
"Export Book": "Exporter le livre",
"Whole word:": "Mot entier :",
"Error": "Erreur",
"Unable to load the article. Try searching directly on {{link}}.": "Impossible de charger l'article. Essayez de rechercher directement sur {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Impossible de charger le mot. Essayez de rechercher directement sur {{link}}.",
"Date Published": "Date de publication",
"Only for TTS:": "Uniquement pour TTS :",
"Uploaded": "Téléversé",
"Downloaded": "Téléchargé",
"Deleted": "Supprimé",
"Note:": "Note :",
"Time:": "Heure :",
"Format Options": "Options de format",
"Export Date": "Date d'export",
"Chapter Titles": "Titres de chapitres",
"Chapter Separator": "Séparateur de chapitres",
"Highlights": "Surlignages",
"Note Date": "Date de note",
"Advanced": "Avancé",
"Hide": "Masquer",
"Show": "Afficher",
"Use Custom Template": "Utiliser un modèle personnalisé",
"Export Template": "Modèle d'export",
"Template Syntax:": "Syntaxe du modèle :",
"Insert value": "Insérer une valeur",
"Format date (locale)": "Formater la date (locale)",
"Format date (custom)": "Formater la date (personnalisé)",
"Conditional": "Conditionnel",
"Loop": "Boucle",
"Available Variables:": "Variables disponibles :",
"Book title": "Titre du livre",
"Book author": "Auteur du livre",
"Export date": "Date d'export",
"Array of chapters": "Liste de chapitres",
"Chapter title": "Titre du chapitre",
"Array of annotations": "Liste d'annotations",
"Highlighted text": "Texte surligné",
"Annotation note": "Note d'annotation",
"Date Format Tokens:": "Jetons de format de date :",
"Year (4 digits)": "Année (4 chiffres)",
"Month (01-12)": "Mois (01-12)",
"Day (01-31)": "Jour (01-31)",
"Hour (00-23)": "Heure (00-23)",
"Minute (00-59)": "Minute (00-59)",
"Second (00-59)": "Seconde (00-59)",
"Show Source": "Afficher la source",
"No content to preview": "Aucun contenu à prévisualiser",
"Export": "Exporter",
"Set Timeout": "Définir le délai",
"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",
"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",
"{{number}} WPM": "{{number}} MPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "टोकन",
"Your OTP token": "आपका ओटीपी टोकन",
"Verify token": "टोकन सत्यापित करें",
"Sign in with Google": "Google के साथ साइन इन करें",
"Sign in with Apple": "Apple के साथ साइन इन करें",
"Sign in with GitHub": "GitHub के साथ साइन इन करें",
"Account": "खाता",
"Failed to delete user. Please try again later.": "उपयोगकर्ता को हटाने में विफल। कृपया बाद में पुनः प्रयास करें।",
"Community Support": "सामुदायिक सहायता",
@@ -341,8 +338,8 @@
"{{count}} pages left in chapter_one": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
"{{count}} pages left in chapter_other": "इस अध्याय में {{count}} पृष्ठ शेष हैं",
"Show Remaining Pages": "शेष पृष्ठ दिखाएँ",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "सदस्यता प्रबंधित करें",
"Coming Soon": "जल्द आ रहा है",
@@ -835,6 +832,204 @@
"Show Results": "परिणाम दिखाएं",
"Clear search": "खोज साफ़ करें",
"Clear search history": "खोज इतिहास साफ़ करें",
"Quick action disabled": "त्वरित क्रिया अक्षम",
"Tap to Toggle Footer": "फुटर टॉगल करने के लिए टैप करें"
"Tap to Toggle Footer": "फुटर टॉगल करने के लिए टैप करें",
"Exported successfully": "सफलतापूर्वक निर्यात किया गया",
"Book exported successfully.": "पुस्तक सफलतापूर्वक निर्यात की गई।",
"Failed to export the book.": "पुस्तक निर्यात करने में विफल।",
"Export Book": "पुस्तक निर्यात करें",
"Whole word:": "पूरा शब्द:",
"Error": "त्रुटि",
"Unable to load the article. Try searching directly on {{link}}.": "लेख लोड करने में असमर्थ। सीधे {{link}} पर खोजने का प्रयास करें।",
"Unable to load the word. Try searching directly on {{link}}.": "शब्द लोड करने में असमर्थ। सीधे {{link}} पर खोजने का प्रयास करें।",
"Date Published": "प्रकाशन तिथि",
"Only for TTS:": "केवल TTS के लिए:",
"Uploaded": "अपलोड किया गया",
"Downloaded": "डाउनलोड किया गया",
"Deleted": "हटाया गया",
"Note:": "नोट:",
"Time:": "समय:",
"Format Options": "प्रारूप विकल्प",
"Export Date": "निर्यात तिथि",
"Chapter Titles": "अध्याय शीर्षक",
"Chapter Separator": "अध्याय विभाजक",
"Highlights": "हाइलाइट्स",
"Note Date": "नोट तिथि",
"Advanced": "उन्नत",
"Hide": "छिपाएं",
"Show": "दिखाएं",
"Use Custom Template": "कस्टम टेम्पलेट उपयोग करें",
"Export Template": "निर्यात टेम्पलेट",
"Template Syntax:": "टेम्पलेट सिंटैक्स:",
"Insert value": "मान डालें",
"Format date (locale)": "तिथि फ़ॉर्मेट करें (स्थानीय)",
"Format date (custom)": "तिथि फ़ॉर्मेट करें (कस्टम)",
"Conditional": "सशर्त",
"Loop": "लूप",
"Available Variables:": "उपलब्ध चर:",
"Book title": "पुस्तक शीर्षक",
"Book author": "पुस्तक लेखक",
"Export date": "निर्यात तिथि",
"Array of chapters": "अध्यायों की सूची",
"Chapter title": "अध्याय शीर्षक",
"Array of annotations": "एनोटेशन की सूची",
"Highlighted text": "हाइलाइट किया गया पाठ",
"Annotation note": "एनोटेशन नोट",
"Date Format Tokens:": "तिथि प्रारूप टोकन:",
"Year (4 digits)": "वर्ष (4 अंक)",
"Month (01-12)": "माह (01-12)",
"Day (01-31)": "दिन (01-31)",
"Hour (00-23)": "घंटा (00-23)",
"Minute (00-59)": "मिनट (00-59)",
"Second (00-59)": "सेकंड (00-59)",
"Show Source": "स्रोत दिखाएं",
"No content to preview": "पूर्वावलोकन के लिए कोई सामग्री नहीं",
"Export": "निर्यात करें",
"Set Timeout": "टाइमआउट सेट करें",
"Select Voice": "आवाज़ चुनें",
"Toggle Sticky Bottom TTS Bar": "स्थिर TTS बार टॉगल करें",
"Display what I'm reading on Discord": "Discord पर पढ़ रही किताब दिखाएं",
"Show on Discord": "Discord पर दिखाएं",
"Instant {{action}}": "तत्काल {{action}}",
"Instant {{action}} Disabled": "तत्काल {{action}} अक्षम",
"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": "अध्याय चुनें",
"{{number}} WPM": "{{number}} WPM",
"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": "टीटीएस स्थान पर वापस जाएं"
}
@@ -173,9 +173,6 @@
"Token": "Token",
"Your OTP token": "Token OTP Anda",
"Verify token": "Verifikasi token",
"Sign in with Google": "Masuk dengan Google",
"Sign in with Apple": "Masuk dengan Apple",
"Sign in with GitHub": "Masuk dengan GitHub",
"Account": "Akun",
"Failed to delete user. Please try again later.": "Gagal menghapus pengguna. Silakan coba lagi nanti.",
"Community Support": "Dukungan Komunitas",
@@ -337,8 +334,8 @@
"Reset Settings": "Atur Ulang Pengaturan",
"{{count}} pages left in chapter_other": "{{count}} halaman tersisa di bab ini",
"Show Remaining Pages": "Tampilkan halaman tersisa",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Kelola Langganan",
"Coming Soon": "Segera Hadir",
@@ -825,6 +822,202 @@
"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"
"Tap to Toggle Footer": "Ketuk untuk beralih footer",
"Exported successfully": "Berhasil diekspor",
"Book exported successfully.": "Buku berhasil diekspor.",
"Failed to export the book.": "Gagal mengekspor buku.",
"Export Book": "Ekspor Buku",
"Whole word:": "Kata utuh:",
"Error": "Kesalahan",
"Unable to load the article. Try searching directly on {{link}}.": "Tidak dapat memuat artikel. Coba cari langsung di {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Tidak dapat memuat kata. Coba cari langsung di {{link}}.",
"Date Published": "Tanggal terbit",
"Only for TTS:": "Hanya untuk TTS:",
"Uploaded": "Diunggah",
"Downloaded": "Diunduh",
"Deleted": "Dihapus",
"Note:": "Catatan:",
"Time:": "Waktu:",
"Format Options": "Opsi Format",
"Export Date": "Tanggal Ekspor",
"Chapter Titles": "Judul Bab",
"Chapter Separator": "Pemisah Bab",
"Highlights": "Sorotan",
"Note Date": "Tanggal Catatan",
"Advanced": "Lanjutan",
"Hide": "Sembunyikan",
"Show": "Tampilkan",
"Use Custom Template": "Gunakan Template Kustom",
"Export Template": "Template Ekspor",
"Template Syntax:": "Sintaks Template:",
"Insert value": "Masukkan nilai",
"Format date (locale)": "Format tanggal (lokal)",
"Format date (custom)": "Format tanggal (kustom)",
"Conditional": "Kondisional",
"Loop": "Loop",
"Available Variables:": "Variabel yang Tersedia:",
"Book title": "Judul buku",
"Book author": "Penulis buku",
"Export date": "Tanggal ekspor",
"Array of chapters": "Daftar bab",
"Chapter title": "Judul bab",
"Array of annotations": "Daftar anotasi",
"Highlighted text": "Teks yang disorot",
"Annotation note": "Catatan anotasi",
"Date Format Tokens:": "Token Format Tanggal:",
"Year (4 digits)": "Tahun (4 digit)",
"Month (01-12)": "Bulan (01-12)",
"Day (01-31)": "Hari (01-31)",
"Hour (00-23)": "Jam (00-23)",
"Minute (00-59)": "Menit (00-59)",
"Second (00-59)": "Detik (00-59)",
"Show Source": "Tampilkan Sumber",
"No content to preview": "Tidak ada konten untuk dipratinjau",
"Export": "Ekspor",
"Set Timeout": "Atur batas waktu",
"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",
"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",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Token",
"Your OTP token": "Il tuo token OTP",
"Verify token": "Verifica token",
"Sign in with Google": "Accedi con Google",
"Sign in with Apple": "Accedi con Apple",
"Sign in with GitHub": "Accedi con GitHub",
"Account": "Account",
"Failed to delete user. Please try again later.": "Impossibile eliminare l'utente. Riprova più tardi.",
"Community Support": "Supporto della community",
@@ -345,8 +342,8 @@
"{{count}} pages left in chapter_many": "{{count}} pagine rimaste nel capitolo",
"{{count}} pages left in chapter_other": "{{count}} pagine rimaste nel capitolo",
"Show Remaining Pages": "Mostra pagine rimanenti",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gestisci Abbonamento",
"Coming Soon": "Prossimamente",
@@ -845,6 +842,206 @@
"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"
"Tap to Toggle Footer": "Tocca per mostrare/nascondere il piè di pagina",
"Exported successfully": "Esportato con successo",
"Book exported successfully.": "Libro esportato con successo.",
"Failed to export the book.": "Impossibile esportare il libro.",
"Export Book": "Esporta libro",
"Whole word:": "Parola intera:",
"Error": "Errore",
"Unable to load the article. Try searching directly on {{link}}.": "Impossibile caricare l'articolo. Prova a cercare direttamente su {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Impossibile caricare la parola. Prova a cercare direttamente su {{link}}.",
"Date Published": "Data di pubblicazione",
"Only for TTS:": "Solo per TTS:",
"Uploaded": "Caricato",
"Downloaded": "Scaricato",
"Deleted": "Eliminato",
"Note:": "Nota:",
"Time:": "Ora:",
"Format Options": "Opzioni formato",
"Export Date": "Data esportazione",
"Chapter Titles": "Titoli capitoli",
"Chapter Separator": "Separatore capitoli",
"Highlights": "Evidenziazioni",
"Note Date": "Data nota",
"Advanced": "Avanzate",
"Hide": "Nascondi",
"Show": "Mostra",
"Use Custom Template": "Usa modello personalizzato",
"Export Template": "Modello esportazione",
"Template Syntax:": "Sintassi modello:",
"Insert value": "Inserisci valore",
"Format date (locale)": "Formatta data (locale)",
"Format date (custom)": "Formatta data (personalizzato)",
"Conditional": "Condizionale",
"Loop": "Ciclo",
"Available Variables:": "Variabili disponibili:",
"Book title": "Titolo libro",
"Book author": "Autore libro",
"Export date": "Data esportazione",
"Array of chapters": "Elenco capitoli",
"Chapter title": "Titolo capitolo",
"Array of annotations": "Elenco annotazioni",
"Highlighted text": "Testo evidenziato",
"Annotation note": "Nota annotazione",
"Date Format Tokens:": "Token formato data:",
"Year (4 digits)": "Anno (4 cifre)",
"Month (01-12)": "Mese (01-12)",
"Day (01-31)": "Giorno (01-31)",
"Hour (00-23)": "Ora (00-23)",
"Minute (00-59)": "Minuto (00-59)",
"Second (00-59)": "Secondo (00-59)",
"Show Source": "Mostra sorgente",
"No content to preview": "Nessun contenuto da visualizzare",
"Export": "Esporta",
"Set Timeout": "Imposta timeout",
"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",
"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",
"{{number}} WPM": "{{number}} PPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "トークン",
"Your OTP token": "あなたのOTPトークン",
"Verify token": "トークンを確認",
"Sign in with Google": "Googleでサインイン",
"Sign in with Apple": "Appleでサインイン",
"Sign in with GitHub": "GitHubでサインイン",
"Account": "アカウント",
"Failed to delete user. Please try again later.": "ユーザーの削除に失敗しました。後ほど再試行してください。",
"Community Support": "コミュニティサポート",
@@ -337,8 +334,8 @@
"Reset Settings": "設定をリセット",
"{{count}} pages left in chapter_other": "{{count}} ページ残り",
"Show Remaining Pages": "残りを表示",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "サブスクリプションを管理",
"Coming Soon": "近日公開",
@@ -825,6 +822,202 @@
"Show Results": "結果を表示",
"Clear search": "検索をクリア",
"Clear search history": "検索履歴をクリア",
"Quick action disabled": "クイックアクション無効",
"Tap to Toggle Footer": "タップでフッターを切り替え"
"Tap to Toggle Footer": "タップでフッターを切り替え",
"Exported successfully": "エクスポート成功",
"Book exported successfully.": "書籍のエクスポートに成功しました。",
"Failed to export the book.": "書籍のエクスポートに失敗しました。",
"Export Book": "書籍をエクスポート",
"Whole word:": "単語全体:",
"Error": "エラー",
"Unable to load the article. Try searching directly on {{link}}.": "記事を読み込めません。{{link}}で直接検索してみてください。",
"Unable to load the word. Try searching directly on {{link}}.": "単語を読み込めません。{{link}}で直接検索してみてください。",
"Date Published": "出版日",
"Only for TTS:": "TTSのみ:",
"Uploaded": "アップロード済み",
"Downloaded": "ダウンロード済み",
"Deleted": "削除済み",
"Note:": "ノート:",
"Time:": "時刻:",
"Format Options": "形式オプション",
"Export Date": "エクスポート日",
"Chapter Titles": "章タイトル",
"Chapter Separator": "章区切り",
"Highlights": "ハイライト",
"Note Date": "ノート日付",
"Advanced": "詳細設定",
"Hide": "非表示",
"Show": "表示",
"Use Custom Template": "カスタムテンプレート使用",
"Export Template": "エクスポートテンプレート",
"Template Syntax:": "テンプレート構文:",
"Insert value": "値を挿入",
"Format date (locale)": "日付形式(ロケール)",
"Format date (custom)": "日付形式(カスタム)",
"Conditional": "条件分岐",
"Loop": "ループ",
"Available Variables:": "利用可能な変数:",
"Book title": "本のタイトル",
"Book author": "著者",
"Export date": "エクスポート日",
"Array of chapters": "章の配列",
"Chapter title": "章タイトル",
"Array of annotations": "注釈の配列",
"Highlighted text": "ハイライトテキスト",
"Annotation note": "注釈ノート",
"Date Format Tokens:": "日付形式トークン:",
"Year (4 digits)": "年(4桁)",
"Month (01-12)": "月(01-12)",
"Day (01-31)": "日(01-31)",
"Hour (00-23)": "時(00-23)",
"Minute (00-59)": "分(00-59)",
"Second (00-59)": "秒(00-59)",
"Show Source": "ソース表示",
"No content to preview": "プレビューするコンテンツがありません",
"Export": "エクスポート",
"Set Timeout": "タイムアウト設定",
"Select Voice": "音声選択",
"Toggle Sticky Bottom TTS Bar": "TTSバー固定切替",
"Display what I'm reading on Discord": "読書中の本をDiscordに表示",
"Show on Discord": "Discordに表示",
"Instant {{action}}": "インスタント{{action}}",
"Instant {{action}} Disabled": "インスタント{{action}}を無効化",
"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": "章を選択",
"{{number}} WPM": "{{number}} WPM",
"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の位置に戻る"
}
@@ -173,9 +173,6 @@
"Token": "토큰",
"Your OTP token": "당신의 OTP 토큰",
"Verify token": "토큰 확인",
"Sign in with Google": "Google로 로그인",
"Sign in with Apple": "Apple로 로그인",
"Sign in with GitHub": "GitHub로 로그인",
"Account": "계정",
"Failed to delete user. Please try again later.": "사용자 삭제에 실패했습니다. 나중에 다시 시도해 주세요.",
"Community Support": "커뮤니티 지원",
@@ -337,8 +334,8 @@
"Reset Settings": "설정 재설정",
"{{count}} pages left in chapter_other": "남은 페이지 {{count}}장",
"Show Remaining Pages": "남은 페이지 보기",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "구독 관리",
"Coming Soon": "곧 출시 예정",
@@ -825,6 +822,202 @@
"Show Results": "결과 표시",
"Clear search": "검색 지우기",
"Clear search history": "검색 기록 지우기",
"Quick action disabled": "빠른 작업 비활성화됨",
"Tap to Toggle Footer": "탭하여 바닥글 전환"
"Tap to Toggle Footer": "탭하여 바닥글 전환",
"Exported successfully": "내보내기 성공",
"Book exported successfully.": "책을 성공적으로 내보냈습니다.",
"Failed to export the book.": "책 내보내기에 실패했습니다.",
"Export Book": "책 내보내기",
"Whole word:": "전체 단어:",
"Error": "오류",
"Unable to load the article. Try searching directly on {{link}}.": "문서를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요.",
"Unable to load the word. Try searching directly on {{link}}.": "단어를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요.",
"Date Published": "출판일",
"Only for TTS:": "TTS 전용:",
"Uploaded": "업로드됨",
"Downloaded": "다운로드됨",
"Deleted": "삭제됨",
"Note:": "노트:",
"Time:": "시간:",
"Format Options": "형식 옵션",
"Export Date": "내보내기 날짜",
"Chapter Titles": "챕터 제목",
"Chapter Separator": "챕터 구분 기호",
"Highlights": "하이라이트",
"Note Date": "노트 날짜",
"Advanced": "고급",
"Hide": "숨기기",
"Show": "표시",
"Use Custom Template": "사용자 지정 템플릿 사용",
"Export Template": "내보내기 템플릿",
"Template Syntax:": "템플릿 구문:",
"Insert value": "값 삽입",
"Format date (locale)": "날짜 형식 (로케일)",
"Format date (custom)": "날짜 형식 (사용자 지정)",
"Conditional": "조건부",
"Loop": "반복",
"Available Variables:": "사용 가능한 변수:",
"Book title": "책 제목",
"Book author": "책 저자",
"Export date": "내보내기 날짜",
"Array of chapters": "챕터 배열",
"Chapter title": "챕터 제목",
"Array of annotations": "주석 배열",
"Highlighted text": "하이라이트된 텍스트",
"Annotation note": "주석 노트",
"Date Format Tokens:": "날짜 형식 토큰:",
"Year (4 digits)": "연도 (4자리)",
"Month (01-12)": "월 (01-12)",
"Day (01-31)": "일 (01-31)",
"Hour (00-23)": "시 (00-23)",
"Minute (00-59)": "분 (00-59)",
"Second (00-59)": "초 (00-59)",
"Show Source": "소스 표시",
"No content to preview": "미리 볼 내용 없음",
"Export": "내보내기",
"Set Timeout": "시간 제한 설정",
"Select Voice": "음성 선택",
"Toggle Sticky Bottom TTS Bar": "고정 TTS 바 전환",
"Display what I'm reading on Discord": "Discord에 읽는 책 표시",
"Show on Discord": "Discord에 표시",
"Instant {{action}}": "즉시 {{action}}",
"Instant {{action}} Disabled": "즉시 {{action}} 비활성화",
"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": "챕터 선택",
"{{number}} WPM": "{{number}} WPM",
"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 위치로 돌아가기"
}
@@ -25,9 +25,6 @@
"Your OTP token": "Token OTP anda",
"Verify token": "Sahkan token",
"Go Back": "Kembali",
"Sign in with Google": "Log masuk dengan Google",
"Sign in with Apple": "Log masuk dengan Apple",
"Sign in with GitHub": "Log masuk dengan GitHub",
"New Password": "Kata Laluan Baharu",
"Your new password": "Kata laluan baharu anda",
"Update password": "Kemas kini kata laluan",
@@ -649,8 +646,8 @@
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"Source Han Serif CN": "Source Han Serif CN",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Google Books": "Google Books",
"Open Library": "Open Library",
@@ -825,6 +822,202 @@
"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"
"Tap to Toggle Footer": "Ketik untuk togol pengaki",
"Exported successfully": "Berjaya dieksport",
"Book exported successfully.": "Buku berjaya dieksport.",
"Failed to export the book.": "Gagal mengeksport buku.",
"Export Book": "Eksport Buku",
"Whole word:": "Perkataan penuh:",
"Error": "Ralat",
"Unable to load the article. Try searching directly on {{link}}.": "Tidak dapat memuatkan artikel. Cuba cari terus di {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Tidak dapat memuatkan perkataan. Cuba cari terus di {{link}}.",
"Date Published": "Tarikh diterbitkan",
"Only for TTS:": "Hanya untuk TTS:",
"Uploaded": "Dimuat naik",
"Downloaded": "Dimuat turun",
"Deleted": "Dipadam",
"Note:": "Nota:",
"Time:": "Masa:",
"Format Options": "Pilihan Format",
"Export Date": "Tarikh Eksport",
"Chapter Titles": "Tajuk Bab",
"Chapter Separator": "Pemisah Bab",
"Highlights": "Serlahan",
"Note Date": "Tarikh Nota",
"Advanced": "Lanjutan",
"Hide": "Sembunyikan",
"Show": "Tunjukkan",
"Use Custom Template": "Gunakan Templat Tersuai",
"Export Template": "Templat Eksport",
"Template Syntax:": "Sintaks Templat:",
"Insert value": "Masukkan nilai",
"Format date (locale)": "Format tarikh (tempatan)",
"Format date (custom)": "Format tarikh (tersuai)",
"Conditional": "Bersyarat",
"Loop": "Gelung",
"Available Variables:": "Pembolehubah Tersedia:",
"Book title": "Tajuk buku",
"Book author": "Pengarang buku",
"Export date": "Tarikh eksport",
"Array of chapters": "Senarai bab",
"Chapter title": "Tajuk bab",
"Array of annotations": "Senarai anotasi",
"Highlighted text": "Teks yang diserlahkan",
"Annotation note": "Nota anotasi",
"Date Format Tokens:": "Token Format Tarikh:",
"Year (4 digits)": "Tahun (4 digit)",
"Month (01-12)": "Bulan (01-12)",
"Day (01-31)": "Hari (01-31)",
"Hour (00-23)": "Jam (00-23)",
"Minute (00-59)": "Minit (00-59)",
"Second (00-59)": "Saat (00-59)",
"Show Source": "Tunjukkan Sumber",
"No content to preview": "Tiada kandungan untuk pratonton",
"Export": "Eksport",
"Set Timeout": "Tetapkan masa tamat",
"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",
"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",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -23,9 +23,6 @@
"Token": "Token",
"Your OTP token": "Uw OTP-token",
"Verify token": "Token verifiëren",
"Sign in with Google": "Inloggen met Google",
"Sign in with Apple": "Inloggen met Apple",
"Sign in with GitHub": "Inloggen met GitHub",
"New Password": "Nieuw wachtwoord",
"Your new password": "Uw nieuwe wachtwoord",
"Update password": "Wachtwoord bijwerken",
@@ -341,8 +338,8 @@
"{{count}} pages left in chapter_one": "Nog {{count}} pagina in hoofdstuk",
"{{count}} pages left in chapter_other": "Nog {{count}} paginas in hoofdstuk",
"Show Remaining Pages": "Toon resterende paginas",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Abonnement beheren",
"Coming Soon": "Binnenkort beschikbaar",
@@ -835,6 +832,204 @@
"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"
"Tap to Toggle Footer": "Tik om voettekst te wisselen",
"Exported successfully": "Succesvol geëxporteerd",
"Book exported successfully.": "Boek succesvol geëxporteerd.",
"Failed to export the book.": "Exporteren van het boek mislukt.",
"Export Book": "Boek exporteren",
"Whole word:": "Heel woord:",
"Error": "Fout",
"Unable to load the article. Try searching directly on {{link}}.": "Kan het artikel niet laden. Probeer direct te zoeken op {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Kan het woord niet laden. Probeer direct te zoeken op {{link}}.",
"Date Published": "Publicatiedatum",
"Only for TTS:": "Alleen voor TTS:",
"Uploaded": "Geüpload",
"Downloaded": "Gedownload",
"Deleted": "Verwijderd",
"Note:": "Notitie:",
"Time:": "Tijd:",
"Format Options": "Formaatopties",
"Export Date": "Exportdatum",
"Chapter Titles": "Hoofdstuktitels",
"Chapter Separator": "Hoofdstukscheiding",
"Highlights": "Markeringen",
"Note Date": "Notitiedatum",
"Advanced": "Geavanceerd",
"Hide": "Verbergen",
"Show": "Tonen",
"Use Custom Template": "Aangepast sjabloon gebruiken",
"Export Template": "Exportsjabloon",
"Template Syntax:": "Sjabloonsyntaxis:",
"Insert value": "Waarde invoegen",
"Format date (locale)": "Datum opmaken (lokaal)",
"Format date (custom)": "Datum opmaken (aangepast)",
"Conditional": "Voorwaardelijk",
"Loop": "Lus",
"Available Variables:": "Beschikbare variabelen:",
"Book title": "Boektitel",
"Book author": "Boekauteur",
"Export date": "Exportdatum",
"Array of chapters": "Reeks hoofdstukken",
"Chapter title": "Hoofdstuktitel",
"Array of annotations": "Reeks annotaties",
"Highlighted text": "Gemarkeerde tekst",
"Annotation note": "Annotatienotitie",
"Date Format Tokens:": "Datumformaattokens:",
"Year (4 digits)": "Jaar (4 cijfers)",
"Month (01-12)": "Maand (01-12)",
"Day (01-31)": "Dag (01-31)",
"Hour (00-23)": "Uur (00-23)",
"Minute (00-59)": "Minuut (00-59)",
"Second (00-59)": "Seconde (00-59)",
"Show Source": "Bron tonen",
"No content to preview": "Geen inhoud om te bekijken",
"Export": "Exporteren",
"Set Timeout": "Time-out instellen",
"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",
"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",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Token",
"Your OTP token": "Twój OTP token",
"Verify token": "Weryfikacja tokena",
"Sign in with Google": "Zaloguj się za pomocą Google",
"Sign in with Apple": "Zaloguj się za pomocą Apple",
"Sign in with GitHub": "Zaloguj się za pomocą GitHub",
"Account": "Konto",
"Failed to delete user. Please try again later.": "Nie udało się usunąć użytkownika. Spróbuj ponownie później.",
"Community Support": "Wsparcie społeczności",
@@ -349,8 +346,8 @@
"{{count}} pages left in chapter_many": "Pozostało {{count}} stron w rozdziale",
"{{count}} pages left in chapter_other": "Pozostało {{count}} stron w rozdziale",
"Show Remaining Pages": "Pokaż pozostałe strony",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Zarządzaj subskrypcją",
"Coming Soon": "Wkrótce dostępne",
@@ -855,6 +852,208 @@
"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ę"
"Tap to Toggle Footer": "Dotknij, aby przełączyć stopkę",
"Exported successfully": "Wyeksportowano pomyślnie",
"Book exported successfully.": "Książka została wyeksportowana pomyślnie.",
"Failed to export the book.": "Nie udało się wyeksportować książki.",
"Export Book": "Eksportuj książkę",
"Whole word:": "Całe słowo:",
"Error": "Błąd",
"Unable to load the article. Try searching directly on {{link}}.": "Nie można załadować artykułu. Spróbuj wyszukać bezpośrednio na {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Nie można załadować słowa. Spróbuj wyszukać bezpośrednio na {{link}}.",
"Date Published": "Data wydania",
"Only for TTS:": "Tylko dla TTS:",
"Uploaded": "Przesłano",
"Downloaded": "Pobrano",
"Deleted": "Usunięto",
"Note:": "Notatka:",
"Time:": "Czas:",
"Format Options": "Opcje formatu",
"Export Date": "Data eksportu",
"Chapter Titles": "Tytuły rozdziałów",
"Chapter Separator": "Separator rozdziałów",
"Highlights": "Zaznaczenia",
"Note Date": "Data notatki",
"Advanced": "Zaawansowane",
"Hide": "Ukryj",
"Show": "Pokaż",
"Use Custom Template": "Użyj własnego szablonu",
"Export Template": "Szablon eksportu",
"Template Syntax:": "Składnia szablonu:",
"Insert value": "Wstaw wartość",
"Format date (locale)": "Formatuj datę (lokalizacja)",
"Format date (custom)": "Formatuj datę (własny)",
"Conditional": "Warunkowy",
"Loop": "Pętla",
"Available Variables:": "Dostępne zmienne:",
"Book title": "Tytuł książki",
"Book author": "Autor książki",
"Export date": "Data eksportu",
"Array of chapters": "Tablica rozdziałów",
"Chapter title": "Tytuł rozdziału",
"Array of annotations": "Tablica adnotacji",
"Highlighted text": "Zaznaczony tekst",
"Annotation note": "Notatka adnotacji",
"Date Format Tokens:": "Tokeny formatu daty:",
"Year (4 digits)": "Rok (4 cyfry)",
"Month (01-12)": "Miesiąc (01-12)",
"Day (01-31)": "Dzień (01-31)",
"Hour (00-23)": "Godzina (00-23)",
"Minute (00-59)": "Minuta (00-59)",
"Second (00-59)": "Sekunda (00-59)",
"Show Source": "Pokaż źródło",
"No content to preview": "Brak treści do podglądu",
"Export": "Eksportuj",
"Set Timeout": "Ustaw limit czasu",
"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",
"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ł",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Token",
"Your OTP token": "Seu token OTP",
"Verify token": "Verificar token",
"Sign in with Google": "Entrar com o Google",
"Sign in with Apple": "Entrar com o Apple",
"Sign in with GitHub": "Entrar com o GitHub",
"Account": "Conta",
"Failed to delete user. Please try again later.": "Falha ao excluir usuário. Por favor, tente novamente mais tarde.",
"Community Support": "Suporte da comunidade",
@@ -345,8 +342,8 @@
"{{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",
"Show Remaining Pages": "Mostrar páginas restantes",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gerenciar assinatura",
"Coming Soon": "Em breve",
@@ -845,6 +842,206 @@
"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é"
"Tap to Toggle Footer": "Toque para alternar rodapé",
"Exported successfully": "Exportado com sucesso",
"Book exported successfully.": "Livro exportado com sucesso.",
"Failed to export the book.": "Falha ao exportar o livro.",
"Export Book": "Exportar livro",
"Whole word:": "Palavra inteira:",
"Error": "Erro",
"Unable to load the article. Try searching directly on {{link}}.": "Não foi possível carregar o artigo. Tente pesquisar diretamente em {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Não foi possível carregar a palavra. Tente pesquisar diretamente em {{link}}.",
"Date Published": "Data de publicação",
"Only for TTS:": "Apenas para TTS:",
"Uploaded": "Enviado",
"Downloaded": "Baixado",
"Deleted": "Excluído",
"Note:": "Nota:",
"Time:": "Hora:",
"Format Options": "Opções de formato",
"Export Date": "Data de exportação",
"Chapter Titles": "Títulos dos capítulos",
"Chapter Separator": "Separador de capítulos",
"Highlights": "Destaques",
"Note Date": "Data da nota",
"Advanced": "Avançado",
"Hide": "Ocultar",
"Show": "Mostrar",
"Use Custom Template": "Usar modelo personalizado",
"Export Template": "Modelo de exportação",
"Template Syntax:": "Sintaxe do modelo:",
"Insert value": "Inserir valor",
"Format date (locale)": "Formatar data (local)",
"Format date (custom)": "Formatar data (personalizado)",
"Conditional": "Condicional",
"Loop": "Loop",
"Available Variables:": "Variáveis disponíveis:",
"Book title": "Título do livro",
"Book author": "Autor do livro",
"Export date": "Data de exportação",
"Array of chapters": "Lista de capítulos",
"Chapter title": "Título do capítulo",
"Array of annotations": "Lista de anotações",
"Highlighted text": "Texto destacado",
"Annotation note": "Nota de anotação",
"Date Format Tokens:": "Tokens de formato de data:",
"Year (4 digits)": "Ano (4 dígitos)",
"Month (01-12)": "Mês (01-12)",
"Day (01-31)": "Dia (01-31)",
"Hour (00-23)": "Hora (00-23)",
"Minute (00-59)": "Minuto (00-59)",
"Second (00-59)": "Segundo (00-59)",
"Show Source": "Mostrar fonte",
"No content to preview": "Sem conteúdo para visualizar",
"Export": "Exportar",
"Set Timeout": "Definir tempo limite",
"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",
"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",
"{{number}} WPM": "{{number}} PPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Токен",
"Your OTP token": "Ваш OTP токен",
"Verify token": "Проверить токен",
"Sign in with Google": "Войти через Google",
"Sign in with Apple": "Войти через Apple",
"Sign in with GitHub": "Войти через GitHub",
"Account": "Аккаунт",
"Failed to delete user. Please try again later.": "Не удалось удалить пользователя. Пожалуйста, повторите попытку позже.",
"Community Support": "Поддержка сообщества",
@@ -349,8 +346,8 @@
"{{count}} pages left in chapter_many": "Осталось {{count}} страниц в главе",
"{{count}} pages left in chapter_other": "Осталось {{count}} страниц в главе",
"Show Remaining Pages": "Показать оставшиеся страницы",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Управление подпиской",
"Coming Soon": "Скоро будет",
@@ -855,6 +852,208 @@
"Show Results": "Показать результаты",
"Clear search": "Очистить поиск",
"Clear search history": "Очистить историю поиска",
"Quick action disabled": "Быстрое действие отключено",
"Tap to Toggle Footer": "Нажмите для переключения нижнего колонтитула"
"Tap to Toggle Footer": "Нажмите для переключения нижнего колонтитула",
"Exported successfully": "Успешно экспортировано",
"Book exported successfully.": "Книга успешно экспортирована.",
"Failed to export the book.": "Не удалось экспортировать книгу.",
"Export Book": "Экспорт книги",
"Whole word:": "Слово целиком:",
"Error": "Ошибка",
"Unable to load the article. Try searching directly on {{link}}.": "Не удалось загрузить статью. Попробуйте искать напрямую на {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Не удалось загрузить слово. Попробуйте искать напрямую на {{link}}.",
"Date Published": "Дата публикации",
"Only for TTS:": "Только для TTS:",
"Uploaded": "Загружено",
"Downloaded": "Скачано",
"Deleted": "Удалено",
"Note:": "Примечание:",
"Time:": "Время:",
"Format Options": "Параметры формата",
"Export Date": "Дата экспорта",
"Chapter Titles": "Названия глав",
"Chapter Separator": "Разделитель глав",
"Highlights": "Выделения",
"Note Date": "Дата примечания",
"Advanced": "Дополнительно",
"Hide": "Скрыть",
"Show": "Показать",
"Use Custom Template": "Использовать свой шаблон",
"Export Template": "Шаблон экспорта",
"Template Syntax:": "Синтаксис шаблона:",
"Insert value": "Вставить значение",
"Format date (locale)": "Форматировать дату (локаль)",
"Format date (custom)": "Форматировать дату (свой)",
"Conditional": "Условный",
"Loop": "Цикл",
"Available Variables:": "Доступные переменные:",
"Book title": "Название книги",
"Book author": "Автор книги",
"Export date": "Дата экспорта",
"Array of chapters": "Массив глав",
"Chapter title": "Название главы",
"Array of annotations": "Массив примечаний",
"Highlighted text": "Выделенный текст",
"Annotation note": "Примечание аннотации",
"Date Format Tokens:": "Токены формата даты:",
"Year (4 digits)": "Год (4 цифры)",
"Month (01-12)": "Месяц (01-12)",
"Day (01-31)": "День (01-31)",
"Hour (00-23)": "Час (00-23)",
"Minute (00-59)": "Минута (00-59)",
"Second (00-59)": "Секунда (00-59)",
"Show Source": "Показать источник",
"No content to preview": "Нет содержимого для просмотра",
"Export": "Экспортировать",
"Set Timeout": "Установить тайм-аут",
"Select Voice": "Выбрать голос",
"Toggle Sticky Bottom TTS Bar": "Переключить закреплённую панель TTS",
"Display what I'm reading on Discord": "Показывать книгу в Discord",
"Show on Discord": "Показать в Discord",
"Instant {{action}}": "Мгновенное {{action}}",
"Instant {{action}} Disabled": "Мгновенное {{action}} отключено",
"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": "Выбрать главу",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -24,9 +24,6 @@
"Token": "ටෝකනය",
"Your OTP token": "ඔබේ OTP ටෝකනය",
"Verify token": "ටෝකනය තහවුරු කරන්න",
"Sign in with Google": "Google සමඟ ඇතුල් වන්න",
"Sign in with Apple": "Apple සමඟ ඇතුල් වන්න",
"Sign in with GitHub": "GitHub සමඟ ඇතුල් වන්න",
"New Password": "නව මුරපදය",
"Your new password": "ඔබේ නව මුරපදය",
"Update password": "මුරපදය යාවත්කාලීන කරන්න",
@@ -488,8 +485,8 @@
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"Source Han Serif CN": "Source Han Serif CN",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Google Books": "Google Books",
"Open Library": "Open Library",
@@ -835,6 +832,204 @@
"Show Results": "ප්‍රතිඵල පෙන්වන්න",
"Clear search": "සෙවුම හිස් කරන්න",
"Clear search history": "සෙවුම් ඉතිහාසය හිස් කරන්න",
"Quick action disabled": "ඉක්මන් ක්‍රියාව අක්‍රීයයි",
"Tap to Toggle Footer": "පාදකය ටොගල් කිරීමට තට්ටු කරන්න"
"Tap to Toggle Footer": "පාදකය ටොගල් කිරීමට තට්ටු කරන්න",
"Exported successfully": "සාර්ථකව අපනයනය කරන ලදී",
"Book exported successfully.": "පොත සාර්ථකව අපනයනය කරන ලදී.",
"Failed to export the book.": "පොත අපනයනය කිරීමට අසමත් විය.",
"Export Book": "පොත අපනයනය කරන්න",
"Whole word:": "සම්පූර්ණ වචනය:",
"Error": "දෝෂය",
"Unable to load the article. Try searching directly on {{link}}.": "ලිපිය පූරණය කළ නොහැක. {{link}} හි කෙලින්ම සෙවීමට උත්සාහ කරන්න.",
"Unable to load the word. Try searching directly on {{link}}.": "වචනය පූරණය කළ නොහැක. {{link}} හි කෙලින්ම සෙවීමට උත්සාහ කරන්න.",
"Date Published": "ප්‍රකාශන දිනය",
"Only for TTS:": "TTS සඳහා පමණක්:",
"Uploaded": "උඩුගත කරන ලදී",
"Downloaded": "බාගත කරන ලදී",
"Deleted": "මකා දමන ලදී",
"Note:": "සටහන:",
"Time:": "කාලය:",
"Format Options": "ආකෘති විකල්ප",
"Export Date": "අපනයන දිනය",
"Chapter Titles": "කොටස් මාතෘකා",
"Chapter Separator": "කොටස් වෙන්කරු",
"Highlights": "ඉස්මතු කිරීම්",
"Note Date": "සටහන දිනය",
"Advanced": "උසස්",
"Hide": "සඟවන්න",
"Show": "පෙන්වන්න",
"Use Custom Template": "අභිරුචි සැකිල්ල භාවිතා කරන්න",
"Export Template": "අපනයන සැකිල්ල",
"Template Syntax:": "සැකිල්ලේ ව්‍යවස්ථාව:",
"Insert value": "වටිනාකම ඇතුළත් කරන්න",
"Format date (locale)": "දිනය ආකෘතිය (දේශීය)",
"Format date (custom)": "අභිරුචි දිනය ආකෘතිය",
"Conditional": "ශර්තමය",
"Loop": "ලූප්",
"Available Variables:": "ලබා ගත හැකි විචල්‍ය:",
"Book title": "පොත් මාතෘකාව",
"Book author": "පොත් රචකයා",
"Export date": "අපනයන දිනය",
"Array of chapters": "කොටස් අරය",
"Chapter title": "කොටස් මාතෘකාව",
"Array of annotations": "අනුසටහන් අරය",
"Highlighted text": "ඉස්මතු කළ පෙළ",
"Annotation note": "අනුසටහන් සටහන",
"Date Format Tokens:": "දිනය ආකෘති ටෝකන:",
"Year (4 digits)": "වසර (අංක 4)",
"Month (01-12)": "මාසය (01-12)",
"Day (01-31)": "දින (01-31)",
"Hour (00-23)": "පැය (00-23)",
"Minute (00-59)": "මිනිත්තු (00-59)",
"Second (00-59)": "තත්පර (00-59)",
"Show Source": "මූලාශ්‍රය පෙන්වන්න",
"No content to preview": "පෙරදසුන සඳහා අන්තර්ගතයක් නැත",
"Export": "අපනයනය කරන්න",
"Set Timeout": "කාල සීමාව සකසන්න",
"Select Voice": "හඬ තෝරන්න",
"Toggle Sticky Bottom TTS Bar": "ඇලවූ TTS තීරුව මාරු කරන්න",
"Display what I'm reading on Discord": "Discord හි කියවන පොත පෙන්වන්න",
"Show on Discord": "Discord හි පෙන්වන්න",
"Instant {{action}}": "ක්ෂණික {{action}}",
"Instant {{action}} Disabled": "ක්ෂණික {{action}} අබල කර ඇත",
"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": "පරිච්ඡේදය තෝරන්න",
"{{number}} WPM": "{{number}} WPM",
"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 ස්ථානයට ආපසු යන්න"
}
@@ -25,9 +25,6 @@
"Your OTP token": "Din OTP-token",
"Verify token": "Verifiera token",
"Go Back": "Tillbaka",
"Sign in with Google": "Logga in med Google",
"Sign in with Apple": "Logga in med Apple",
"Sign in with GitHub": "Logga in med GitHub",
"New Password": "Nytt lösenord",
"Your new password": "Ditt nya lösenord",
"Update password": "Uppdatera lösenord",
@@ -573,8 +570,8 @@
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"Source Han Serif CN": "Source Han Serif CN",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Google Books": "Google Böcker",
"Open Library": "Open Library",
@@ -835,6 +832,204 @@
"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"
"Tap to Toggle Footer": "Tryck för att växla sidfot",
"Exported successfully": "Exporterad",
"Book exported successfully.": "Boken har exporterats.",
"Failed to export the book.": "Det gick inte att exportera boken.",
"Export Book": "Exportera bok",
"Whole word:": "Hela ordet:",
"Error": "Fel",
"Unable to load the article. Try searching directly on {{link}}.": "Kan inte ladda artikeln. Försök söka direkt på {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Kan inte ladda ordet. Försök söka direkt på {{link}}.",
"Date Published": "Publiceringsdatum",
"Only for TTS:": "Endast för TTS:",
"Uploaded": "Uppladdad",
"Downloaded": "Nedladdad",
"Deleted": "Borttagen",
"Note:": "Anteckning:",
"Time:": "Tid:",
"Format Options": "Formatalternativ",
"Export Date": "Exportdatum",
"Chapter Titles": "Kapiteltitlar",
"Chapter Separator": "Kapitelavgränsare",
"Highlights": "Markeringar",
"Note Date": "Anteckningsdatum",
"Advanced": "Avancerat",
"Hide": "Dölj",
"Show": "Visa",
"Use Custom Template": "Använd anpassad mall",
"Export Template": "Exportmall",
"Template Syntax:": "Mallsyntax:",
"Insert value": "Infoga värde",
"Format date (locale)": "Formatera datum (lokal)",
"Format date (custom)": "Formatera datum (anpassad)",
"Conditional": "Villkorlig",
"Loop": "Loop",
"Available Variables:": "Tillgängliga variabler:",
"Book title": "Boktitel",
"Book author": "Bokförfattare",
"Export date": "Exportdatum",
"Array of chapters": "Lista med kapitel",
"Chapter title": "Kapiteltitel",
"Array of annotations": "Lista med anteckningar",
"Highlighted text": "Markerad text",
"Annotation note": "Anteckningsnotering",
"Date Format Tokens:": "Datumformattecken:",
"Year (4 digits)": "År (4 siffror)",
"Month (01-12)": "Månad (01-12)",
"Day (01-31)": "Dag (01-31)",
"Hour (00-23)": "Timme (00-23)",
"Minute (00-59)": "Minut (00-59)",
"Second (00-59)": "Sekund (00-59)",
"Show Source": "Visa källa",
"No content to preview": "Inget innehåll att förhandsgranska",
"Export": "Exportera",
"Set Timeout": "Ställ in timeout",
"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",
"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",
"{{number}} WPM": "{{number}} ord/min",
"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"
}
@@ -24,9 +24,6 @@
"Token": "டோக்கன்",
"Your OTP token": "உங்கள் OTP டோக்கன்",
"Verify token": "டோக்கனை சரிபார்க்கவும்",
"Sign in with Google": "Google மூலம் உள்நுழையவும்",
"Sign in with Apple": "Apple மூலம் உள்நுழையவும்",
"Sign in with GitHub": "GitHub மூலம் உள்நுழையவும்",
"New Password": "புதிய கடவுச்சொல்",
"Your new password": "உங்கள் புதிய கடவுச்சொல்",
"Update password": "கடவுச்சொல் புதுப்பிக்கவும்",
@@ -488,8 +485,8 @@
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"Source Han Serif CN": "Source Han Serif CN",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Google Books": "Google Books",
"Open Library": "Open Library",
@@ -835,6 +832,204 @@
"Show Results": "முடிவுகளைக் காட்டு",
"Clear search": "தேடலை அழி",
"Clear search history": "தேடல் வரலாற்றை அழி",
"Quick action disabled": "விரைவு செயல் முடக்கப்பட்டது",
"Tap to Toggle Footer": "அடிக்குறிப்பை மாற்ற தட்டவும்"
"Tap to Toggle Footer": "அடிக்குறிப்பை மாற்ற தட்டவும்",
"Exported successfully": "ஏற்றுமதி வெற்றி",
"Book exported successfully.": "புத்தகம் வெற்றிகரமாக ஏற்றுமதி செய்யப்பட்டது.",
"Failed to export the book.": "புத்தகத்தை ஏற்றுமதி செய்ய இயலவில்லை.",
"Export Book": "புத்தகத்தை ஏற்றுமதி செய்",
"Whole word:": "முழு சொல்:",
"Error": "பிழை",
"Unable to load the article. Try searching directly on {{link}}.": "கட்டுரையை ஏற்ற முடியவில்லை. நேரடியாக {{link}} இல் தேடவும்.",
"Unable to load the word. Try searching directly on {{link}}.": "சொல்லை ஏற்ற முடியவில்லை. நேரடியாக {{link}} இல் தேடவும்.",
"Date Published": "வெளியீட்டு தேதி",
"Only for TTS:": "TTS க்கு மட்டும்:",
"Uploaded": "பதிவேற்றப்பட்டது",
"Downloaded": "பதிவிறக்கப்பட்டது",
"Deleted": "நீக்கப்பட்டது",
"Note:": "குறிப்பு:",
"Time:": "நேரம்:",
"Format Options": "வடிவமைப்பு விருப்பங்கள்",
"Export Date": "ஏற்றுமதி தேதி",
"Chapter Titles": "அத்தியாயத் தலைப்புகள்",
"Chapter Separator": "அத்தியாய பிரிப்பான்",
"Highlights": "சிறப்பம்சங்கள்",
"Note Date": "குறிப்பு தேதி",
"Advanced": "மேம்பட்ட",
"Hide": "மறை",
"Show": "காட்டு",
"Use Custom Template": "தனிப்பயன் வார்ப்புரு பயன்படுத்து",
"Export Template": "ஏற்றுமதி வார்ப்புரு",
"Template Syntax:": "வார்ப்புரு தொடரியல்:",
"Insert value": "மதிப்பு செருகு",
"Format date (locale)": "தேதி வடிவமைப்பு (உள்ளூர்)",
"Format date (custom)": "தேதி வடிவமைப்பு (தனிப்பயன்)",
"Conditional": "நிபந்தனை",
"Loop": "சுழற்சி",
"Available Variables:": "கிடைக்கும் மாறிகள்:",
"Book title": "புத்தகத் தலைப்பு",
"Book author": "புத்தக ஆசிரியர்",
"Export date": "ஏற்றுமதி தேதி",
"Array of chapters": "அத்தியாயங்களின் பட்டியல்",
"Chapter title": "அத்தியாய தலைப்பு",
"Array of annotations": "குறிப்புகளின் பட்டியல்",
"Highlighted text": "முன்னிலைப்படுத்தப்பட்ட உரை",
"Annotation note": "குறிப்பு குறிப்பு",
"Date Format Tokens:": "தேதி வடிவமைப்பு டோக்கன்கள்:",
"Year (4 digits)": "ஆண்டு (4 இலக்கங்கள்)",
"Month (01-12)": "மாதம் (01-12)",
"Day (01-31)": "நாள் (01-31)",
"Hour (00-23)": "மணி (00-23)",
"Minute (00-59)": "நிமிடம் (00-59)",
"Second (00-59)": "விநாடி (00-59)",
"Show Source": "மூலத்தைக் காட்டு",
"No content to preview": "முன்னோட்டத்திற்கு உள்ளடக்கம் இல்லை",
"Export": "ஏற்றுமதி",
"Set Timeout": "நேர வரம்பை அமைக்கவும்",
"Select Voice": "குரலைத் தேர்ந்தெடுக்கவும்",
"Toggle Sticky Bottom TTS Bar": "நிலையான TTS பட்டியை மாற்றவும்",
"Display what I'm reading on Discord": "Discord இல் படிக்கும் புத்தகத்தை காட்டு",
"Show on Discord": "Discord இல் காட்டு",
"Instant {{action}}": "உடனடி {{action}}",
"Instant {{action}} Disabled": "உடனடி {{action}} முடக்கப்பட்டது",
"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": "அத்தியாயத்தைத் தேர்ந்தெடு",
"{{number}} WPM": "{{number}} WPM",
"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 இடத்திற்குத் திரும்பு"
}
@@ -23,9 +23,6 @@
"Token": "รหัสยืนยัน",
"Your OTP token": "รหัส OTP ของคุณ",
"Verify token": "ยืนยันรหัส",
"Sign in with Google": "เข้าสู่ระบบด้วย Google",
"Sign in with Apple": "เข้าสู่ระบบด้วย Apple",
"Sign in with GitHub": "เข้าสู่ระบบด้วย GitHub",
"New Password": "รหัสผ่านใหม่",
"Your new password": "รหัสผ่านใหม่ของคุณ",
"Update password": "อัปเดตรหัสผ่าน",
@@ -379,8 +376,8 @@
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"Source Han Serif CN": "Source Han Serif CN",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Azure Translator": "Azure Translator",
"DeepL": "DeepL",
@@ -825,6 +822,202 @@
"Show Results": "แสดงผลลัพธ์",
"Clear search": "ล้างการค้นหา",
"Clear search history": "ล้างประวัติการค้นหา",
"Quick action disabled": "ปิดใช้งานการดำเนินการด่วน",
"Tap to Toggle Footer": "แตะเพื่อสลับส่วนท้าย"
"Tap to Toggle Footer": "แตะเพื่อสลับส่วนท้าย",
"Exported successfully": "ส่งออกสำเร็จ",
"Book exported successfully.": "ส่งออกหนังสือสำเร็จ",
"Failed to export the book.": "ส่งออกหนังสือล้มเหลว",
"Export Book": "ส่งออกหนังสือ",
"Whole word:": "คำทั้งคำ:",
"Error": "ข้อผิดพลาด",
"Unable to load the article. Try searching directly on {{link}}.": "ไม่สามารถโหลดบทความได้ ลองค้นหาโดยตรงที่ {{link}}",
"Unable to load the word. Try searching directly on {{link}}.": "ไม่สามารถโหลดคำได้ ลองค้นหาโดยตรงที่ {{link}}",
"Date Published": "วันที่เผยแพร่",
"Only for TTS:": "สำหรับ TTS เท่านั้น:",
"Uploaded": "อัปโหลดแล้ว",
"Downloaded": "ดาวน์โหลดแล้ว",
"Deleted": "ลบแล้ว",
"Note:": "หมายเหตุ:",
"Time:": "เวลา:",
"Format Options": "ตัวเลือกรูปแบบ",
"Export Date": "วันที่ส่งออก",
"Chapter Titles": "ชื่อบท",
"Chapter Separator": "ตัวคั่นบท",
"Highlights": "ไฮไลต์",
"Note Date": "วันที่หมายเหตุ",
"Advanced": "ขั้นสูง",
"Hide": "ซ่อน",
"Show": "แสดง",
"Use Custom Template": "ใช้เทมเพลตที่กำหนดเอง",
"Export Template": "เทมเพลตส่งออก",
"Template Syntax:": "ไวยากรณ์เทมเพลต:",
"Insert value": "แทรกค่า",
"Format date (locale)": "จัดรูปแบบวันที่ (ท้องถิ่น)",
"Format date (custom)": "จัดรูปแบบวันที่ (กำหนดเอง)",
"Conditional": "เงื่อนไข",
"Loop": "ลูป",
"Available Variables:": "ตัวแปรที่พร้อมใช้งาน:",
"Book title": "ชื่อหนังสือ",
"Book author": "ผู้แต่งหนังสือ",
"Export date": "วันที่ส่งออก",
"Array of chapters": "อาร์เรย์ของบท",
"Chapter title": "ชื่อบท",
"Array of annotations": "อาร์เรย์ของคำอธิบาย",
"Highlighted text": "ข้อความที่ไฮไลต์",
"Annotation note": "หมายเหตุคำอธิบาย",
"Date Format Tokens:": "โทเค็นรูปแบบวันที่:",
"Year (4 digits)": "ปี (4 หลัก)",
"Month (01-12)": "เดือน (01-12)",
"Day (01-31)": "วัน (01-31)",
"Hour (00-23)": "ชั่วโมง (00-23)",
"Minute (00-59)": "นาที (00-59)",
"Second (00-59)": "วินาที (00-59)",
"Show Source": "แสดงซอร์ส",
"No content to preview": "ไม่มีเนื้อหาให้แสดงตัวอย่าง",
"Export": "ส่งออก",
"Set Timeout": "ตั้งค่าหมดเวลา",
"Select Voice": "เลือกเสียง",
"Toggle Sticky Bottom TTS Bar": "สลับแถบ TTS แบบติด",
"Display what I'm reading on Discord": "แสดงหนังสือที่กำลังอ่านบน Discord",
"Show on Discord": "แสดงบน Discord",
"Instant {{action}}": "{{action}}ทันที",
"Instant {{action}} Disabled": "{{action}}ทันทีถูกปิดใช้งาน",
"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": "เลือกบท",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Token",
"Your OTP token": "OTP token'ınız",
"Verify token": "Token'ı doğrula",
"Sign in with Google": "Google ile giriş yap",
"Sign in with Apple": "Apple ile giriş yap",
"Sign in with GitHub": "GitHub ile giriş yap",
"Account": "Hesap",
"Failed to delete user. Please try again later.": "Kullanıcı silinemedi. Lütfen daha sonra tekrar deneyin.",
"Community Support": "Topluluk Desteği",
@@ -341,8 +338,8 @@
"{{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ı",
"Show Remaining Pages": "Kalan sayfaları göster",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Aboneliği Yönet",
"Coming Soon": "Yakında",
@@ -835,6 +832,204 @@
"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"
"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ı.",
"Failed to export the book.": "Kitap dışa aktarılamadı.",
"Export Book": "Kitabı Dışa Aktar",
"Whole word:": "Tam kelime:",
"Error": "Hata",
"Unable to load the article. Try searching directly on {{link}}.": "Makale yüklenemiyor. Doğrudan {{link}} üzerinde arama yapmayı deneyin.",
"Unable to load the word. Try searching directly on {{link}}.": "Kelime yüklenemiyor. Doğrudan {{link}} üzerinde arama yapmayı deneyin.",
"Date Published": "Yayın tarihi",
"Only for TTS:": "Sadece TTS için:",
"Uploaded": "Yüklendi",
"Downloaded": "İndirildi",
"Deleted": "Silindi",
"Note:": "Not:",
"Time:": "Saat:",
"Format Options": "Format Seçenekleri",
"Export Date": "Dışa Aktarma Tarihi",
"Chapter Titles": "Bölüm Başlıkları",
"Chapter Separator": "Bölüm Ayracı",
"Highlights": "Vurgular",
"Note Date": "Not Tarihi",
"Advanced": "Gelişmiş",
"Hide": "Gizle",
"Show": "Göster",
"Use Custom Template": "Özel Şablon Kullan",
"Export Template": "Dışa Aktarma Şablonu",
"Template Syntax:": "Şablon Sözdizimi:",
"Insert value": "Değer ekle",
"Format date (locale)": "Tarihi biçimlendir (yerel)",
"Format date (custom)": "Tarihi biçimlendir (özel)",
"Conditional": "Koşullu",
"Loop": "Döngü",
"Available Variables:": "Kullanılabilir Değişkenler:",
"Book title": "Kitap başlığı",
"Book author": "Kitap yazarı",
"Export date": "Dışa aktarma tarihi",
"Array of chapters": "Bölümler dizisi",
"Chapter title": "Bölüm başlığı",
"Array of annotations": "Notlar dizisi",
"Highlighted text": "Vurgulanan metin",
"Annotation note": "Not açıklaması",
"Date Format Tokens:": "Tarih Biçimi Belirteçleri:",
"Year (4 digits)": "Yıl (4 basamak)",
"Month (01-12)": "Ay (01-12)",
"Day (01-31)": "Gün (01-31)",
"Hour (00-23)": "Saat (00-23)",
"Minute (00-59)": "Dakika (00-59)",
"Second (00-59)": "Saniye (00-59)",
"Show Source": "Kaynağı Göster",
"No content to preview": "Önizlenecek içerik yok",
"Export": "Dışa Aktar",
"Set Timeout": "Zaman aşımını ayarla",
"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",
"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ç",
"{{number}} WPM": "{{number}} Kelime/Dakika",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Токен",
"Your OTP token": "Ваш OTP-токен",
"Verify token": "Перевірити токен",
"Sign in with Google": "Увійти через Google",
"Sign in with Apple": "Увійти через Apple",
"Sign in with GitHub": "Увійти через GitHub",
"Account": "Обліковий запис",
"Failed to delete user. Please try again later.": "Не вдалося видалити користувача. Будь ласка, спробуйте пізніше.",
"Community Support": "Підтримка спільноти",
@@ -349,8 +346,8 @@
"{{count}} pages left in chapter_many": "Залишилось {{count}} сторінок у розділі",
"{{count}} pages left in chapter_other": "Залишилось {{count}} сторінок у розділі",
"Show Remaining Pages": "Показати залишок сторінок",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Керувати підпискою",
"Coming Soon": "Незабаром",
@@ -855,6 +852,208 @@
"Show Results": "Показати результати",
"Clear search": "Очистити пошук",
"Clear search history": "Очистити історію пошуку",
"Quick action disabled": "Швидку дію вимкнено",
"Tap to Toggle Footer": "Торкніться, щоб перемкнути нижній колонтитул"
"Tap to Toggle Footer": "Торкніться, щоб перемкнути нижній колонтитул",
"Exported successfully": "Успішно експортовано",
"Book exported successfully.": "Книгу успішно експортовано.",
"Failed to export the book.": "Не вдалося експортувати книгу.",
"Export Book": "Експортувати книгу",
"Whole word:": "Ціле слово:",
"Error": "Помилка",
"Unable to load the article. Try searching directly on {{link}}.": "Не вдалося завантажити статтю. Спробуйте шукати безпосередньо на {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Не вдалося завантажити слово. Спробуйте шукати безпосередньо на {{link}}.",
"Date Published": "Дата публікації",
"Only for TTS:": "Тільки для TTS:",
"Uploaded": "Завантажено",
"Downloaded": "Звантажено",
"Deleted": "Видалено",
"Note:": "Примітка:",
"Time:": "Час:",
"Format Options": "Параметри формату",
"Export Date": "Дата експорту",
"Chapter Titles": "Назви розділів",
"Chapter Separator": "Роздільник розділів",
"Highlights": "Виділення",
"Note Date": "Дата примітки",
"Advanced": "Розширені",
"Hide": "Сховати",
"Show": "Показати",
"Use Custom Template": "Використовувати власний шаблон",
"Export Template": "Шаблон експорту",
"Template Syntax:": "Синтаксис шаблону:",
"Insert value": "Вставити значення",
"Format date (locale)": "Форматувати дату (локаль)",
"Format date (custom)": "Форматувати дату (власний)",
"Conditional": "Умовний",
"Loop": "Цикл",
"Available Variables:": "Доступні змінні:",
"Book title": "Назва книги",
"Book author": "Автор книги",
"Export date": "Дата експорту",
"Array of chapters": "Масив розділів",
"Chapter title": "Назва розділу",
"Array of annotations": "Масив приміток",
"Highlighted text": "Виділений текст",
"Annotation note": "Примітка анотації",
"Date Format Tokens:": "Токени формату дати:",
"Year (4 digits)": "Рік (4 цифри)",
"Month (01-12)": "Місяць (01-12)",
"Day (01-31)": "День (01-31)",
"Hour (00-23)": "Година (00-23)",
"Minute (00-59)": "Хвилина (00-59)",
"Second (00-59)": "Секунда (00-59)",
"Show Source": "Показати джерело",
"No content to preview": "Немає вмісту для перегляду",
"Export": "Експортувати",
"Set Timeout": "Встановити тайм-аут",
"Select Voice": "Вибрати голос",
"Toggle Sticky Bottom TTS Bar": "Перемкнути закріплену панель TTS",
"Display what I'm reading on Discord": "Показувати книгу в Discord",
"Show on Discord": "Показати в Discord",
"Instant {{action}}": "Миттєва {{action}}",
"Instant {{action}} Disabled": "Миттєву {{action}} вимкнено",
"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": "Вибрати розділ",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "Mã token",
"Your OTP token": "Mã OTP của bạn",
"Verify token": "Xác minh mã token",
"Sign in with Google": "Đăng nhập với Google",
"Sign in with Apple": "Đăng nhập với Apple",
"Sign in with GitHub": "Đăng nhập với GitHub",
"Account": "Tài khoản",
"Failed to delete user. Please try again later.": "Không thể xóa người dùng. Vui lòng thử lại sau.",
"Community Support": "Hỗ trợ cộng đồng",
@@ -337,8 +334,8 @@
"Reset Settings": "Đặt lại cài đặt",
"{{count}} pages left in chapter_other": "Còn {{count}} trang trong chương",
"Show Remaining Pages": "Hiển thị trang còn lại",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"Source Han Serif CN": "Source Han Serif",
"Huiwen-MinchoGBK": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Quản lý đăng ký",
"Coming Soon": "Sắp ra mắt",
@@ -825,6 +822,202 @@
"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"
"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.",
"Failed to export the book.": "Xuất sách thất bại.",
"Export Book": "Xuất sách",
"Whole word:": "Toàn bộ từ:",
"Error": "Lỗi",
"Unable to load the article. Try searching directly on {{link}}.": "Không thể tải bài viết. Hãy thử tìm kiếm trực tiếp trên {{link}}.",
"Unable to load the word. Try searching directly on {{link}}.": "Không thể tải từ. Hãy thử tìm kiếm trực tiếp trên {{link}}.",
"Date Published": "Ngày xuất bản",
"Only for TTS:": "Chỉ dành cho TTS:",
"Uploaded": "Đã tải lên",
"Downloaded": "Đã tải xuống",
"Deleted": "Đã xóa",
"Note:": "Ghi chú:",
"Time:": "Thời gian:",
"Format Options": "Tùy chọn định dạng",
"Export Date": "Ngày xuất",
"Chapter Titles": "Tiêu đề chương",
"Chapter Separator": "Dấu phân cách chương",
"Highlights": "Đánh dấu",
"Note Date": "Ngày ghi chú",
"Advanced": "Nâng cao",
"Hide": "Ẩn",
"Show": "Hiển thị",
"Use Custom Template": "Sử dụng mẫu tùy chỉnh",
"Export Template": "Mẫu xuất",
"Template Syntax:": "Cú pháp mẫu:",
"Insert value": "Chèn giá trị",
"Format date (locale)": "Định dạng ngày (địa phương)",
"Format date (custom)": "Định dạng ngày (tùy chỉnh)",
"Conditional": "Điều kiện",
"Loop": "Vòng lặp",
"Available Variables:": "Biến có sẵn:",
"Book title": "Tiêu đề sách",
"Book author": "Tác giả sách",
"Export date": "Ngày xuất",
"Array of chapters": "Danh sách chương",
"Chapter title": "Tiêu đề chương",
"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",
"Date Format Tokens:": "Token định dạng ngày:",
"Year (4 digits)": "Năm (4 chữ số)",
"Month (01-12)": "Tháng (01-12)",
"Day (01-31)": "Ngày (01-31)",
"Hour (00-23)": "Giờ (00-23)",
"Minute (00-59)": "Phút (00-59)",
"Second (00-59)": "Giây (00-59)",
"Show Source": "Hiển thị nguồn",
"No content to preview": "Không có nội dung để xem trước",
"Export": "Xuất",
"Set Timeout": "Đặt thời gian chờ",
"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",
"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",
"{{number}} WPM": "{{number}} WPM",
"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"
}
@@ -173,9 +173,6 @@
"Token": "令牌",
"Your OTP token": "您的 OTP 令牌",
"Verify token": "验证令牌",
"Sign in with Google": "使用 Google 登录",
"Sign in with Apple": "使用 Apple 登录",
"Sign in with GitHub": "使用 GitHub 登录",
"Account": "账户",
"Failed to delete user. Please try again later.": "删除用户失败。请稍后重试。",
"Community Support": "社区支持",
@@ -337,8 +334,8 @@
"Reset Settings": "重置设置",
"{{count}} pages left in chapter_other": "本章剩余 {{count}} 页",
"Show Remaining Pages": "显示剩余页数",
"Source Han Serif CN VF": "思源宋体",
"Huiwen-mincho": "汇文明朝体",
"Source Han Serif CN": "思源宋体",
"Huiwen-MinchoGBK": "汇文明朝体",
"KingHwa_OldSong": "京华老宋体",
"Manage Subscription": "管理订阅",
"Coming Soon": "即将上线",
@@ -825,6 +822,202 @@
"Show Results": "显示结果",
"Clear search": "清除搜索",
"Clear search history": "清除搜索历史",
"Quick action disabled": "快捷操作已禁用",
"Tap to Toggle Footer": "点击切换页脚"
"Tap to Toggle Footer": "点击切换页脚",
"Exported successfully": "导出成功",
"Book exported successfully.": "书籍导出成功。",
"Failed to export the book.": "书籍导出失败。",
"Export Book": "导出书籍",
"Whole word:": "全词匹配:",
"Only for TTS:": "仅用于TTS",
"Error": "错误",
"Unable to load the article. Try searching directly on {{link}}.": "无法加载文章。请直接在 {{link}} 上搜索。",
"Unable to load the word. Try searching directly on {{link}}.": "无法加载词汇。请直接在 {{link}} 上搜索。",
"Date Published": "出版日期",
"Uploaded": "已上传",
"Downloaded": "已下载",
"Deleted": "已删除",
"Note:": "笔记:",
"Time:": "时间:",
"Format Options": "格式选项",
"Export Date": "导出日期",
"Chapter Titles": "章节标题",
"Chapter Separator": "章节分隔符",
"Highlights": "高亮",
"Note Date": "笔记日期",
"Advanced": "高级",
"Hide": "隐藏",
"Show": "显示",
"Use Custom Template": "使用自定义模板",
"Export Template": "导出模板",
"Template Syntax:": "模板语法:",
"Insert value": "插入值",
"Format date (locale)": "格式化日期(区域)",
"Format date (custom)": "格式化日期(自定义)",
"Conditional": "条件",
"Loop": "循环",
"Available Variables:": "可用变量:",
"Book title": "书名",
"Book author": "作者",
"Export date": "导出日期",
"Array of chapters": "章节数组",
"Chapter title": "章节标题",
"Array of annotations": "标注数组",
"Highlighted text": "高亮文本",
"Annotation note": "标注笔记",
"Date Format Tokens:": "日期格式标记:",
"Year (4 digits)": "年(4位)",
"Month (01-12)": "月(01-12",
"Day (01-31)": "日(01-31",
"Hour (00-23)": "时(00-23",
"Minute (00-59)": "分(00-59",
"Second (00-59)": "秒(00-59",
"Show Source": "显示源码",
"No content to preview": "无内容可预览",
"Export": "导出",
"Set Timeout": "设置超时",
"Select Voice": "选择语音",
"Toggle Sticky Bottom TTS Bar": "切换固定TTS栏",
"Display what I'm reading on Discord": "在Discord显示阅读状态",
"Show on Discord": "在Discord显示",
"Instant {{action}}": "即时{{action}}",
"Instant {{action}} Disabled": "即时{{action}}已禁用",
"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": "选择章节",
"{{number}} WPM": "{{number}} WPM",
"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": "回到朗读位置"
}
@@ -173,9 +173,6 @@
"Token": "令牌",
"Your OTP token": "您的 OTP 令牌",
"Verify token": "驗證令牌",
"Sign in with Google": "使用 Google 登入",
"Sign in with Apple": "使用 Apple 登入",
"Sign in with GitHub": "使用 GitHub 登入",
"Account": "帳戶",
"Failed to delete user. Please try again later.": "刪除使用者失敗。請稍後再試。",
"Community Support": "社群支援",
@@ -337,8 +334,8 @@
"Reset Settings": "重置設置",
"{{count}} pages left in chapter_other": "本章剩餘 {{count}} 頁",
"Show Remaining Pages": "顯示剩餘頁數",
"Source Han Serif CN VF": "思源宋體",
"Huiwen-mincho": "匯文明朝體",
"Source Han Serif CN": "思源宋體",
"Huiwen-MinchoGBK": "匯文明朝體",
"KingHwa_OldSong": "京華老宋體",
"Manage Subscription": "管理訂閱",
"Coming Soon": "即將推出",
@@ -825,6 +822,202 @@
"Show Results": "顯示結果",
"Clear search": "清除搜尋",
"Clear search history": "清除搜尋歷史",
"Quick action disabled": "快捷操作已停用",
"Tap to Toggle Footer": "點擊切換頁尾"
"Tap to Toggle Footer": "點擊切換頁尾",
"Exported successfully": "匯出成功",
"Book exported successfully.": "書籍匯出成功。",
"Failed to export the book.": "書籍匯出失敗。",
"Export Book": "匯出書籍",
"Whole word:": "全詞匹配:",
"Error": "錯誤",
"Unable to load the article. Try searching directly on {{link}}.": "無法載入文章。請直接在 {{link}} 上搜尋。",
"Unable to load the word. Try searching directly on {{link}}.": "無法載入詞彙。請直接在 {{link}} 上搜尋。",
"Date Published": "出版日期",
"Only for TTS:": "僅用於TTS",
"Uploaded": "已上傳",
"Downloaded": "已下載",
"Deleted": "已刪除",
"Note:": "筆記:",
"Time:": "時間:",
"Format Options": "格式選項",
"Export Date": "匯出日期",
"Chapter Titles": "章節標題",
"Chapter Separator": "章節分隔符",
"Highlights": "標記",
"Note Date": "筆記日期",
"Advanced": "進階",
"Hide": "隱藏",
"Show": "顯示",
"Use Custom Template": "使用自訂模板",
"Export Template": "匯出模板",
"Template Syntax:": "模板語法:",
"Insert value": "插入值",
"Format date (locale)": "格式化日期(區域)",
"Format date (custom)": "格式化日期(自訂)",
"Conditional": "條件",
"Loop": "迴圈",
"Available Variables:": "可用變數:",
"Book title": "書名",
"Book author": "作者",
"Export date": "匯出日期",
"Array of chapters": "章節陣列",
"Chapter title": "章節標題",
"Array of annotations": "標註陣列",
"Highlighted text": "標記文字",
"Annotation note": "標註筆記",
"Date Format Tokens:": "日期格式標記:",
"Year (4 digits)": "年(4位)",
"Month (01-12)": "月(01-12",
"Day (01-31)": "日(01-31",
"Hour (00-23)": "時(00-23",
"Minute (00-59)": "分(00-59",
"Second (00-59)": "秒(00-59",
"Show Source": "顯示原始碼",
"No content to preview": "無內容可預覽",
"Export": "匯出",
"Set Timeout": "設定逾時",
"Select Voice": "選擇語音",
"Toggle Sticky Bottom TTS Bar": "切換固定TTS欄",
"Display what I'm reading on Discord": "在Discord顯示閱讀狀態",
"Show on Discord": "在Discord顯示",
"Instant {{action}}": "即時{{action}}",
"Instant {{action}} Disabled": "即時{{action}}已停用",
"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": "選擇章節",
"{{number}} WPM": "{{number}} WPM",
"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": "回到朗讀位置"
}
+40
View File
@@ -1,5 +1,45 @@
{
"releases": {
"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": [
"Bookshelf: Added the ability to export original book files directly from the book details dialog",
"Bookshelf: Added a new sorting option to organize books by publish date",
"Bookshelf: Improved performance for large library with faster and more stable imports",
"OPDS: Improved download reliability by correctly detecting filenames for PDF and CBZ files",
"Proofreading: Added support for whole-word replacement to make text corrections more precise",
"Notes: Added the ability to export notes using a customizable template",
"PDF: Added support for panning within PDF documents for easier navigation",
"PDF: Improved performance by pre-rendering the next page for smoother page transitions",
"Text-to-Speech: Added support for word replacement to improve pronunciation during playback",
"Text-to-Speech: Fixed an issue where playback speed settings were not applied correctly on Linux",
"E-ink: Improved reading stability by preventing unwanted scrolling when animations are disabled or on e-ink devices",
"Discord: Added Discord Rich Presence to display your current reading status on Desktop platforms",
"Android: Fixed an issue that could cause occasional crashes during app startup",
"Fonts: Improved web font loading for faster and more stable text rendering"
]
},
"0.9.97": {
"date": "2026-01-10",
"notes": [
+4 -1
View File
@@ -26,6 +26,7 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
thiserror = "2"
walkdir = "2"
tokio = { version = "1", features = ["fs"] }
tokio-util = { version = "0.7", features = ["codec"] }
futures-util = "0.3"
@@ -35,7 +36,7 @@ reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
] }
tauri = { version = "2.5.1", features = [ "protocol-asset" ] }
tauri = { version = "2", features = [ "protocol-asset" ] }
tauri-build = "2"
tauri-plugin-log = "2"
tauri-plugin-fs = "2"
@@ -53,6 +54,7 @@ tauri-plugin-persisted-scope = "2"
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
tauri-plugin-websocket = "2"
tauri-plugin-sharekit = "0.3"
[target."cfg(target_os = \"macos\")".dependencies]
rand = "0.8"
@@ -70,3 +72,4 @@ tauri-plugin-cli = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"
tauri-plugin-window-state = "2"
discord-rich-presence = "1.0.0"
@@ -152,12 +152,33 @@
}
]
},
{
"identifier": "shell:allow-spawn",
"allow": [
{
"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$" }]
}
]
},
"haptics:allow-vibrate",
"haptics:allow-impact-feedback",
"haptics:allow-notification-feedback",
"haptics:allow-selection-feedback",
"native-bridge:default",
"deep-link:default",
"sharekit:default",
"native-tts:default",
"deep-link:default"
"native-bridge:default"
]
}
@@ -24,7 +24,7 @@ import com.readest.native_bridge.KeyDownInterceptor
import com.readest.native_bridge.NativeBridgePlugin
class MainActivity : TauriActivity(), KeyDownInterceptor {
private lateinit var wv: WebView
private var wv: WebView? = null
private var interceptVolumeKeysEnabled = false
private var interceptBackKeyEnabled = false
@@ -65,7 +65,7 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
val y = event.getY(pointerIndex)
val pressure = event.getPressure(pointerIndex)
wv.evaluateJavascript(
wv?.evaluateJavascript(
"""
try {
if (window.onNativeTouch) {
@@ -103,7 +103,7 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
}
if (shouldIntercept) {
wv.evaluateJavascript(
wv?.evaluateJavascript(
"""
try { window.onNativeKeyDown("$keyName", $keyCode); } catch (_) {}
""".trimIndent(),
@@ -119,7 +119,7 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val keyName = keyEventMap[keyCode]
if (keyName != null) {
wv.evaluateJavascript(
wv?.evaluateJavascript(
"""
try {
window.onNativeKeyDown("$keyName", $keyCode)
@@ -178,7 +178,7 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
Log.d("MainActivity", "Back invoked callback triggered ${interceptBackKeyEnabled}")
if (interceptBackKeyEnabled) {
Log.d("MainActivity", "Back intercepted (OnBackInvokedCallback)")
wv.evaluateJavascript(
wv?.evaluateJavascript(
"""window.onNativeKeyDown("Back", ${KeyEvent.KEYCODE_BACK});""",
null
)
@@ -194,7 +194,7 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
override fun handleOnBackPressed() {
if (interceptBackKeyEnabled) {
Log.d("MainActivity", "Back intercepted (OnBackPressedDispatcher)")
wv.evaluateJavascript(
wv?.evaluateJavascript(
"""window.onNativeKeyDown("Back", ${KeyEvent.KEYCODE_BACK});""",
null
)
@@ -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",
@@ -581,7 +581,6 @@ class NativeBridgePlugin: Plugin {
let darkMode = args.darkMode
DispatchQueue.main.async {
UIApplication.shared.isIdleTimerDisabled = !visible
UIApplication.shared.setStatusBarHidden(!visible, with: .none)
let windows = UIApplication.shared.connectedScenes
@@ -880,6 +879,16 @@ 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")
}
}
}
}
@_cdecl("init_plugin_native_bridge")
@@ -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"]
@@ -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",
@@ -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>,
}
@@ -0,0 +1,96 @@
use std::path::Path;
use tauri::AppHandle;
use tauri_plugin_fs::FsExt;
use walkdir::WalkDir;
#[derive(serde::Serialize)]
pub struct ScannedFile {
pub path: String,
pub size: u64,
}
#[tauri::command]
pub fn read_dir(
app: AppHandle,
path: String,
recursive: bool,
extensions: Vec<String>,
) -> Result<Vec<ScannedFile>, String> {
let scope = app.fs_scope();
let path_buf = std::path::PathBuf::from(&path);
if !scope.is_allowed(&path_buf) {
return Err("Permission denied: Path not in filesystem scope".to_string());
}
let mut files = Vec::new();
let normalized_extensions: Vec<String> =
extensions.iter().map(|ext| ext.to_lowercase()).collect();
if recursive {
for entry_result in WalkDir::new(&path).into_iter() {
match entry_result {
Ok(entry) => {
if entry.file_type().is_file() {
if let Some(scanned_file) =
process_file_entry(entry.path(), &normalized_extensions)
{
files.push(scanned_file);
}
}
}
Err(e) => {
log::warn!("RUST: Skipping file due to error: {}", e);
}
}
}
} else {
match std::fs::read_dir(&path_buf) {
Ok(entries) => {
for entry_result in entries {
match entry_result {
Ok(entry) => {
let path = entry.path();
if path.is_file() {
if let Some(scanned_file) =
process_file_entry(&path, &normalized_extensions)
{
files.push(scanned_file);
}
}
}
Err(e) => {
log::warn!("RUST: Skipping entry due to error: {}", e);
}
}
}
}
Err(e) => {
return Err(format!("Failed to read directory: {}", e));
}
}
}
Ok(files)
}
fn process_file_entry(path: &Path, extensions: &[String]) -> Option<ScannedFile> {
if extensions.is_empty() || extensions.contains(&"*".to_string()) {
let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
return Some(ScannedFile {
path: path.to_string_lossy().to_string(),
size,
});
} else if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if extensions.contains(&ext_str) {
let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
return Some(ScannedFile {
path: path.to_string_lossy().to_string(),
size,
});
}
}
None
}
@@ -0,0 +1,182 @@
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient};
use serde::Deserialize;
use std::sync::{Arc, Mutex};
use tauri::State;
const DISCORD_APP_ID: &str = "1462683110612144348";
const MAX_TITLE_LENGTH: usize = 128;
const MAX_AUTHOR_LENGTH: usize = 128;
#[derive(Debug)]
pub struct DiscordRpcClient {
client: Option<DiscordIpcClient>,
current_book_hash: Option<String>,
}
impl DiscordRpcClient {
pub fn new() -> Self {
DiscordRpcClient {
client: None,
current_book_hash: None,
}
}
fn ensure_connected(&mut self) -> Result<(), String> {
if self.client.is_some() {
return Ok(());
}
let mut client = DiscordIpcClient::new(DISCORD_APP_ID);
match client.connect() {
Ok(_) => {
log::info!("Successfully connected to Discord");
self.client = Some(client);
Ok(())
}
Err(e) => {
log::warn!("Failed to connect to Discord: {}", e);
Err(format!("Failed to connect to Discord: {}", e))
}
}
}
fn disconnect(&mut self) {
if let Some(mut client) = self.client.take() {
let _ = client.close();
log::debug!("Disconnected from Discord");
}
self.current_book_hash = None;
}
fn truncate_string(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len - 3])
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BookPresenceData {
book_hash: String,
title: String,
author: Option<String>,
cover_url: Option<String>,
session_start: i64,
}
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
#[tauri::command]
pub async fn update_book_presence(
state: State<'_, Arc<Mutex<DiscordRpcClient>>>,
presence: BookPresenceData,
) -> Result<(), String> {
let mut client = state
.lock()
.map_err(|e| format!("Mutex lock error: {}", e))?;
if let Err(e) = client.ensure_connected() {
log::debug!("Discord not available: {}", e);
return Ok(());
}
let BookPresenceData {
book_hash,
title,
author,
cover_url,
session_start,
} = presence;
// Truncate title and author to avoid Discord API limits
let truncated_title = DiscordRpcClient::truncate_string(&title, MAX_TITLE_LENGTH);
let state_text = if let Some(ref author_name) = author {
let truncated_author = DiscordRpcClient::truncate_string(author_name, MAX_AUTHOR_LENGTH);
format!("by {}", truncated_author)
} else {
String::new()
};
let mut activity_builder = activity::Activity::new().details(&truncated_title);
if !state_text.is_empty() {
activity_builder = activity_builder.state(&state_text);
}
activity_builder =
activity_builder.timestamps(activity::Timestamps::new().start(session_start / 1000));
let large_image = cover_url
.as_deref()
.filter(|url| url.starts_with("https://"))
.unwrap_or("book_icon");
let assets_builder = activity::Assets::new()
.large_image(large_image)
.large_text(&truncated_title);
activity_builder = activity_builder.assets(assets_builder);
let button = activity::Button::new("Read on Readest", "https://web.readest.com");
activity_builder = activity_builder.buttons(vec![button]);
if let Some(ref mut discord_client) = client.client {
match discord_client.set_activity(activity_builder) {
Ok(_) => {
log::info!("Successfully updated Discord presence");
client.current_book_hash = Some(book_hash);
Ok(())
}
Err(e) => {
log::error!("Failed to update Discord activity: {}", e);
client.disconnect();
Err(format!("Failed to update Discord activity: {}", e))
}
}
} else {
Err("Discord client not initialized".to_string())
}
}
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
#[tauri::command]
pub async fn clear_book_presence(
state: State<'_, Arc<Mutex<DiscordRpcClient>>>,
) -> Result<(), String> {
let mut client = state
.lock()
.map_err(|e| format!("Mutex lock error: {}", e))?;
if let Some(ref mut discord_client) = client.client {
match discord_client.clear_activity() {
Ok(_) => {
log::info!("Successfully cleared Discord presence");
client.current_book_hash = None;
Ok(())
}
Err(e) => {
log::error!("Failed to clear Discord activity: {}", e);
client.disconnect();
Ok(())
}
}
} else {
log::debug!("No Discord client to clear");
client.current_book_hash = None;
Ok(())
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
#[tauri::command]
pub async fn update_book_presence(_presence: BookPresenceData) -> Result<(), String> {
Ok(()) // No-op on non-desktop platforms
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
#[tauri::command]
pub async fn clear_book_presence() -> Result<(), String> {
Ok(()) // No-op on non-desktop platforms
}
+58 -49
View File
@@ -22,6 +22,9 @@ use tauri_plugin_fs::FsExt;
#[cfg(desktop)]
use tauri::{Listener, Url};
mod dir_scanner;
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
mod discord_rpc;
#[cfg(target_os = "macos")]
mod macos;
mod transfer_file;
@@ -41,14 +44,14 @@ fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
let asset_protocol_scope = app.asset_protocol_scope();
for file in &files {
if let Err(e) = fs_scope.allow_file(file) {
eprintln!("Failed to allow file in fs_scope: {e}");
log::error!("Failed to allow file in fs_scope: {e}");
} else {
println!("Allowed file in fs_scope: {file:?}");
log::debug!("Allowed file in fs_scope: {file:?}");
}
if let Err(e) = asset_protocol_scope.allow_file(file) {
eprintln!("Failed to allow file in asset_protocol_scope: {e}");
log::error!("Failed to allow file in asset_protocol_scope: {e}");
} else {
println!("Allowed file in asset_protocol_scope: {file:?}");
log::debug!("Allowed file in asset_protocol_scope: {file:?}");
}
}
}
@@ -57,14 +60,14 @@ fn allow_dir_in_scopes(app: &AppHandle, dir: &PathBuf) {
let fs_scope = app.fs_scope();
let asset_protocol_scope = app.asset_protocol_scope();
if let Err(e) = fs_scope.allow_directory(dir, true) {
eprintln!("Failed to allow directory in fs_scope: {e}");
log::error!("Failed to allow directory in fs_scope: {e}");
} else {
println!("Allowed directory in fs_scope: {dir:?}");
log::info!("Allowed directory in fs_scope: {dir:?}");
}
if let Err(e) = asset_protocol_scope.allow_directory(dir, true) {
eprintln!("Failed to allow directory in asset_protocol_scope: {e}");
log::error!("Failed to allow directory in asset_protocol_scope: {e}");
} else {
println!("Allowed directory in asset_protocol_scope: {dir:?}");
log::info!("Allowed directory in asset_protocol_scope: {dir:?}");
}
}
@@ -147,6 +150,11 @@ struct SingleInstancePayload {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.plugin(
tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Info)
.build(),
)
.plugin(tauri_plugin_websocket::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_oauth::init())
@@ -156,12 +164,17 @@ pub fn run() {
upload_file,
get_environment_variable,
get_executable_dir,
dir_scanner::read_dir,
#[cfg(target_os = "macos")]
macos::safari_auth::auth_with_safari,
#[cfg(target_os = "macos")]
macos::apple_auth::start_apple_sign_in,
#[cfg(target_os = "macos")]
macos::traffic_light::set_traffic_lights,
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
discord_rpc::update_book_presence,
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
discord_rpc::clear_book_presence,
])
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_persisted_scope::init())
@@ -170,6 +183,7 @@ pub fn run() {
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_sharekit::init())
.plugin(tauri_plugin_native_bridge::init())
.plugin(tauri_plugin_native_tts::init());
@@ -209,6 +223,13 @@ pub fn run() {
builder
.setup(|#[allow(unused_variables)] app| {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
{
use std::sync::{Arc, Mutex};
let discord_client = Arc::new(Mutex::new(discord_rpc::DiscordRpcClient::new()));
app.manage(discord_client);
}
#[cfg(desktop)]
{
let files = get_files_from_argv(std::env::args().collect());
@@ -232,46 +253,16 @@ 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();
}
if let Err(e) = app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
) {
eprintln!("Failed to initialize tauri_plugin_log: {e}");
};
#[cfg(desktop)]
{
app.handle().plugin(tauri_plugin_cli::init())?;
}
// Check for e-ink device on Android before building the window
#[cfg(target_os = "android")]
@@ -279,15 +270,30 @@ pub fn run() {
#[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:' ||
@@ -309,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();
@@ -38,8 +38,8 @@ pub struct AppleIDAuthorizationResponse {
}
thread_local! {
static APPLE_SIGN_IN_DELEGATE: RefCell<Option<Retained<ASAuthorizationControllerDelegateImpl>>> = RefCell::new(None);
static AUTHORIZATION_CONTROLLER: RefCell<Option<Retained<ASAuthorizationController>>> = RefCell::new(None);
static APPLE_SIGN_IN_DELEGATE: RefCell<Option<Retained<ASAuthorizationControllerDelegateImpl>>> = const { RefCell::new(None) };
static AUTHORIZATION_CONTROLLER: RefCell<Option<Retained<ASAuthorizationController>>> = const { RefCell::new(None) };
}
#[derive(Clone)]
@@ -70,7 +70,7 @@ define_class!(
{
let user_identifier = {
let user = credential.user();
if user.len() > 0 {
if !user.is_empty() {
Some(user.to_string())
} else {
None
@@ -183,7 +183,7 @@ pub fn start_apple_sign_in(app: AppHandle, payload: AppleIDAuthorizationRequest)
let auth_request = &request as &ASAuthorizationRequest;
let controller = ASAuthorizationController::initWithAuthorizationRequests(
ASAuthorizationController::alloc(),
&*NSArray::from_slice(&[auth_request]),
&NSArray::from_slice(&[auth_request]),
);
let delegate = ASAuthorizationControllerDelegateImpl::new(app.clone());
@@ -35,16 +35,11 @@ impl ThreadSafeObjcPointer {
}
}
#[derive(Default)]
pub struct AuthSession {
auth_session: Option<ThreadSafeObjcPointer>,
}
impl Default for AuthSession {
fn default() -> Self {
Self { auth_session: None }
}
}
impl Drop for AuthSession {
fn drop(&mut self) {
if let Some(session) = &self.auth_session {
@@ -110,7 +110,7 @@ pub async fn download_file(
single_threaded: Option<bool>,
skip_ssl_verification: Option<bool>,
on_progress: Channel<ProgressPayload>,
) -> Result<()> {
) -> Result<HashMap<String, String>> {
use futures::stream::{self, StreamExt};
use std::cmp::min;
use tokio::io::AsyncSeekExt;
@@ -130,7 +130,7 @@ pub async fn download_file(
headers: &HashMap<String, String>,
body: &Option<String>,
on_progress: Channel<ProgressPayload>,
) -> Result<()> {
) -> Result<HashMap<String, String>> {
let mut request = if let Some(body) = body {
client.post(url).body(body.clone())
} else {
@@ -149,6 +149,13 @@ pub async fn download_file(
));
}
let mut resp_headers = HashMap::new();
for (key, value) in response.headers().iter() {
if let Ok(val_str) = value.to_str() {
resp_headers.insert(key.to_string(), val_str.to_string());
}
}
let total = response.content_length().unwrap_or(0);
let mut file = BufWriter::new(File::create(file_path).await?);
let mut stream = response.bytes_stream();
@@ -165,7 +172,7 @@ pub async fn download_file(
}
file.flush().await?;
Ok(())
Ok(resp_headers)
}
if force_single {
@@ -193,6 +200,13 @@ pub async fn download_file(
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
let mut resp_headers = HashMap::new();
for (key, value) in range_resp.headers().iter() {
if let Ok(val_str) = value.to_str() {
resp_headers.insert(key.to_string(), val_str.to_string());
}
}
if !accept_ranges || total == 0 {
return single_threaded_download(&client, url, file_path, &headers, &body, on_progress)
.await;
@@ -260,7 +274,7 @@ pub async fn download_file(
})
.await;
Ok(())
Ok(resp_headers)
}
#[command]
+2 -2
View File
@@ -17,8 +17,8 @@
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost http://*:* https://*:* https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
"img-src": "'self' blob: data: asset: http://asset.localhost https://* https://*:* http://* http://*:*",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.deno.dev https://cdnjs.cloudflare.com",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.deno.dev https://cdnjs.cloudflare.com",
"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"
},
@@ -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);
});
});
@@ -67,9 +67,14 @@ vi.mock('@/services/environment', async (importOriginal) => {
});
import { EnvProvider } from '@/context/EnvContext';
import { AuthProvider } from '@/context/AuthContext';
function renderWithProviders(ui: React.ReactNode) {
return render(<EnvProvider>{ui}</EnvProvider>);
return render(
<EnvProvider>
<AuthProvider>{ui}</AuthProvider>
</EnvProvider>,
);
}
describe('ProofreadRulesManager', () => {
@@ -150,9 +155,9 @@ describe('ProofreadRulesManager', () => {
expect(dialog).toBeTruthy();
// Library (global) rules
expect(screen.getByText('foo')).toBeTruthy();
expect(screen.getByText('bar')).toBeTruthy();
expect(screen.getByText("'bar'")).toBeTruthy();
expect(screen.getByText('hello')).toBeTruthy();
expect(screen.getByText('world')).toBeTruthy();
expect(screen.getByText("'world'")).toBeTruthy();
});
it('renders selection rules separately from book/library rules', async () => {
@@ -231,11 +236,11 @@ describe('ProofreadRulesManager', () => {
// Single Instance Rules section
expect(screen.getByText('Selected Text Rules')).toBeTruthy();
expect(screen.getByText('only-once')).toBeTruthy();
expect(screen.getByText('single-hit')).toBeTruthy();
expect(screen.getByText("'single-hit'")).toBeTruthy();
// Book section should still show book-wide rule
expect(screen.getByText('book-wide')).toBeTruthy();
expect(screen.getByText('book-hit')).toBeTruthy();
expect(screen.getByText("'book-hit'")).toBeTruthy();
});
it('displays correct scope labels for different rule types', async () => {
@@ -402,11 +407,11 @@ describe('ProofreadRulesManager', () => {
const csRuleElement = screen.getByText('case-sensitive').closest('li');
expect(within(csRuleElement!).getByText(/Case sensitive:/)).toBeTruthy();
expect(within(csRuleElement!).getByText(/Yes/)).toBeTruthy();
expect(within(csRuleElement!).getAllByText(/Yes/)).toBeTruthy();
const ciRuleElement = screen.getByText('case-insensitive').closest('li');
expect(within(ciRuleElement!).getByText(/Case sensitive:/)).toBeTruthy();
expect(within(ciRuleElement!).getByText(/No/)).toBeTruthy();
expect(within(ciRuleElement!).getAllByText(/No/)).toBeTruthy();
});
it('opens when BookMenu item is clicked (integration)', async () => {
@@ -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();
});
});
@@ -0,0 +1,378 @@
import { describe, it, expect } from 'vitest';
import { makeSafeFilename } from '../../utils/misc';
describe('makeSafeFilename', () => {
describe('Basic sanitization', () => {
it('should replace unsafe characters with underscore', () => {
expect(makeSafeFilename('file<name>.txt')).toBe('file_name_.txt');
expect(makeSafeFilename('file>name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file:name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file"name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file/name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file\\name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file|name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file?name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file*name.txt')).toBe('file_name.txt');
});
it('should replace multiple unsafe characters', () => {
expect(makeSafeFilename('file<>:"|?*.txt')).toBe('file_______.txt');
});
it('should use custom replacement character', () => {
expect(makeSafeFilename('file<name>.txt', '-')).toBe('file-name-.txt');
expect(makeSafeFilename('file:name.txt', '')).toBe('filename.txt');
});
it('should handle control characters', () => {
expect(makeSafeFilename('file\x00name.txt')).toBe('file_name.txt');
expect(makeSafeFilename('file\x1Fname.txt')).toBe('file_name.txt');
});
it('should trim whitespace from result', () => {
expect(makeSafeFilename(' filename.txt ')).toBe('filename.txt');
expect(makeSafeFilename('filename.txt ')).toBe('filename.txt');
expect(makeSafeFilename(' filename.txt')).toBe('filename.txt');
});
});
describe('Reserved filenames (Windows)', () => {
it('should handle reserved names case-insensitively', () => {
expect(makeSafeFilename('CON')).toBe('CON_');
expect(makeSafeFilename('con')).toBe('con_');
expect(makeSafeFilename('Con')).toBe('Con_');
expect(makeSafeFilename('PRN')).toBe('PRN_');
expect(makeSafeFilename('AUX')).toBe('AUX_');
expect(makeSafeFilename('NUL')).toBe('NUL_');
});
it('should handle reserved names with port numbers', () => {
expect(makeSafeFilename('COM1')).toBe('COM1_');
expect(makeSafeFilename('COM9')).toBe('COM9_');
expect(makeSafeFilename('LPT1')).toBe('LPT1_');
expect(makeSafeFilename('LPT9')).toBe('LPT9_');
});
it('should not affect reserved names with extensions', () => {
// Reserved names only apply to the base name without extension
const result = makeSafeFilename('CON.txt');
expect(result).toBe('CON.txt'); // This might be CON_.txt depending on implementation
});
it('should not affect similar but non-reserved names', () => {
expect(makeSafeFilename('CONFIG')).toBe('CONFIG');
expect(makeSafeFilename('CONSOLE')).toBe('CONSOLE');
expect(makeSafeFilename('PRINTER')).toBe('PRINTER');
});
});
describe('Multi-byte UTF-8 characters', () => {
it('should preserve single multi-byte characters', () => {
expect(makeSafeFilename('文件.txt')).toBe('文件.txt');
expect(makeSafeFilename('ファイル.txt')).toBe('ファイル.txt');
expect(makeSafeFilename('파일.txt')).toBe('파일.txt');
});
it('should preserve emoji characters', () => {
expect(makeSafeFilename('📚 Book.txt')).toBe('📚 Book.txt');
expect(makeSafeFilename('🎉🎊🎈.txt')).toBe('🎉🎊🎈.txt');
expect(makeSafeFilename('Test 😀.txt')).toBe('Test 😀.txt');
});
it('should handle mixed ASCII and multi-byte characters', () => {
expect(makeSafeFilename('Book-书籍-本.txt')).toBe('Book-书籍-本.txt');
expect(makeSafeFilename('Test_测试_テスト.txt')).toBe('Test_测试_テスト.txt');
});
it('should preserve complex emoji (with modifiers and ZWJ)', () => {
expect(makeSafeFilename('👨‍👩‍👧‍👦.txt')).toBe('👨‍👩‍👧‍👦.txt'); // Family emoji with ZWJ
expect(makeSafeFilename('👍🏽.txt')).toBe('👍🏽.txt'); // Thumbs up with skin tone modifier
});
it('should handle combining characters', () => {
expect(makeSafeFilename('café.txt')).toBe('café.txt'); // é is composed
expect(makeSafeFilename('naïve.txt')).toBe('naïve.txt');
});
});
describe('Byte length truncation (250 bytes max)', () => {
it('should not truncate short filenames', () => {
const shortName = 'short.txt';
expect(makeSafeFilename(shortName)).toBe(shortName);
});
it('should truncate long ASCII filenames', () => {
const longName = 'a'.repeat(260) + '.txt';
const result = makeSafeFilename(longName);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
});
it('should truncate at exactly 250 bytes', () => {
const longName = 'x'.repeat(255); // More than 250 bytes
const result = makeSafeFilename(longName);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBe(250);
});
it('should preserve valid UTF-8 when truncating multi-byte characters', () => {
// Chinese characters: each is 3 bytes in UTF-8
const chineseChars = '书'.repeat(100); // 300 bytes total
const result = makeSafeFilename(chineseChars);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
// Verify no broken UTF-8 by encoding and decoding
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain(''); // No replacement characters
});
it('should handle Japanese characters when truncating', () => {
// Japanese hiragana: each is 3 bytes in UTF-8
const japaneseChars = 'あ'.repeat(100); // 300 bytes
const result = makeSafeFilename(japaneseChars);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
});
it('should handle emoji when truncating', () => {
// Most emoji are 4 bytes in UTF-8
const emojiString = '😀'.repeat(70); // 280 bytes
const result = makeSafeFilename(emojiString);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
});
it('should handle mixed-width characters when truncating', () => {
// Mix of 1-byte (ASCII), 2-byte (Latin extended), 3-byte (CJK), 4-byte (emoji)
const mixedString = 'Test测试тест😀'.repeat(20); // Over 250 bytes
const result = makeSafeFilename(mixedString);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
});
it('should handle Korean characters when truncating', () => {
// Korean characters: each is 3 bytes in UTF-8
const koreanChars = '가'.repeat(100); // 300 bytes
const result = makeSafeFilename(koreanChars);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
});
});
describe('Edge cases', () => {
it('should handle empty string', () => {
expect(makeSafeFilename('')).toBe('');
});
it('should handle string with only unsafe characters', () => {
expect(makeSafeFilename('<>:"|?*#')).toBe('________');
});
it('should handle string that becomes empty after sanitization and trimming', () => {
const result = makeSafeFilename(' ');
expect(result).toBe('');
});
it('should handle very long extension', () => {
const longExt = '.txt'.repeat(50);
const filename = 'file' + longExt;
const result = makeSafeFilename(filename);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
});
it('should handle filename with only whitespace', () => {
expect(makeSafeFilename(' ')).toBe('');
});
it('should handle complex real-world Chinese book title', () => {
const longTitle =
'这是一个非常长的中文书名用来测试文件名处理功能这个标题包含了很多汉字字符'.repeat(3) +
'.epub';
const result = makeSafeFilename(longTitle);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
// Verify UTF-8 validity
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
});
it('should handle complex real-world Chinese book title', () => {
const longTitle =
'榎宮祐 - NO GAME NO LIFE 遊戲人生 02 遊戲玩家兄妹似乎盯上獸耳娘的國家了'.repeat(3);
const result = makeSafeFilename(longTitle);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
// Verify UTF-8 validity
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
});
it('should handle complex real-world Chinese book titles with varying zero padding', () => {
const testCases = Array.from({ length: 31 }, (_, i) => {
const padding = '0'.repeat(i) + '2';
return `榎宮祐 - ${'NO GAME NO LIFE'.repeat(12)} 遊戲人生 ${padding} 遊戲玩家兄妹似乎盯上獸耳娘的國家了`;
});
for (const longTitle of testCases) {
const result = makeSafeFilename(longTitle);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
}
});
it('should handle right-to-left text (Arabic)', () => {
const arabicName = 'كتاب'.repeat(50) + '.pdf'; // Over 250 bytes
const result = makeSafeFilename(arabicName);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
});
it('should handle Cyrillic characters', () => {
const cyrillicName = 'Книга'.repeat(60) + '.txt'; // Over 250 bytes
const result = makeSafeFilename(cyrillicName);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
});
it('should handle Thai characters', () => {
const thaiName = 'หนังสือ'.repeat(50) + '.pdf'; // Over 250 bytes
const result = makeSafeFilename(thaiName);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
});
});
describe('Combined sanitization and truncation', () => {
it('should sanitize and truncate in correct order', () => {
const unsafeLongName = '<'.repeat(260) + '.txt';
const result = makeSafeFilename(unsafeLongName);
// Should replace < with _, then truncate
expect(result).not.toContain('<');
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
});
it('should handle reserved name that needs truncation', () => {
// Edge case: reserved name with very long content
const longReservedLike = 'CON' + 'x'.repeat(260);
const result = makeSafeFilename(longReservedLike);
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
});
it('should sanitize, handle reserved names, and truncate multi-byte characters', () => {
const complexName = 'CON:文件'.repeat(50) + '😀'.repeat(20);
const result = makeSafeFilename(complexName);
// Should not contain unsafe characters
expect(result).not.toContain(':');
// Should be within byte limit
const byteLength = new TextEncoder().encode(result).length;
expect(byteLength).toBeLessThanOrEqual(250);
// Should be valid UTF-8
const encoded = new TextEncoder().encode(result);
const decoded = new TextDecoder().decode(encoded);
expect(decoded).toBe(result);
expect(decoded).not.toContain('');
});
});
describe('Real-world scenarios', () => {
it('should handle typical book title with author', () => {
const bookTitle = 'The Great Gatsby - F. Scott Fitzgerald.epub';
expect(makeSafeFilename(bookTitle)).toBe('The Great Gatsby - F. Scott Fitzgerald.epub');
});
it('should handle Chinese book with long title', () => {
const chineseBook = '红楼梦:中国古典文学四大名著之一(全120回完整版).epub';
const result = makeSafeFilename(chineseBook);
expect(result).toBe(chineseBook); // Should fit within 250 bytes
});
it('should handle Japanese light novel title', () => {
const japaneseTitle = 'ソードアート・オンライン:アリシゼーション編.epub';
expect(makeSafeFilename(japaneseTitle)).toBe(japaneseTitle);
});
it('should handle filename with unsafe characters and emoji', () => {
const unsafeEmoji = '📚 Book: "Title" <Part 1>.epub';
const result = makeSafeFilename(unsafeEmoji);
expect(result).toBe('📚 Book_ _Title_ _Part 1_.epub');
});
it('should handle Windows-style path in filename', () => {
const windowsPath = 'C:\\Users\\Documents\\book.pdf';
const result = makeSafeFilename(windowsPath);
expect(result).toBe('C__Users_Documents_book.pdf');
});
it('should handle URL in filename', () => {
const url = 'https://example.com/book.pdf';
const result = makeSafeFilename(url);
expect(result).toBe('https___example.com_book.pdf');
});
});
});
@@ -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 { 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 });
}
}
@@ -103,6 +103,7 @@ async function handleRequest(request: NextRequest, method: 'GET' | 'HEAD') {
return new NextResponse(null, {
status: 200,
headers: {
...Object.fromEntries(response.headers.entries()),
'Content-Type': contentType,
'Content-Length': contentLength || '',
'Cache-Control': 'public, max-age=300',
@@ -119,6 +120,7 @@ async function handleRequest(request: NextRequest, method: 'GET' | 'HEAD') {
return new NextResponse(response.body, {
status: 200,
headers: {
...Object.fromEntries(response.headers.entries()),
'Content-Type': contentType,
'X-Content-Length': contentLength || '',
'Cache-Control': 'public, max-age=300',
@@ -132,10 +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: {
...proxyHeaders,
'Content-Type': contentType,
'Content-Length': length.toString(),
'Cache-Control': 'public, max-age=300',
+19 -9
View File
@@ -6,8 +6,7 @@ import { useRouter } from 'next/navigation';
import { Auth } from '@supabase/auth-ui-react';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import { FcGoogle } from 'react-icons/fc';
import { FaApple } from 'react-icons/fa';
import { FaGithub } from 'react-icons/fa';
import { FaApple, FaGithub, FaDiscord } from 'react-icons/fa';
import { IoArrowBack } from 'react-icons/io5';
import { useAuth } from '@/context/AuthContext';
@@ -29,7 +28,7 @@ import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
import WindowButtons from '@/components/WindowButtons';
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github' | 'discord';
interface SingleInstancePayload {
args: string[];
@@ -232,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 = () => {
@@ -390,7 +394,7 @@ export default function AuthPage() {
provider='google'
handleSignIn={tauriSignIn}
Icon={FcGoogle}
label={_('Sign in with Google')}
label={_('Sign in with {{provider}}', { provider: 'Google' })}
/>
<ProviderLogin
provider='apple'
@@ -398,15 +402,21 @@ export default function AuthPage() {
appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn
}
Icon={FaApple}
label={_('Sign in with Apple')}
label={_('Sign in with {{provider}}', { provider: 'Apple' })}
/>
<ProviderLogin
provider='github'
handleSignIn={tauriSignIn}
Icon={FaGithub}
label={_('Sign in with GitHub')}
label={_('Sign in with {{provider}}', { provider: 'GitHub' })}
/>
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
<ProviderLogin
provider='discord'
handleSignIn={tauriSignIn}
Icon={FaDiscord}
label={_('Sign in with {{provider}}', { provider: 'Discord' })}
/>
<hr aria-hidden='true' className='border-base-300 my-3 mt-6 w-64 border-t' />
<div className='w-full'>
<Auth
supabaseClient={supabase}
@@ -434,7 +444,7 @@ export default function AuthPage() {
appearance={{ theme: ThemeSupa }}
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={['google', 'apple', 'github']}
providers={['google', 'apple', 'github', 'discord']}
redirectTo={getWebRedirectTo()}
localization={getAuthLocalization()}
/>
Binary file not shown.
Binary file not shown.
@@ -11,10 +11,11 @@ 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';
import { formatAuthors } from '@/utils/book';
import { formatAuthors, formatDescription } from '@/utils/book';
import ReadingProgress from './ReadingProgress';
import BookCover from '@/components/BookCover';
@@ -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 (
@@ -91,7 +93,7 @@ const BookItem: React.FC<BookItemProps> = ({
className={clsx(
'flex w-full flex-col p-0',
mode === 'grid' && 'pt-2',
mode === 'list' && 'py-2',
mode === 'list' && 'gap-2 py-0',
)}
>
<div className={clsx('min-w-0 flex-1', mode === 'list' && 'flex flex-col gap-2')}>
@@ -110,14 +112,22 @@ const BookItem: React.FC<BookItemProps> = ({
</p>
)}
</div>
{mode === 'list' && (
<h4 className='text-neutral-content line-clamp-1 text-sm'>
{formatDescription(book.metadata?.description)}
</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
@@ -150,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={() => {
@@ -164,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,19 +3,33 @@ 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';
import { useSettingsStore } from '@/store/settingsStore';
import { useLibraryStore } from '@/store/libraryStore';
import { useTransferStore } from '@/store/transferStore';
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';
@@ -24,7 +38,7 @@ import ModalPortal from '@/components/ModalPortal';
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
import SelectModeActions from './SelectModeActions';
import GroupingModal from './GroupingModal';
import TransferQueuePanel from './TransferQueuePanel';
import SetStatusAlert from './SetStatusAlert';
interface BookshelfProps {
libraryBooks: Book[];
@@ -61,34 +75,35 @@ const Bookshelf: React.FC<BookshelfProps> = ({
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') || '');
const abortDeletionRef = useRef(false);
const isImportingBook = useRef(false);
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();
const { isTransferQueueOpen } = useTransferStore();
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
const updateUrlParams = useCallback(
@@ -103,8 +118,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');
@@ -124,13 +140,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) {
@@ -141,24 +177,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;
@@ -223,9 +295,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const confirmDelete = async () => {
const books = getBooksToDelete();
const concurrency = 4;
const concurrency = 20;
for (let i = 0; i < books.length; i += concurrency) {
if (abortDeletionRef.current) {
abortDeletionRef.current = false;
break;
}
const batch = books.slice(i, i + concurrency);
await Promise.all(batch.map((book) => handleBookDelete(book, false)));
}
@@ -246,6 +322,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);
@@ -317,6 +426,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
handleBookDelete={handleBookDelete}
handleSetSelectMode={handleSetSelectMode}
handleShowDetailsBook={handleShowDetailsBook}
handleUpdateReadingStatus={handleUpdateReadingStatus}
transferProgress={
'hash' in item ? booksTransferProgress[(item as Book).hash] || null : null
}
@@ -362,6 +472,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
onOpen={openSelectedBooks}
onGroup={groupSelectedBooks}
onDetails={openBookDetails}
onStatus={showStatusSelection}
onDelete={deleteSelectedBooks}
onCancel={() => handleSetSelectMode(false)}
/>
@@ -383,11 +494,6 @@ const Bookshelf: React.FC<BookshelfProps> = ({
/>
</ModalPortal>
)}
{isTransferQueueOpen && (
<ModalPortal>
<TransferQueuePanel />
</ModalPortal>
)}
{showDeleteAlert && (
<div
className={clsx('delete-alert fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
@@ -401,6 +507,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
count: getBooksToDelete().length,
})}
onCancel={() => {
abortDeletionRef.current = true;
setShowDeleteAlert(false);
setShowSelectModeActions(true);
}}
@@ -408,6 +515,17 @@ const Bookshelf: React.FC<BookshelfProps> = ({
/>
</div>
)}
{showStatusAlert && (
<SetStatusAlert
selectedCount={getSelectedBooks().length}
safeAreaBottom={safeAreaInsets?.bottom || 0}
onCancel={() => {
setShowStatusAlert(false);
setShowSelectModeActions(true);
}}
onUpdateStatus={updateBooksStatus}
/>
)}
</div>
);
};
@@ -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,7 @@ interface BookshelfItemProps {
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
handleUpdateReadingStatus: (book: Book, status: ReadingStatus | undefined) => void;
}
const BookshelfItem: React.FC<BookshelfItemProps> = ({
@@ -106,6 +116,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
handleBookDownload,
handleSetSelectMode,
handleShowDetailsBook,
handleUpdateReadingStatus,
}) => {
const _ = useTranslation();
const router = useRouter();
@@ -201,6 +212,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 +264,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,
@@ -149,7 +151,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 +211,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,107 @@
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:
'bg-amber-500/15 text-amber-600 dark:text-amber-400 hover:bg-amber-500/25 border-amber-500/20',
},
{
label: _('Mark as Finished'),
status: 'finished' as ReadingStatus,
className: 'bg-success/15 text-success hover:bg-success/25 border-success/20',
},
{
label: _('Clear Status'),
status: undefined,
className: 'bg-base-300 text-base-content hover:bg-base-content/10 border-base-content/10',
},
];
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(
'border-base-content/10 hidden items-center gap-2 rounded-full border px-4 py-2',
'bg-base-300 text-base-content shadow-sm',
'hover:bg-base-content/10 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