Compare commits

..

164 Commits

Author SHA1 Message Date
Huang Xin 1ef126f820 release: version 0.9.69 (#1733) 2025-08-02 03:36:10 +02:00
Huang Xin 1788b86ced chore: bump to upstream foliate-js (#1732) 2025-08-02 03:24:58 +02:00
Huang Xin 3111e93054 layout: fix unintended reader view scroll when highlighting, closes #1716 (#1731) 2025-08-02 02:58:58 +02:00
Huang Xin 288abc678c refactor: unified path resolver on all platforms (#1730) 2025-08-01 16:51:17 +02:00
Huang Xin 7e559c5b90 metadata: fix formatting for subjects, closes #1724 (#1727) 2025-08-01 00:19:58 +08:00
Huang Xin eaaeb844f3 txt: fix language detection when parsing TXT files, closes #1720 (#1723) 2025-07-31 08:52:25 +02:00
Huang Xin 097fde203d css: overriding book color also applies to the font elements (#1721) 2025-07-31 04:40:08 +02:00
Huang Xin efd85e67d9 api: production for apple iap verifier (#1719) 2025-07-31 01:41:55 +02:00
Huang Xin bd4bf459e3 epub: fix some images are not displayed in EPUBs, closes #1709 (#1718) 2025-07-30 14:11:55 +02:00
Huang Xin 8fb1745dbb config: add an option to disable double click, closes #1713 (#1715) 2025-07-30 11:35:07 +02:00
Huang Xin 47c6d6d058 layout: fix cover image width in fit mode in list view (#1714) 2025-07-30 11:29:31 +02:00
Huang Xin 564693d456 security: add updater permission for standalone reader window, closes #1710 (#1712) 2025-07-30 05:00:46 +02:00
Huang Xin aa3cd2a0c5 txt: trim punctuations in parsed author names (#1707)
* txt: trim punctuations in parsed author names

* release: version 0.9.68
2025-07-29 20:50:21 +02:00
Huang Xin c581f8ab47 txt: more robust TXT parsing (#1706) 2025-07-29 20:06:55 +02:00
Huang Xin 63ed763499 api: handle downloading books of sanitized titles (#1705) 2025-07-29 18:23:19 +08:00
Huang Xin 213fc0d21e api: handle more string sanitization (#1702) 2025-07-28 19:12:42 +02:00
Huang Xin 9bc1494582 api: sanitize string before saving into db (#1701) 2025-07-28 18:44:26 +02:00
Huang Xin 33b2ba1609 chore: add legal links in about window and user page (#1700) 2025-07-28 18:19:45 +02:00
Huang Xin b9add62ba7 translation: skip translating pre, code and math tags, closes #1693 (#1698) 2025-07-28 09:18:19 +02:00
Huang Xin 23fa2dc8b2 css: fix hard-coded font color, closes #1695 (#1697) 2025-07-28 08:36:10 +02:00
Huang Xin 67ac09a290 premium: increased cloud sync storage for premium users (#1696) 2025-07-28 08:25:22 +02:00
Huang Xin f10c14ae96 api: batch updating daily usage key in KV (#1694) 2025-07-28 06:25:47 +02:00
Huang Xin 438d3ae0c2 config: default to open file with new window (#1691) 2025-07-27 19:12:19 +02:00
Huang Xin 842062fe44 perf: multi-part download with range access (#1690) 2025-07-27 18:56:41 +02:00
Huang Xin d01897d62a fix: load backup library data if main library data is unavailable, closes #1672 (#1689) 2025-07-27 16:48:58 +02:00
Huang Xin 6eeb8e7580 layout: fix insets for double borders and add book spine decorator (#1688) 2025-07-27 14:36:55 +02:00
Huang Xin 9cebadba57 font: fix broken links for online CJK fonts (#1687) 2025-07-27 08:25:25 +02:00
Huang Xin e5ec9abca3 chore: suppress warnings from old objc crate (#1686) 2025-07-27 08:03:45 +02:00
Huang Xin 5e7f09d5c8 perf: eliminate redundant re-renders of book cover components (#1685) 2025-07-27 07:02:05 +02:00
Huang Xin ffa193d116 fix: importing books from url and set downloaded timestamp for reimported books (#1684) 2025-07-26 14:34:51 +02:00
Huang Xin 686f8988bc api: use node api endpoint for iap verifying (#1683) 2025-07-26 13:34:36 +02:00
Huang Xin 8d5c98004b api: ensure proper string decoded on edge runtimes (#1680) 2025-07-25 21:13:01 +02:00
Huang Xin 214411919f iap: use sandbox environment for testflight (#1679) 2025-07-25 19:49:53 +02:00
Huang Xin 42c2825e90 feat: enable IAP for upgrading to Readest Premium on iOS (#1678) 2025-07-25 19:08:53 +02:00
Huang Xin 8974a87168 feat: add IAP server api (#1676) 2025-07-25 14:44:56 +02:00
Huang Xin f3e9983742 feat: add IAP in native bridge plugin for iOS (#1673) 2025-07-24 14:54:17 +02:00
初五 beaf034035 docs: fix outdated repository links and spelling error (#1669)
* docs: update outdated repository links in CONTRIBUTING.md

- Update issue tracker link from chrox/readest to readest/readest    
- Update new issue link from chrox/readest to readest/readest

* fix: correct spelling of "quota" in release notes

- Fix typo "quata" -> "quota" in 0.9.30 release notes
2025-07-23 20:00:55 +02:00
Huang Xin a5f18ab024 release: version 0.9.67 (#1664) 2025-07-22 16:11:00 +02:00
Huang Xin d1f5934947 library: various fixes on metadata editor and bookshelf (#1663)
* i18n: update translations

* library: various fixes on metadata editor and bookshelf

* chore: enable parallel web builds checking
2025-07-22 16:04:44 +02:00
Huang Xin f03d6f6861 feat: search in book format, group names and descriptions (#1662) 2025-07-22 11:40:14 +02:00
Huang Xin 20523b7eaf PDF: Support custom background theming for PDF files, closes #1649 (#1661) 2025-07-22 11:34:38 +02:00
Huang Xin f31c9b2a98 layout: fix hardcoded image layout in fixed layout documents (#1660) 2025-07-22 10:40:21 +02:00
Huang Xin 93071e727f linux: fixed multiple instances created in OAuth, closes #1654 (#1659) 2025-07-22 09:00:19 +02:00
Huang Xin 2d12210606 css: multiply img color in light mode when overriding book color (#1656)
This fixed image color issue for EPUBs in #1649.
2025-07-21 18:17:16 +02:00
Huang Xin 88ccc528d4 layout: hover header to show traffic light window control on macOS, closes #1645 (#1653) 2025-07-21 17:16:58 +02:00
Yurii Kostyukov 9d3fc07865 feat: Added Yandex Translator (#1652) 2025-07-21 17:13:13 +02:00
Huang Xin 6d465a40fb fix: don't use comma as separator when parsing filenames, closes #1622 (#1650) 2025-07-21 15:32:10 +02:00
Huang Xin 01c7e4c2e2 chore: fixed failed AppImage builds for Linux 2025-07-21 20:39:57 +08:00
Huang Xin 4005858242 release: version 0.9.66 (#1643) 2025-07-20 23:12:40 +02:00
Huang Xin c24f951375 fix: set xdg-mime with mime type other than scheme, closes #1621 (#1641) 2025-07-21 05:02:22 +08:00
Huang Xin dbe1efd1e9 tts: convert iso6392 language code to iso6391 to filter tts voices, closes #1627 (#1639) 2025-07-20 22:10:14 +02:00
Huang Xin 0299cce343 library: fix book not redownloaded for the redownload button in detail modal, closes #1628 (#1638) 2025-07-20 21:00:29 +02:00
Huang Xin ca9f504329 layout: fix layout for auth and user page (#1637) 2025-07-20 20:05:55 +02:00
Huang Xin df3527ceca fix: chaining file open with OS opening the highest precedence, closes #1622 (#1636) 2025-07-20 17:09:19 +02:00
Huang Xin 73d0838291 doc: run a preview build to identify compatibility issues between OpenNext, Next.js, and Supabase (#1635) 2025-07-20 11:32:00 +02:00
Huang Xin 8e3574509e chore: downgrade next.js to 15.3 (#1634)
Until https://github.com/opennextjs/opennextjs-cloudflare/issues/667 is fixed.
2025-07-20 11:12:06 +02:00
Huang Xin 9bdf375304 api: fix cors for api with new nextjs version (#1633) 2025-07-20 10:24:28 +02:00
Huang Xin effa922597 chore: bump tauri, next and zustand to latest versions (#1631) 2025-07-20 09:42:32 +02:00
Huang Xin fe484257a0 doc: add deepwiki badge and link (#1630) 2025-07-20 07:02:14 +02:00
Huang Xin cc0cd9a114 layout: fix nested toc items not expanded in very long toc list, closes #1625 (#1629) 2025-07-20 06:42:31 +02:00
Huang Xin 916d63806e chore: fix loading chunk error of optional chaining for Android WebView below 80 (#1626) 2025-07-19 20:24:30 +02:00
Huang Xin 59d0118067 chore: handle ChunkLoadError by refreshing page (#1619) 2025-07-19 06:37:13 +02:00
Huang Xin 957c6b9f32 fix: update book cover image from metadata in sidebar (#1615) 2025-07-18 19:11:02 +02:00
Huang Xin 9c5a1147fa release: version 0.9.65 (#1614) 2025-07-18 18:31:38 +02:00
Huang Xin 60ad6706ff ios: skip context menu when long-press on book cover, closes #1612 (#1613) 2025-07-18 18:22:59 +02:00
Huang Xin d9199d1182 sync: also sync book metadata (#1611) 2025-07-18 17:59:05 +02:00
Huang Xin 219edb9faf translation: fix translation not working for table of contents (#1610) 2025-07-18 11:54:26 +02:00
Huang Xin 30a0227ef3 layout: unset text indent inside of list elements (#1609) 2025-07-18 11:26:28 +02:00
Huang Xin 29b5a7cb88 tts: skip text to speech for rubys and footnote anchors, closes #1334 (#1608) 2025-07-18 11:10:12 +02:00
Huang Xin abb727870e tts: handle invalid language code and show no voices hints, closes #1579 (#1607) 2025-07-18 09:27:51 +02:00
Dave Vasilevsky af84a8b110 chore: match dev-web command to docs (#1606) 2025-07-18 06:50:41 +02:00
Huang Xin 0dea5d3094 layout: maintain the layout of anchor elements while increasing their tap target size, closes #1603 (#1605) 2025-07-18 06:26:54 +02:00
Huang Xin 527b95521e fix: replace fallback book cover with new cover image (#1604) 2025-07-18 06:03:22 +02:00
Huang Xin 396b23ddb4 css: named container classes for easier css customization, closes #1598 (#1600) 2025-07-17 20:17:51 +02:00
Huang Xin ee0d1549bd layout: add window borders on Linux, closes #1570 (#1599) 2025-07-18 01:40:12 +08:00
Huang Xin 6da6b85530 feat: support multiple reader windows on desktop, closes #259 (#1596) 2025-07-17 10:21:37 +02:00
StepanSad cde055dcb7 Update translation.json (#1595) 2025-07-17 05:03:39 +02:00
Huang Xin 08957ce3d7 release: version 0.9.64 (#1589) 2025-07-15 19:40:03 +02:00
Huang Xin 33515f1f09 fix: save custom cover images in apps (#1588) 2025-07-15 19:33:13 +02:00
Huang Xin e3c411f043 feat: support to edit book metadata, closes #753 (#1583) 2025-07-14 19:46:57 +02:00
StepanSad eda71725da Update translation.json (#1581) 2025-07-14 20:09:46 +08:00
StepanSad f19f45c00e Fixed bad Ukrainian translation. (#1576) 2025-07-12 18:01:41 +02:00
Huang Xin 6fb7897092 chore: flatpak with custom oauth (#1574) 2025-07-11 11:28:43 +08:00
Huang Xin 2de941d7fb doc: add appdata metainfo for Flathub (#1569) 2025-07-10 14:10:40 +02:00
Huang Xin 3e4b7ebb13 css: remove unintended indent for images, closes #1567 (#1568)
* chore: target Android SDK to version 36

* css: remove unintended indent for images, closes #1567
2025-07-10 12:05:07 +02:00
Huang Xin 2b52f82b57 fix: bookmark ribbon is now placed correctly when sidebar is pinned (#1565) 2025-07-09 18:23:55 +02:00
Huang Xin 9fb7e80757 fix: pull down to refresh is now more smoother and responsive (#1564) 2025-07-09 18:01:33 +02:00
Huang Xin 38869ce438 search: more responsive full-text search, closes #1558 (#1562) 2025-07-09 12:36:22 +02:00
Huang Xin 1dbf93c1cf css: lighten the highlight color in dark mode, closes #1489 (#1561) 2025-07-09 10:41:40 +02:00
Huang Xin aafcab8232 layout: fix dimension of inline images, closes #1555 (#1560) 2025-07-09 09:52:51 +02:00
Huang Xin bcfe1b8830 fix: invalid href handling in EPUB (#1557) 2025-07-09 04:57:21 +02:00
Huang Xin 5db1847f7e layout: add window border on Windows 10, closes #1551 (#1556) 2025-07-08 17:24:43 +02:00
Huang Xin 165ca4a6e9 chore: add fastlane action to release to Google Play (#1554) 2025-07-08 16:18:53 +02:00
Huang Xin 9556e1305f updater: show update notes for new releases (#1552) 2025-07-08 12:13:47 +02:00
Huang Xin f5b686abd9 release: version 0.9.63 (#1550) 2025-07-07 15:07:35 +02:00
Huang Xin 74e1b733a9 layout: dismiss system context menu popup when selecting text on iPad, closes #1485 (#1549) 2025-07-07 14:57:04 +02:00
Huang Xin a9e0090275 i18n: add Thai (th-TH) translations (#1548) 2025-07-07 11:21:41 +02:00
Huang Xin 68d57da9ca perf: speed up opening for large MOBI books, closes #1544 (#1547) 2025-07-07 10:15:54 +02:00
Bhscer 3cef529ebd feat: support deleting only the cloud backup of a book (#1546)
* feat: support delete the cloud storage of a book

* feat: merge to delete function

* fix: change a logic
2025-07-07 09:37:11 +02:00
Huang Xin 85ac399e2c tts: fix some direct text being skipped in tts, closes #1515 (#1543) 2025-07-06 12:13:40 +02:00
Huang Xin e8d09773a2 fix: handling toc links for some mobi books, closes #1525 (#1542) 2025-07-06 08:49:47 +02:00
Huang Xin 23905407ca doc: layout for the download badges (#1541) 2025-07-06 05:42:29 +02:00
Huang Xin 2ab1304b6d doc: update Readme for mobile apps downloads (#1540) 2025-07-06 05:20:30 +02:00
Huang Xin d94236e127 layout: fix grid cover image height in crop mode (#1537)
Support setting arbitrary columns with custom UI css:
```
.bookshelf-items {
  grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
}
```
2025-07-05 17:29:33 +02:00
Huang Xin 630025c123 chore: update Android target sdk to 35 (#1534) 2025-07-05 08:49:41 +02:00
Huang Xin 2b73c5a85a perf: lazy loading fonts in fonts list (#1533) 2025-07-05 07:32:22 +02:00
Huang Xin a03f2c585f layout: fix overlay scrollbar with virtualized list, closes #1524 (#1530) 2025-07-04 19:36:13 +02:00
Huang Xin 8a0216dcf8 fix: links in mobi are now properly followed, closes #1503 (#1528) 2025-07-04 07:47:27 +02:00
Huang Xin 59013be586 settings: remove unsupported screen orientation lock on iPad, closes #1520 (#1522) 2025-07-03 13:41:42 +02:00
Huang Xin c6a638dbfd compat: footnote img size, closes #1518 (#1519) 2025-07-02 18:55:09 +02:00
Huang Xin e54440ad80 chore: retry workflow for r2 uploading (#1517) 2025-07-02 17:37:05 +02:00
Huang Xin 446b014f7d chore: upload to r2 after release with retry (#1516) 2025-07-02 17:23:12 +02:00
Huang Xin 9190433ea1 subscription: fix action button in plan card (#1514) 2025-07-02 16:48:32 +02:00
Huang Xin d78d135600 release: version 0.9.62 (#1513) 2025-07-02 15:02:20 +02:00
Huang Xin 04b6c89e58 layout: refine user profile page (#1512) 2025-07-02 13:50:22 +02:00
Huang Xin 8eefba2bee feat: add an option to show/hide original text in translation, also closes #1492 (#1511) 2025-07-02 07:27:05 +02:00
Huang Xin a254139200 layout: extend drag area outward so the scrollbar remains clickable (#1509) 2025-07-01 09:55:09 +02:00
Huang Xin 0514bed5b7 css: fix for legacy epub image dimension, closes #1507 (#1508) 2025-07-01 09:46:27 +02:00
Huang Xin 49978c5e3f layout: add overlay scrollbar for TOC on Android (#1506) 2025-07-01 09:17:18 +02:00
Huang Xin b959bf070c feat: add upgrade to premium option in settings menu (#1505) 2025-07-01 09:15:25 +02:00
Huang Xin 49e6877810 feat: add option to toggle Parallel Reading on/off when viewing multiple books, closes #1496 (#1504) 2025-06-30 19:06:59 +02:00
Huang Xin bb759597e9 shortcuts: add ESC to dismiss search bar, closes #1495 (#1502) 2025-06-30 16:13:43 +02:00
Huang Xin bf0bb80009 api: fix for supabase RLS (#1501) 2025-06-30 15:08:53 +02:00
Huang Xin 5d61a9808f feat: add subscription management for tauri platforms (#1499) 2025-06-30 10:04:57 +02:00
Huang Xin bc36da6b25 feat: add subscription management for web (#1494) 2025-06-29 18:33:36 +02:00
Huang Xin 280cb75ca4 api: cors in middleware (#1493) 2025-06-29 17:00:52 +02:00
Huang Xin d5d0a9abd3 api: add subscription API (#1491) 2025-06-29 14:38:04 +02:00
Huang Xin 9f343992b2 settings: add three new CJK fonts (#1484) 2025-06-27 12:31:01 +02:00
Huang Xin 7ab7058dba refactor: fix fallback cover image for fitted mode (#1483) 2025-06-27 10:28:38 +02:00
Huang Xin c3113ade13 chore: fix uninlined format args warning (#1482) 2025-06-27 10:27:21 +02:00
Huang Xin 7737c05c8f layout: apply custom css also to the library page, tweaks on fitted cover images (#1481) 2025-06-27 10:01:17 +02:00
Huang Xin 4fde9955b2 release: version 0.9.61 (#1479) 2025-06-26 11:27:02 +02:00
Huang Xin 3875c8f239 feat: add an option to show remaining pages in chapter (#1478) 2025-06-26 11:19:30 +02:00
Huang Xin 7bd1b03652 fix: get correct filename when importing txt files on iOS (#1477) 2025-06-26 10:21:55 +02:00
Huang Xin 73878c14e3 fix: EPUB covers occasionally fail to display in library view, closes #1029 (#1476) 2025-06-26 10:03:35 +02:00
Huang Xin bc2c3a2c9a feat: add options to reset settings in the config dialog, closes #1111 (#1475) 2025-06-26 09:59:34 +02:00
Huang Xin 460d37c656 chore: update assetlinks and deeplink for Android (#1474) 2025-06-25 14:47:50 +02:00
Huang Xin fff9e3221e chore: use default production supabase and posthog if not customized, closes #1454 (#1473) 2025-06-25 13:46:48 +02:00
Huang Xin f77fa17cbc bump: update tauri to latest dev branch, closes #1468 (#1471) 2025-06-25 05:36:45 +02:00
Huang Xin d4565f7d60 feat: add an option in the view menu to crop or fit book covers of the library page (#1469) 2025-06-25 03:48:08 +02:00
Huang Xin e8b1976f4f doc: adjust the priority of the readest koplugin for Koreader (#1467) 2025-06-24 08:34:31 +02:00
Huang Xin 6b4bb64885 feat: custom css for the reader UI (#1466) 2025-06-24 08:33:49 +02:00
Huang Xin fc0c42f7db android: fix transparent icon background on Android, closes #1462 (#1464) 2025-06-24 06:54:49 +02:00
Huang Xin 41e5f27d4d layout: view menu now has a maximum width, closes #1459 (#1463)
* refactor: auth callback page

* layout: view menu now has a maximum width, closes #1459
2025-06-24 05:44:03 +02:00
Huang Xin a042b4c739 fix: longer timeout to save changes before reloading page (#1461) 2025-06-24 05:01:46 +02:00
Huang Xin a1c1a9f983 chore: bump opennextjs and wrangler to latest versions (#1460) 2025-06-24 03:22:12 +02:00
Huang Xin 423ae77160 doc: add the Support section in README (#1458) 2025-06-23 12:46:08 +02:00
Huang Xin 05c61a4b1a doc: add donation badge and link with crypto payment (#1457) 2025-06-23 11:37:13 +02:00
Huang Xin ce8f06b7fb chore: add sponsor readest link 2025-06-23 05:03:21 +02:00
Huang Xin f815844577 release: version 0.9.60 (#1452) 2025-06-22 11:24:47 +02:00
Huang Xin 7fe4d38c51 fix: allow inline scripts on Tauri platforms (#1451) 2025-06-22 11:22:39 +02:00
Huang Xin 43edcb4e9e android: dismiss the splash screen and change icon background color in task list (#1450)
* layout: various layout fix on iPad

* android: dismiss the splash screen and change icon background color in task list, closes #1424
2025-06-22 09:10:03 +02:00
Huang Xin e259b29afa fix: disable translation when the primary language is not defined, closes #1443 (#1448) 2025-06-22 07:10:22 +02:00
Huang Xin cd5f57a004 layout: ignore system top/bottom safe insets when there is no header or footer, closes #1442 (#1447) 2025-06-22 06:32:23 +02:00
Huang Xin 8c0cdb0e22 layout: tweaks on layout for iPad (#1446) 2025-06-22 05:55:40 +02:00
初五 c0df9f1cb6 doc: correct spelling 'Themeing' to 'Theming' in README (#1444) 2025-06-22 08:19:21 +08:00
Huang Xin cb1ddaeedb fix: smoother orientation change in iOS, closes #1434 (#1441) 2025-06-21 17:11:45 +02:00
Huang Xin 4447dd7b9a css: fix unintended font size adjust in scrolled mode on iOS, closes #1438 (#1440) 2025-06-21 13:25:46 +02:00
Huang Xin dcb7602837 fix: importing books on iOS at first launch is more reliable now (#1439) 2025-06-21 13:17:24 +02:00
Huang Xin 60e48742fe chore: allow download url for installers (#1437) 2025-06-21 11:13:59 +02:00
Huang Xin dd43569778 chore: download releases using cloudflare cdn (#1436) 2025-06-21 10:36:51 +02:00
Huang Xin 8433dd6950 chore: upload release artifacts to a R2 release bucket (#1435) 2025-06-21 16:24:49 +08:00
Huang Xin 379802f580 release: ready for Google Play release, closes #1001 (#1433) 2025-06-20 16:48:53 +02:00
Huang Xin 2dcb5d7c1e sync: add network timeout of sync to 5 sec (#1432)
* sync: add network timeout of sync to 5 sec

* css: minor tweaks on headings and list
2025-06-20 16:45:10 +02:00
262 changed files with 17149 additions and 4307 deletions
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: ['readest']
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+19 -2
View File
@@ -32,6 +32,11 @@ jobs:
build_web_app:
runs-on: ubuntu-latest
strategy:
matrix:
config:
- platform: 'web'
- platform: 'tauri'
steps:
- uses: actions/checkout@v4
with:
@@ -53,7 +58,19 @@ jobs:
run: |
pnpm install && pnpm setup-pdfjs
- name: build the web App
- name: run tests
working-directory: apps/readest-app
run: |
pnpm build-web
pnpm test -- --watchAll=false
- name: build the web App
if: matrix.config.platform == 'web'
working-directory: apps/readest-app
run: |
pnpm build-web && pnpm check:all
- name: build the Tauri App
if: matrix.config.platform == 'tauri'
working-directory: apps/readest-app
run: |
pnpm build && pnpm check:all
+12
View File
@@ -177,6 +177,11 @@ jobs:
pnpm tauri icon ../../data/icons/readest-book.png
git checkout .
MANIFEST="src-tauri/gen/android/app/src/main/AndroidManifest.xml"
PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST" || \
sed -i "/android.permission.INTERNET/a \ $PERMISSION_LINE" "$MANIFEST"
pushd src-tauri/gen/android
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
@@ -315,3 +320,10 @@ jobs:
draft: false,
prerelease: false
})
upload-to-r2:
needs: [get-release, update-release]
uses: ./.github/workflows/upload-to-r2.yml
with:
tag: ${{ needs.get-release.outputs.release_tag }}
secrets: inherit
+18
View File
@@ -0,0 +1,18 @@
name: Retry workflow
on:
workflow_dispatch:
inputs:
run_id:
required: true
jobs:
rerun:
runs-on: ubuntu-latest
steps:
- name: rerun ${{ inputs.run_id }}
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
gh run watch ${{ inputs.run_id }} > /dev/null 2>&1
gh run rerun ${{ inputs.run_id }} --failed
+77
View File
@@ -0,0 +1,77 @@
name: Upload Release Assets to R2
on:
workflow_call:
inputs:
tag:
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: 'Release tag name (e.g., v1.2.3)'
required: true
type: string
jobs:
upload-to-r2:
runs-on: ubuntu-latest
timeout-minutes: 3
strategy:
fail-fast: false
env:
RELEASE_R2_BUCKET: readest-releases
RELEASE_R2_ACCOUNT_ID: ${{ secrets.RELEASE_R2_ACCOUNT_ID }}
RELEASE_R2_ACCESS_KEY_ID: ${{ secrets.RELEASE_R2_ACCESS_KEY_ID }}
RELEASE_R2_SECRET_ACCESS_KEY: ${{ secrets.RELEASE_R2_SECRET_ACCESS_KEY }}
steps:
- name: Download release assets
run: |
gh release download "${{ inputs.tag }}" --repo readest/readest --dir ./release-assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install rclone
run: curl https://rclone.org/install.sh | sudo bash
- name: Configure rclone
run: |
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf <<EOF
[r2]
type = s3
provider = Cloudflare
access_key_id = $RELEASE_R2_ACCESS_KEY_ID
secret_access_key = $RELEASE_R2_SECRET_ACCESS_KEY
endpoint = https://${RELEASE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com
EOF
- name: Modify latest.json download URLs
run: |
GITHUB_BASE_URL="https://github.com/readest/readest/releases/download"
READEST_BASE_URL="https://download.readest.com/releases"
sed -i "s#${GITHUB_BASE_URL}#${READEST_BASE_URL}#g" ./release-assets/latest.json
- name: Upload to R2
run: |
mkdir releases
mv ./release-assets/latest.json releases
mv ./release-assets/release-notes.json releases
rclone copy ./release-assets r2:${RELEASE_R2_BUCKET}/releases/${{ inputs.tag }}/
rclone copy ./releases r2:${RELEASE_R2_BUCKET}/releases/
- name: Upload successful
if: success()
run: echo "Upload completed successfully"
retry-on-failure:
if: failure() && fromJSON(github.run_attempt) < 3
needs: [upload-to-r2]
runs-on: ubuntu-latest
steps:
- env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: gh workflow run retry-workflow.yml -F run_id=${{ github.run_id }}
+2
View File
@@ -38,3 +38,5 @@ next-env.d.ts
# Rust build
target
fastlane/report.xml
+3
View File
@@ -4,3 +4,6 @@
[submodule "packages/tauri"]
path = packages/tauri
url = https://github.com/readest/tauri.git
[submodule "packages/tauri-plugins"]
path = packages/tauri-plugins
url = https://github.com/readest/tauri-plugins-workspace.git
+2 -2
View File
@@ -3,14 +3,14 @@
When contributing to `Readest`, whether on GitHub or in other community spaces:
- Be respectful, civil, and open-minded.
- Before opening a new pull request, try searching through the [issue tracker](https://github.com/chrox/readest/issues) for known issues or fixes.
- Before opening a new pull request, try searching through the [issue tracker](https://github.com/readest/readest/issues) for known issues or fixes.
- If you want to make code changes based on your personal opinion(s), make sure you open an issue first describing the changes you want to make, and open a pull request only when your suggestions get approved by maintainers.
## How to Contribute
### Prerequisites
In order to not waste your time implementing a change that has already been declined, or is generally not needed, start by [opening an issue](https://github.com/chrox/readest/issues/new/choose) describing the problem you would like to solve.
In order to not waste your time implementing a change that has already been declined, or is generally not needed, start by [opening an issue](https://github.com/readest/readest/issues/new/choose) describing the problem you would like to solve.
For the best experience to build Readest for yourself, use a recent version of Node.js and Rust. Refer to the [Tauri documentation](https://v2.tauri.app/start/prerequisites/) for details on setting up the development environment prerequisites on different platforms.
Generated
+905 -801
View File
File diff suppressed because it is too large Load Diff
+26 -1
View File
@@ -2,9 +2,29 @@
members = [
"apps/readest-app/src-tauri",
"packages/tauri/crates/tauri",
"packages/tauri/crates/tauri-utils",
"packages/tauri/crates/tauri-build",
"packages/tauri-plugins/plugins/fs",
"packages/tauri-plugins/plugins/dialog",
"packages/tauri-plugins/plugins/deep-link",
]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
log = "0.4"
tauri = { version = "2", default-features = false }
tauri-build = "2"
tauri-plugin = "2"
tauri-utils = "2"
schemars = "0.8"
serde_json = "1"
thiserror = "2"
glob = "0.3"
dunce = "1"
url = "2"
[workspace.package]
authors = ["Bilingify LLC"]
homepage = "https://readest.com"
@@ -15,4 +35,9 @@ edition = "2021"
rust-version = "1.77.2"
[patch.crates-io]
tauri = { path = "packages/tauri/crates/tauri" }
tauri = { path = "packages/tauri/crates/tauri" }
tauri-utils = { path = "packages/tauri/crates/tauri-utils" }
tauri-build = { path = "packages/tauri/crates/tauri-build" }
tauri-plugin-fs = { path = "packages/tauri-plugins/plugins/fs" }
tauri-plugin-dialog = { path = "packages/tauri-plugins/plugins/dialog" }
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
+37 -3
View File
@@ -15,8 +15,11 @@
[![][badge-discord]][link-discord]
[![AGPL Licence][badge-license]](LICENSE)
[![Latest release][badge-release]][link-gh-releases]
[![Donate][badge-donate]][link-donate]
<br>
[![Last commit][badge-last-commit]][link-gh-commits]
[![Commits][badge-commit-activity]][link-gh-pulse]
[![Ask DeepWiki][badge-deepwiki]][link-deepwiki]
</div>
@@ -27,6 +30,7 @@
<a href="#downloads">Downloads</a> •
<a href="#getting-started">Getting Started</a> •
<a href="#troubleshooting">Troubleshooting</a> •
<a href="#support">Support</a> •
<a href="#license">License</a>
</p>
@@ -53,6 +57,7 @@
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
| **Translate with DeepL** | From a single sentence to the entire book—translate instantly with DeepL. | ✅ |
| **Translate with Yandex** | Instantly translate text or books using Yandex Translate. | ✅ |
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
@@ -64,8 +69,8 @@
| **Feature** | **Description** | **Priority** |
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
@@ -87,13 +92,22 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
![Wikipedia](./data/screenshots/wikipedia_vertical.png)
![Themeing Dark Mode](./data/screenshots/theming_dark_mode.png)
![Theming Dark Mode](./data/screenshots/theming_dark_mode.png)
---
## Downloads
The Readest app is available for download! 🥳 🚀
### Mobile Apps
<div align="center">
<a href="https://apps.apple.com/app/id6738622779">
<img alt="Download on the App Store" src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" style="height: 50px;" /></a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://play.google.com/store/apps/details?id=com.bilingify.readest">
<img alt="Get it on Google Play" src="https://upload.wikimedia.org/wikipedia/commons/7/78/Google_Play_Store_badge_EN.svg" style="height: 50px;" /></a>
</div>
### Platform-Specific Downloads
- macOS / iOS / iPadOS : Search for "Readest" on the [App Store][link-appstore], also available on TestFlight for beta test (send your Apple ID to <readestapp@gmail.com> to request access).
- Windows / Linux / Android: Visit [https://readest.com][link-website] or the [Releases on GitHub][link-gh-releases].
@@ -153,6 +167,8 @@ For Windows targets, “Build Tools for Visual Studio 2022” (or a higher editi
pnpm tauri dev
# or start development for the Web app
pnpm dev-web
# preview with OpenNext build for the Web app
pnpm preview
```
For Android:
@@ -243,6 +259,20 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
</p>
</a>
## Support
If Readest has been useful to you, consider supporting its development. Your contribution helps us squash bugs faster, improve performance, and keep building great features.
### How to Donate
1. **GitHub Sponsors**
Back the project directly on GitHub:
👉 [https://github.com/sponsors/readest](https://github.com/sponsors/readest)
2. **Crypto Donations**
Prefer crypto? You can donate here:
👉 [https://donate.readest.com/](https://donate.readest.com/)
## License
Readest is free software: you can redistribute it and/or modify it under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the [LICENSE](LICENSE) file for details.
@@ -277,6 +307,9 @@ The following fonts are utilized in this software, either bundled within the app
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
[badge-donate]: https://donate.readest.com/badge.svg
[badge-deepwiki]: https://deepwiki.com/badge.svg
[link-donate]: https://donate.readest.com/?tickers=btc%2Ceth%2Csol%2Cusdc
[link-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
[link-web-readest]: https://web.readest.com
@@ -288,3 +321,4 @@ The following fonts are utilized in this software, either bundled within the app
[link-parallel-read]: https://readest.com/#parallel-read
[link-koreader]: https://github.com/koreader/koreader
[link-hellogithub]: https://hellogithub.com/repository/8a5b6ade2aee461a8bd94e59200682a7
[link-deepwiki]: https://deepwiki.com/readest/readest
+8 -2
View File
@@ -2,5 +2,11 @@ PDFJS_BUILD_PATH=../../packages/foliate-js/node_modules/pdfjs-dist/legacy/build
PDFJS_FONTS_PATH=../../packages/foliate-js/node_modules/pdfjs-dist
PDFJS_STYLE_PATH=../../packages/foliate-js/vendor/pdfjs
NEXT_PUBLIC_DEV_SUPABASE_URL=https://gxkhxxxeapexynpouyjz.supabase.co
NEXT_PUBLIC_DEV_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd4a2h4eHhlYXBleHlucG91eWp6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ0MzAwNTksImV4cCI6MjA1MDAwNjA1OX0.jhinkQsimQoidsg_U59YD5ROw4PmMJQNKuyXbr4TiQA
NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64="aHR0cHM6Ly91cy5pLnBvc3Rob2cuY29t"
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThpTmRSNTNsR1A3VFM3VGh4S08="
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv"
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw=="
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_DEV_BASE64="cGtfdGVzdF81MVJmQmdLRTdSWW5pTWsxc0tDV2RUd2hMZzcySzk4eDRWcjlIdDdsRFBONngzcnpZYmhydGtNQnpDdzZKbHFaRVVITVp5eVNjVXhCZXVkcGppWTk0WXNHcDAweFlRRnRRaUU="
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_BASE64="cGtfbGl2ZV81MVFYN3dRRU5ndjJFOUxQRHpZUlE5TlJJeTNjd09EZ1AzSkNFRHRPWlFtdFJWc3Brd053ZE1NNUpIVnVPTmJWcjZ3VGFCMUNZR1pJMmRPVWppTkY0bHJvVjAwalE4TkpkdWk="
@@ -26,6 +26,7 @@ module.exports = {
'id',
'vi',
'ar',
'th',
'zh-CN',
'zh-TW',
],
+24 -3
View File
@@ -1,18 +1,22 @@
import withPWAInit from '@ducanh2912/next-pwa';
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
import withBundleAnalyzer from '@next/bundle-analyzer';
const isDev = process.env['NODE_ENV'] === 'development';
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
if (isDev) {
const { initOpenNextCloudflareForDev } = await import('@opennextjs/cloudflare');
initOpenNextCloudflareForDev();
}
const exportOutput = appPlatform !== 'web' && !isDev;
/** @type {import('next').NextConfig} */
const nextConfig = {
// Ensure Next.js uses SSG instead of SSR
// https://nextjs.org/docs/pages/building-your-application/deploying/static-exports
output: appPlatform === 'web' || isDev ? undefined : 'export',
output: exportOutput ? 'export' : undefined,
pageExtensions: exportOutput ? ['jsx', 'tsx'] : ['js', 'jsx', 'ts', 'tsx'],
// Note: This feature is required to use the Next.js Image component in SSG mode.
// See https://nextjs.org/docs/messages/export-image-api for different workarounds.
images: {
@@ -22,6 +26,19 @@ const nextConfig = {
// Configure assetPrefix or else the server won't properly resolve your assets.
assetPrefix: '',
reactStrictMode: true,
serverExternalPackages: ['isows'],
transpilePackages: !isDev
? [
'i18next-browser-languagedetector',
'react-i18next',
'i18next',
'@ducanh2912/next-pwa',
'@tauri-apps',
'highlight.js',
'foliate-js',
'marked',
]
: [],
async headers() {
return [
{
@@ -52,4 +69,8 @@ const withPWA = withPWAInit({
},
});
export default withPWA(nextConfig);
const withAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default withPWA(withAnalyzer(nextConfig));
+44 -22
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.59",
"version": "0.9.69",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -11,6 +11,7 @@
"start-web": "dotenv -e .env.web -- next start",
"i18n:extract": "i18next-scanner",
"lint": "next lint",
"test": "dotenv -e .env -e .env.test.local vitest",
"tauri": "tauri",
"prepare-public-vendor": "mkdirp ./public/vendor/pdfjs",
"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",
@@ -30,49 +31,61 @@
"build-ios-appstore": "dotenv -e .env.ios-appstore.local -- tauri ios build --export-method app-store-connect",
"release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh",
"release-ios-appstore": "dotenv -e .env.ios-appstore.local -- bash scripts/release-ios-appstore.sh",
"release-google-play": "dotenv -e .env.google-play.local -- bash scripts/release-google-play.sh",
"config-wrangler": "sed -i \"s/\\${TRANSLATIONS_KV_ID}/$TRANSLATIONS_KV_ID/g\" wrangler.toml",
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview",
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview --ip 0.0.0.0",
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
"check:optional-chaining": "count=$(grep -rno '\\?\\.[a-zA-Z_$]' .next/static/chunks/* out/_next/static/chunks/* | wc -l); if [ \"$count\" -gt 0 ]; then echo '❌ Optional chaining found in output!'; exit 1; else echo '✅ No optional chaining found.'; fi",
"check:all": "pnpm check:optional-chaining",
"build-check": "pnpm build && pnpm build-web && pnpm check:all"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.735.0",
"@aws-sdk/s3-request-presigner": "^3.735.0",
"@ducanh2912/next-pwa": "^10.2.9",
"@fabianlars/tauri-plugin-oauth": "2",
"@opennextjs/cloudflare": "^1.1.0",
"@opennextjs/cloudflare": "^1.6.1",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.4.0",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.49.10",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-haptics": "^2.2.4",
"@tauri-apps/plugin-http": "^2.4.3",
"@tauri-apps/plugin-log": "^2.4.0",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.1",
"@supabase/supabase-js": "^2.50.2",
"@tauri-apps/api": "2.6.0",
"@tauri-apps/plugin-cli": "^2.4.0",
"@tauri-apps/plugin-deep-link": "^2.4.0",
"@tauri-apps/plugin-dialog": "^2.3.0",
"@tauri-apps/plugin-fs": "^2.4.0",
"@tauri-apps/plugin-haptics": "^2.3.0",
"@tauri-apps/plugin-http": "^2.5.0",
"@tauri-apps/plugin-log": "^2.6.0",
"@tauri-apps/plugin-opener": "^2.4.0",
"@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "~2.3.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@zip.js/zip.js": "^2.7.53",
"app-store-server-api": "^0.17.1",
"aws4fetch": "^1.0.20",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"cssbeautify": "^0.3.1",
"dayjs": "^1.11.13",
"foliate-js": "workspace:*",
"franc-min": "^6.2.0",
"highlight.js": "^11.11.1",
"i18next": "^24.2.0",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"iso-639-2": "^3.0.2",
"iso-639-3": "^3.0.1",
"js-md5": "^0.8.3",
"jwt-decode": "^4.0.0",
"marked": "^15.0.12",
"next": "15.3.3",
"overlayscrollbars": "^2.11.4",
"overlayscrollbars-react": "^0.5.6",
"posthog-js": "^1.246.0",
"react": "19.0.0",
"react-color": "^2.19.3",
@@ -82,21 +95,27 @@
"react-responsive": "^10.0.0",
"react-window": "^1.8.11",
"semver": "^7.7.1",
"stripe": "^18.2.1",
"tinycolor2": "^1.6.0",
"zustand": "5.0.1"
"zod": "^4.0.8",
"zustand": "5.0.6"
},
"devDependencies": {
"@next/bundle-analyzer": "^15.4.2",
"@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "2.5.0",
"@tauri-apps/cli": "2.7.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
"@types/node": "^22.15.31",
"@types/react": "18.3.12",
"@types/react-color": "^3.0.13",
"@types/react-dom": "18.3.1",
"@types/react-window": "^1.8.8",
"@types/semver": "^7.7.0",
"@types/tinycolor2": "^1.4.6",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.20",
"cpx2": "^8.0.0",
"daisyui": "^4.12.24",
@@ -104,6 +123,7 @@
"eslint": "^9.16.0",
"eslint-config-next": "15.0.3",
"i18next-scanner": "^4.6.0",
"jsdom": "^26.1.0",
"mkdirp": "^3.0.1",
"node-env-run": "^4.0.2",
"postcss": "^8.4.49",
@@ -112,6 +132,8 @@
"raw-loader": "^4.0.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"wrangler": "^4.19.1"
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4",
"wrangler": "^4.26.0"
}
}
@@ -1,11 +1,15 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.bilingify.readest",
"sha256_cert_fingerprints": [
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82"
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82",
"E0:E7:60:55:80:8D:3A:DE:A0:D1:CF:7C:20:85:40:A3:DD:4B:E6:4D:17:5C:0F:DE:26:57:7D:9C:5B:29:5F:51"
]
}
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "تأكيد الحذف",
"Copied to notebook": "تم النسخ إلى المفكرة",
"Copy": "نسخ",
"Custom CSS": "CSS مخصص",
"Dark Mode": "الوضع الداكن",
"Default": "الافتراضي",
"Default Font": "الخط الافتراضي",
@@ -28,7 +27,6 @@
"Dictionary": "القاموس",
"Download Readest": "تحميل ريديست",
"Edit": "تحرير",
"Enter your custom CSS here...": "أدخل الـ CSS المخصص هنا...",
"Excerpts": "مقتطفات",
"Failed to import book(s): {{filenames}}": "فشل استيراد الكتاب/الكتب: {{filenames}}",
"Fast": "سريع",
@@ -48,7 +46,6 @@
"Horizontal Direction": "الاتجاه الأفقي",
"Hyphenation": "الواصلة",
"Import Books": "استيراد الكتب",
"Language:": "اللغة:",
"Layout": "التخطيط",
"Light Mode": "الوضع الفاتح",
"Loading...": "جارٍ التحميل...",
@@ -69,9 +66,8 @@
"Page": "الصفحة",
"Paging Animation": "رسوم متحركةعند الانتقال إلى صفحة جديدة",
"Paragraph": "فقرة",
"Parallel Read": "قراءة متوازية",
"Published:": "تاريخ النشر:",
"Publisher:": "الناشر:",
"Published": "تاريخ النشر",
"Publisher": "الناشر",
"Reading Progress Synced": "تمت مزامنة تقدم القراءة",
"Reload Page": "إعادة تحميل الصفحة",
"Reveal in File Explorer": "إظهار في مستكشف الملفات",
@@ -96,7 +92,7 @@
"Slow": "بطيء",
"Solarized": "سولاريزد",
"Speak": "تحدث",
"Subjects:": "المواضيع:",
"Subjects": "المواضيع",
"System Fonts": "خطوط النظام",
"TTS not supported in this device": "القراءة الصوتية غير مدعومة في هذا الجهاز",
"Theme Color": "لون السمة",
@@ -105,7 +101,7 @@
"Translated Text": "النص المترجم",
"Unknown": "غير معروف",
"Untitled": "بدون عنوان",
"Updated:": "تم التحديث:",
"Updated": "تم التحديث",
"Version {{version}}": "الإصدار {{version}}",
"Vertical Direction": "الاتجاه العمودي",
"Welcome to your library. You can import your books here and read them anytime.": "مرحبًا بكم في مكتبتكم. يمكنكم استيراد كتبكم هنا وقراءتها في أي وقت.",
@@ -128,8 +124,6 @@
"Failed to download book: {{title}}": "فشل تنزيل الكتاب: {{title}}",
"Upload Book": "تحميل الكتاب",
"Auto Upload Books to Cloud": "تحميل الكتب تلقائيًا إلى السحابة",
"{{percentage}}% of Cloud Storage Used.": "تم استخدام {{percentage}}% من مساحة التخزين السحابية.",
"Storage": "التخزين",
"Book deleted: {{title}}": "تم حذف الكتاب: {{title}}",
"Failed to delete book: {{title}}": "فشل حذف الكتاب: {{title}}",
"Check Updates on Start": "التحقق من التحديثات عند البدء",
@@ -161,7 +155,7 @@
"Create a Password": "إنشاء كلمة مرور",
"Sign up": "إنشاء حساب",
"Signing up...": "يتم الآن إنشاء الحساب...",
"Dont have an account? Sign up": "ليس لديك حساب؟ قم بالتسجيل",
"Don't have an account? Sign up": "ليس لديك حساب؟ قم بالتسجيل",
"Check your email for the confirmation link": "تحقق من بريدك الإلكتروني للحصول على رابط التأكيد",
"Signing in ...": "يتم الآن تسجيل الدخول ...",
"Send a magic link email": "إرسال رابط سحري إلى بريدك الإلكتروني",
@@ -185,35 +179,13 @@
"Sign in with GitHub": "تسجيل الدخول عبر GitHub",
"Account": "الحساب",
"Failed to delete user. Please try again later.": "فشل حذف المستخدم. يرجى المحاولة مرة أخرى لاحقًا.",
"Unlimited Offline/Online Reading": "القراءة بدون حدود للكتب المتوفر على الجهاز أو عبر الانترنت",
"Unlimited Cloud Sync Devices": "مزامنة عبر السحابة لعدد غير محدود من الأجهزة",
"Essential Text-to-Speech Voices": "أصوات أساسية لتحويل النص إلى كلام",
"DeepL Free Access": "وصول مجاني لـ DeepL",
"Community Support": "دعم المجتمع",
"500 MB Cloud Sync Space": "مزامنة سحابية بمساحة تخزين 500 ميجابايت",
"Includes All Free Tier Benefits": "تشمل جميع مزايا الفئة المجانية",
"AI Summaries": "ملخصات الذكاء الاصطناعي",
"AI Translations": "ترجمات الذكاء الاصطناعي",
"Priority Support": "دعم ذو أولوية",
"DeepL Pro Access": "وصول لميزات DeepL Pro",
"Expanded Text-to-Speech Voices": "أصوات موسعة لتحويل النص إلى كلام",
"2000 MB Cloud Sync Space": "مزامنة سحابية بمساحة تخزين 2000 ميجابايت",
"Includes All Plus Tier Benefits": "تشمل جميع مزايا فئة Plus",
"Unlimited AI Summaries": "ملخصات ذكاء اصطناعي غير محدودة",
"Unlimited AI Translations": "ترجمات ذكاء اصطناعي غير محدودة",
"Unlimited DeepL Pro Access": "وصول غير محدود لميزات DeepL Pro",
"Advanced AI Tools": "أدوات ذكاء اصطناعي متقدمة",
"Early Feature Access": "وصول مبكر للميزات",
"10 GB Cloud Sync Space": "مزامنة سحابية بمساحة تخزين 10 جيجابايت",
"Loading profile...": "جارٍ تحميل الملف الشخصي...",
"Features": "الميزات",
"Delete Account": "حذف الحساب",
"Delete Your Account?": "حذف حسابك؟",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "لا يمكن التراجع عن هذا الإجراء. سيتم حذف جميع بياناتك في السحابة بشكل دائم.",
"Delete Permanently": "حذف نهائي",
"Free Tier": "الفئة المجانية",
"Plus Tier": "فئة Plus",
"Pro Tier": "فئة Pro",
"RTL Direction": "الاتجاه من اليمين إلى اليسار",
"Maximum Column Height": "الارتفاع الأقصى للعمود",
"Maximum Column Width": "العرض الأقصى للعمود",
@@ -270,7 +242,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "جارٍ تنزيل {{downloaded}} من {{contentLength}}",
"Download finished": "اكتمل التنزيل",
"DOWNLOAD & INSTALL": "تنزيل وتثبيت",
"Changelog:": "سجل التغييرات:",
"Changelog": "سجل التغييرات",
"Software Update": "تحديث البرنامج",
"Title": "العنوان",
"Date Read": "تاريخ القراءة",
@@ -279,10 +251,9 @@
"Ascending": "تصاعدي",
"Descending": "تنازلي",
"Sort by...": "ترتيب حسب...",
"Added:": "تاريخ الإضافة:",
"Format:": "التنسيق:",
"Added": "تاريخ الإضافة",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "الوصف:",
"Description": "الوصف",
"No description available": "لا يوجد وصف متاح",
"List": "قائمة",
"Grid": "شبكة",
@@ -299,7 +270,7 @@
"Checking for updates...": "جارٍ التحقق من التحديثات...",
"Error checking for updates": "خطأ في التحقق من التحديثات",
"Details": "التفاصيل",
"File Size:": "حجم الملف:",
"File Size": "حجم الملف",
"Auto Detect": "الكشف التلقائي",
"Next Section": "القسم التالي",
"Previous Section": "القسم السابق",
@@ -355,7 +326,6 @@
"Quota Exceeded": "تجاوزت الحصة المخصصة",
"{{percentage}}% of Daily Translation Characters Used.": "تم استخدام ما نسبته {{percentage}}% من إجمالي أحرف الترجمة المسموح بها يوميًا.",
"Translation Characters": "أحرف الترجمة",
"Daily translation quota reached. Select another translate service to proceed.": "تم استنفاد حصة الترجمة اليومية. قم بتحديد خدمة ترجمة أخرى للاستمرار.",
"{{engine}}: {{count}} voices_zero": "{{engine}}: لا توجد أصوات",
"{{engine}}: {{count}} voices_one": "{{engine}}: صوت واحد",
"{{engine}}: {{count}} voices_two": "{{engine}}: صوتان",
@@ -375,5 +345,137 @@
"Right Margin (px)": "الهامش الأيمن (بكسل)",
"Left Margin (px)": "الهامش الأيسر (بكسل)",
"Column Gap (%)": "تباعد الأعمدة (%)",
"Always Show Status Bar": "دائمًا إظهار شريط الحالة"
"Always Show Status Bar": "دائمًا إظهار شريط الحالة",
"Translation Not Available": "الترجمة غير متاحة",
"Custom Content CSS": "CSS مخصص للمحتوى",
"Enter CSS for book content styling...": "أدخل CSS لتنسيق محتوى الكتاب...",
"Custom Reader UI CSS": "CSS مخصص لواجهة القارئ",
"Enter CSS for reader interface styling...": "أدخل CSS لتنسيق واجهة القارئ...",
"Crop": "قص",
"Book Covers": "أغلفة الكتب",
"Fit": "ملاءمة",
"Reset {{settings}}": "إعادة تعيين {{settings}}",
"Reset Settings": "إعادة تعيين الإعدادات",
"{{count}} pages left in chapter_zero": "لم يتبق أي صفحات في هذا الفصل",
"{{count}} pages left in chapter_one": "تبقّت صفحة واحدة في هذا الفصل",
"{{count}} pages left in chapter_two": "تبقّت صفحتان في هذا الفصل",
"{{count}} pages left in chapter_few": "تبقّت {{count}} صفحات في هذا الفصل",
"{{count}} pages left in chapter_many": "تبقّت {{count}} صفحة في هذا الفصل",
"{{count}} pages left in chapter_other": "تبقّى {{count}} صفحة في هذا الفصل",
"Show Remaining Pages": "عرض الصفحات المتبقية",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "إدارة الاشتراك",
"Coming Soon": "قريبًا",
"Upgrade to {{plan}}": "الترقية إلى {{plan}}",
"Upgrade to Plus or Pro": "الترقية إلى Plus أو Pro",
"Current Plan": "الخطة الحالية",
"Plan Limits": "حدود الخطة",
"Processing your payment...": "جارٍ معالجة الدفع...",
"Please wait while we confirm your subscription.": "يرجى الانتظار بينما نقوم بتأكيد اشتراكك.",
"Payment Processing": "معالجة الدفع",
"Your payment is being processed. This usually takes a few moments.": "يتم الآن معالجة الدفع الخاص بك. عادةً ما يستغرق ذلك بضع لحظات.",
"Payment Failed": "فشل الدفع",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "تعذر علينا معالجة اشتراكك. يرجى المحاولة مرة أخرى أو التواصل مع الدعم إذا استمرت المشكلة.",
"Back to Profile": "العودة إلى الملف الشخصي",
"Subscription Successful!": "تم الاشتراك بنجاح!",
"Thank you for your subscription! Your payment has been processed successfully.": "شكرًا لاشتراكك! تم معالجة الدفع بنجاح.",
"Email:": "البريد الإلكتروني:",
"Plan:": "الخطة:",
"Amount:": "المبلغ:",
"Subscription ID:": "رقم الاشتراك:",
"Go to Library": "الانتقال إلى المكتبة",
"Need help? Contact our support team at support@readest.com": "تحتاج مساعدة؟ تواصل مع فريق الدعم على support@readest.com",
"Free Plan": "الخطة المجانية",
"month": "شهر",
"AI Translations (per day)": "ترجمات بالذكاء الاصطناعي (يوميًا)",
"Plus Plan": "خطة Plus",
"Includes All Free Plan Benefits": "تتضمن جميع مزايا الخطة المجانية",
"Pro Plan": "خطة Pro",
"More AI Translations": "المزيد من الترجمات بالذكاء الاصطناعي",
"Complete Your Subscription": "أكمل اشتراكك",
"{{percentage}}% of Cloud Sync Space Used.": "تم استخدام {{percentage}}% من مساحة المزامنة السحابية.",
"Cloud Sync Storage": "مساحة المزامنة السحابية",
"Parallel Read": "القراءة المتزامنة",
"Disable": "تعطيل",
"Enable": "تمكين",
"Upgrade to Readest Premium": "ترقية إلى Readest Premium",
"Show Source Text": "عرض النص المصدر",
"Cross-Platform Sync": "مزامنة عبر الأجهزة",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "زامن مكتبتك وتقدمك وملاحظاتك عبر جميع أجهزتك بسهولة.",
"Customizable Reading": "قراءة قابلة للتخصيص",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "خصص الخطوط والتنسيقات والألوان لتجربة قراءة مثالية.",
"AI Read Aloud": "القراءة بالصوت عبر الذكاء الاصطناعي",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "استمع للكتب بأصوات طبيعية بدون استخدام اليدين.",
"AI Translations": "ترجمة ذكية",
"Connect with fellow readers and get help fast in our friendly community channels.": "تواصل مع القراء واحصل على الدعم بسرعة.",
"Unlimited AI Read Aloud Hours": "ساعات قراءة غير محدودة",
"Listen without limits—convert as much text as you like into immersive audio.": "استمع بلا حدود وحوّل النص لصوت بسهولة.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "استمتع بترجمة موسعة وخيارات متقدمة.",
"DeepL Pro Access": "وصول DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "احصل على دعم سريع ومخصص عند الحاجة.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "تم استهلاك حصة الترجمة اليومية. قم بالترقية للاستمرار.",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "ترجم أي نص فورًا بقوة Google أو Azure أو DeepL—افهم المحتوى بأي لغة.",
"Includes All Plus Plan Benefits": "يشمل جميع مزايا خطة بلس",
"Early Feature Access": "الوصول المبكر للميزات",
"Be the first to explore new features, updates, and innovations before anyone else.": "كن أول من يستكشف الميزات والتحديثات والابتكارات الجديدة.",
"Advanced AI Tools": "أدوات الذكاء الاصطناعي المتقدمة",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "استخدم أدوات الذكاء الاصطناعي القوية للقراءة الذكية والترجمة واكتشاف المحتوى.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "ترجم حتى 100 ألف حرف يوميًا بأدق محرك ترجمة متاح.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "ترجم حتى 500 ألف حرف يوميًا بأدق محرك ترجمة متاح.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "احفظ واوصل لمجموعة قراءتك كاملة بـ 5 جيجابايت تخزين سحابي آمن.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "احفظ واوصل لمجموعة قراءتك كاملة بـ 20 جيجابايت تخزين سحابي آمن.",
"Deleted cloud backup of the book: {{title}}": "تم حذف النسخة الاحتياطية السحابية للكتاب: {{title}}",
"Failed to delete cloud backup of the book": "فشل في حذف النسخة الاحتياطية السحابية للكتاب",
"Are you sure to delete the cloud backup of the selected book?": "هل أنت متأكد من حذف النسخة الاحتياطية السحابية للكتاب المحدد؟",
"What's New in Readest": "ما الجديد في Readest",
"Enter book title": "أدخل عنوان الكتاب",
"Subtitle": "العنوان الفرعي",
"Enter book subtitle": "أدخل العنوان الفرعي للكتاب",
"Enter author name": "أدخل اسم المؤلف",
"Series": "السلسلة",
"Enter series name": "أدخل اسم السلسلة",
"Series Index": "رقم الكتاب في السلسلة",
"Enter series index": "أدخل رقم الكتاب في السلسلة",
"Total in Series": "العدد الكلي في السلسلة",
"Enter total books in series": "أدخل العدد الكلي للكتب في السلسلة",
"Enter publisher": "أدخل اسم الناشر",
"Publication Date": "تاريخ النشر",
"Identifier": "المعرف",
"Enter book description": "أدخل وصف الكتاب",
"Change cover image": "تغيير صورة الغلاف",
"Replace": "استبدال",
"Unlock cover": "إلغاء قفل الغلاف",
"Lock cover": "قفل الغلاف",
"Auto-Retrieve Metadata": "استرجاع البيانات الوصفية تلقائياً",
"Auto-Retrieve": "استرجاع تلقائي",
"Unlock all fields": "إلغاء قفل جميع الحقول",
"Unlock All": "إلغاء قفل الكل",
"Lock all fields": "قفل جميع الحقول",
"Lock All": "قفل الكل",
"Reset": "إعادة تعيين",
"Edit Metadata": "تحرير البيانات الوصفية",
"Locked": "مقفل",
"Select Metadata Source": "اختر مصدر البيانات الوصفية",
"Keep manual input": "الاحتفاظ بالإدخال اليدوي",
"Google Books": "كتب جوجل",
"Open Library": "المكتبة المفتوحة",
"Fiction, Science, History": "خيال، علوم، تاريخ",
"Select Cover Image": "اختر صورة الغلاف",
"Open Book in New Window": "فتح الكتاب في نافذة جديدة",
"Voices for {{lang}}": "الأصوات لـ {{lang}}",
"Yandex Translate": "ترجمة Yandex",
"YYYY or YYYY-MM-DD": "YYYY أو YYYY-MM-DD",
"Restore Purchase": "استعادة الشراء",
"No purchases found to restore.": "لا توجد مشتريات لاستعادتها.",
"Failed to restore purchases.": "فشل في استعادة المشتريات.",
"Failed to manage subscription.": "فشل في إدارة الاشتراك.",
"Failed to load subscription plans.": "فشل في تحميل خطط الاشتراك.",
"year": "سنة",
"Failed to create checkout session": "فشل في إنشاء جلسة الدفع",
"Storage": "التخزين",
"Terms of Service": "شروط الخدمة",
"Privacy Policy": "سياسة الخصوصية",
"Disable Double Click": "تعطيل النقر المزدوج"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Löschen bestätigen",
"Copied to notebook": "In Notizbuch kopiert",
"Copy": "Kopieren",
"Custom CSS": "Benutzerdefiniertes CSS",
"Dark Mode": "Dunkelmodus",
"Default": "Standard",
"Default Font": "Standardschriftart",
@@ -28,7 +27,6 @@
"Dictionary": "Wörterbuch",
"Download Readest": "Readest herunterladen",
"Edit": "Bearbeiten",
"Enter your custom CSS here...": "Geben Sie hier Ihr benutzerdefiniertes CSS ein...",
"Excerpts": "Auszüge",
"Failed to import book(s): {{filenames}}": "Fehler beim Importieren von Buch/Büchern: {{filenames}}",
"Fast": "Schnell",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Horizontale Richtung",
"Hyphenation": "Silbentrennung",
"Import Books": "Bücher importieren",
"Language:": "Sprache:",
"Layout": "Layout",
"Light Mode": "Hellmodus",
"Loading...": "Wird geladen...",
@@ -69,9 +66,8 @@
"Page": "Seite",
"Paging Animation": "Blätter-Animation",
"Paragraph": "Absatz",
"Parallel Read": "Paralleles Lesen",
"Published:": "Veröffentlicht:",
"Publisher:": "Verlag:",
"Published": "Veröffentlicht",
"Publisher": "Verlag",
"Reading Progress Synced": "Lesevorgang synchronisiert",
"Reload Page": "Seite neu laden",
"Reveal in File Explorer": "Im Datei-Explorer anzeigen",
@@ -96,7 +92,7 @@
"Slow": "Langsam",
"Solarized": "Solarisiert",
"Speak": "Sprechen",
"Subjects:": "Themen:",
"Subjects": "Themen",
"System Fonts": "System-Schriftarten",
"TTS not supported in this device": "TTS wird auf diesem Gerät nicht unterstützt",
"Theme Color": "Themenfarbe",
@@ -105,7 +101,7 @@
"Translated Text": "Übersetzter Text",
"Unknown": "Unbekannt",
"Untitled": "Ohne Titel",
"Updated:": "Aktualisiert:",
"Updated": "Aktualisiert",
"Version {{version}}": "Version {{version}}",
"Vertical Direction": "Vertikale Richtung",
"Welcome to your library. You can import your books here and read them anytime.": "Willkommen in Ihrer Bibliothek. Sie können hier Ihre Bücher importieren und jederzeit lesen.",
@@ -128,8 +124,6 @@
"Failed to download book: {{title}}": "Fehler beim Herunterladen des Buches: {{title}}",
"Upload Book": "Buch hochladen",
"Auto Upload Books to Cloud": "Bücher automatisch hochladen",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% des Cloud-Speichers verwendet.",
"Storage": "Speicher",
"Book deleted: {{title}}": "Buch gelöscht: {{title}}",
"Failed to delete book: {{title}}": "Fehler beim Löschen des Buches: {{title}}",
"Check Updates on Start": "Aktualisierungen beim Start prüfen",
@@ -161,7 +155,7 @@
"Create a Password": "Passwort erstellen",
"Sign up": "Registrieren",
"Signing up...": "Registrierung...",
"Dont have an account? Sign up": "Kein Konto? Registrieren",
"Don't have an account? Sign up": "Kein Konto? Registrieren",
"Check your email for the confirmation link": "Überprüfen Sie Ihre E-Mails auf den Bestätigungslink",
"Signing in ...": "Anmeldung ...",
"Send a magic link email": "Magic-Link-E-Mail senden",
@@ -185,35 +179,13 @@
"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.",
"Unlimited Offline/Online Reading": "Unbegrenztes Offline/Online Lesen",
"Unlimited Cloud Sync Devices": "Unbegrenzte Cloud-Synchronisierungsgeräte",
"Essential Text-to-Speech Voices": "Grundlegende Text-zu-Sprache-Stimmen",
"DeepL Free Access": "Kostenloser DeepL-Zugang",
"Community Support": "Community-Support",
"500 MB Cloud Sync Space": "500 MB Cloud-Synchronisierungsspeicher",
"Includes All Free Tier Benefits": "Beinhaltet alle Vorteile der kostenlosen Stufe",
"AI Summaries": "KI-Zusammenfassungen",
"AI Translations": "KI-Übersetzungen",
"Priority Support": "Bevorzugter Support",
"DeepL Pro Access": "DeepL Pro-Zugang",
"Expanded Text-to-Speech Voices": "Erweiterte Text-zu-Sprache-Stimmen",
"2000 MB Cloud Sync Space": "2000 MB Cloud-Synchronisierungsspeicher",
"Includes All Plus Tier Benefits": "Beinhaltet alle Vorteile der Plus-Stufe",
"Unlimited AI Summaries": "Unbegrenzte KI-Zusammenfassungen",
"Unlimited AI Translations": "Unbegrenzte KI-Übersetzungen",
"Unlimited DeepL Pro Access": "Unbegrenzter DeepL Pro-Zugang",
"Advanced AI Tools": "Fortgeschrittene KI-Tools",
"Early Feature Access": "Früher Zugang zu neuen Funktionen",
"10 GB Cloud Sync Space": "10 GB Cloud-Synchronisierungsspeicher",
"Loading profile...": "Profil wird geladen...",
"Features": "Funktionen",
"Delete Account": "Konto löschen",
"Delete Your Account?": "Ihr Konto löschen?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Diese Aktion kann nicht rückgängig gemacht werden. Alle Ihre Daten in der Cloud werden dauerhaft gelöscht.",
"Delete Permanently": "Dauerhaft löschen",
"Free Tier": "Kostenlose Stufe",
"Plus Tier": "Plus-Stufe",
"Pro Tier": "Pro-Stufe",
"RTL Direction": "RTL-Richtung",
"Maximum Column Height": "Maximale Spaltenhöhe",
"Maximum Column Width": "Maximale Spaltenbreite",
@@ -221,7 +193,7 @@
"Fullscreen": "Vollbild",
"No supported files found. Supported formats: {{formats}}": "Keine unterstützten Dateien gefunden. Unterstützte Formate: {{formats}}",
"Drop to Import Books": "Zum Importieren von Büchern ablegen",
"Custom": "Benutzerdefiniert",
"Custom": "Eigen",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "Die ganze Welt ist eine Bühne,\nUnd alle Männer und Frauen sind bloß Spieler;\nSie haben ihre Abgänge und ihre Auftritte,\nUnd ein Mann spielt in seinem Leben viele Rollen,\nSeine Taten sind sieben Zeitalter.\n\n— William Shakespeare",
"Custom Theme": "Benutzerdefiniertes Thema",
"Theme Name": "Themenname",
@@ -229,7 +201,7 @@
"Background Color": "Hintergrundfarbe",
"Preview": "Vorschau",
"Contrast": "Kontrast",
"Sunset": "Sonnenuntergang",
"Sunset": "Abend",
"Double Border": "Doppelter Rand",
"Border Color": "Randfarbe",
"Border Frame": "Rahmenrand",
@@ -270,7 +242,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Herunterladen {{downloaded}} von {{contentLength}}",
"Download finished": "Download abgeschlossen",
"DOWNLOAD & INSTALL": "HERUNTERLADEN & INSTALLIEREN",
"Changelog:": "Änderungsprotokoll:",
"Changelog": "Änderungsprotokoll",
"Software Update": "Software-Update",
"Title": "Titel",
"Date Read": "Datum gelesen",
@@ -279,10 +251,9 @@
"Ascending": "Aufsteigend",
"Descending": "Absteigend",
"Sort by...": "Sortieren nach...",
"Added:": "Hinzugefügt:",
"Format:": "Format:",
"Added": "Hinzugefügt",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Beschreibung:",
"Description": "Beschreibung",
"No description available": "Keine Beschreibung verfügbar",
"List": "Liste",
"Grid": "Raster",
@@ -299,7 +270,7 @@
"Checking for updates...": "Auf Updates prüfen...",
"Error checking for updates": "Fehler beim Überprüfen auf Updates",
"Details": "Details",
"File Size:": "Dateigröße:",
"File Size": "Dateigröße",
"Auto Detect": "Automatisch erkennen",
"Next Section": "Nächster Abschnitt",
"Previous Section": "Vorheriger Abschnitt",
@@ -318,10 +289,10 @@
"Invert Image In Dark Mode": "Invertiere Bild im Dunkeln",
"Help improve Readest": "Readest verbessern helfen",
"Sharing anonymized statistics": "Anonymisierte Statistiken teilen",
"Interface Language": "Benutzeroberflächensprache",
"Interface Language": "UI-Sprache",
"Translation": "Übersetzung",
"Enable Translation": "Übersetzung aktivieren",
"Translation Service": "Übersetzungsdienst",
"Translation Service": "Übersetzer",
"Translate To": "Übersetzen nach",
"Disable Translation": "Übersetzung deaktivieren",
"Scroll": "Scrollen",
@@ -347,7 +318,6 @@
"Quota Exceeded": "Limit überschritten",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% der täglichen Übersetzungszeichen verwendet.",
"Translation Characters": "Übersetzungszeichen",
"Daily translation quota reached. Select another translate service to proceed.": "Das tägliche Übersetzungslimit wurde erreicht. Wählen Sie einen anderen Übersetzungsdienst, um fortzufahren.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} Stimme",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} Stimmen",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Etwas ist schief gelaufen. Keine Sorge, unser Team wurde benachrichtigt und arbeitet an einer Lösung.",
@@ -363,5 +333,133 @@
"Right Margin (px)": "Rechter Rand (px)",
"Left Margin (px)": "Linker Rand (px)",
"Column Gap (%)": "Spaltenabstand (%)",
"Always Show Status Bar": "Statusleiste immer anzeigen"
"Always Show Status Bar": "Statusleiste immer anzeigen",
"Translation Not Available": "Übersetzung nicht verfügbar",
"Custom Content CSS": "Benutzerdefiniertes Inhalts-CSS",
"Enter CSS for book content styling...": "CSS für die Buchinhaltsgestaltung eingeben...",
"Custom Reader UI CSS": "Benutzerdefiniertes CSS für die Leseroberfläche",
"Enter CSS for reader interface styling...": "CSS für die Leseroberfläche eingeben...",
"Crop": "Zuschneiden",
"Book Covers": "Buchcover",
"Fit": "Anpassen",
"Reset {{settings}}": "{{settings}} zurücksetzen",
"Reset Settings": "Einstellungen zurücksetzen",
"{{count}} pages left in chapter_one": "{{count}} Seite verbleibend im Kapitel",
"{{count}} pages left in chapter_other": "{{count}} Seiten verbleibend im Kapitel",
"Show Remaining Pages": "Verbleibende Seiten anzeigen",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Abo verwalten",
"Coming Soon": "Demnächst verfügbar",
"Upgrade to {{plan}}": "Upgrade auf {{plan}}",
"Upgrade to Plus or Pro": "Upgrade auf Plus oder Pro",
"Current Plan": "Aktueller Tarif",
"Plan Limits": "Tariflimits",
"Processing your payment...": "Zahlung wird verarbeitet...",
"Please wait while we confirm your subscription.": "Bitte warten Sie, während wir Ihr Abo bestätigen.",
"Payment Processing": "Zahlung wird verarbeitet",
"Your payment is being processed. This usually takes a few moments.": "Ihre Zahlung wird verarbeitet. Dies dauert normalerweise nur wenige Augenblicke.",
"Payment Failed": "Zahlung fehlgeschlagen",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Ihr Abo konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support, falls das Problem weiterhin besteht.",
"Back to Profile": "Zurück zum Profil",
"Subscription Successful!": "Abo erfolgreich!",
"Thank you for your subscription! Your payment has been processed successfully.": "Vielen Dank für Ihr Abo! Die Zahlung wurde erfolgreich verarbeitet.",
"Email:": "E-Mail:",
"Plan:": "Tarif:",
"Amount:": "Betrag:",
"Subscription ID:": "Abo-ID:",
"Go to Library": "Zur Bibliothek",
"Need help? Contact our support team at support@readest.com": "Brauchen Sie Hilfe? Kontaktieren Sie unser Support-Team unter support@readest.com",
"Free Plan": "Gratis-Tarif",
"month": "Monat",
"AI Translations (per day)": "KI-Übersetzungen (pro Tag)",
"Plus Plan": "Plus-Tarif",
"Includes All Free Plan Benefits": "Enthält alle Vorteile des Gratis-Tarifs",
"Pro Plan": "Pro-Tarif",
"More AI Translations": "Mehr KI-Übersetzungen",
"Complete Your Subscription": "Abschließen Sie Ihr Abonnement",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% des Cloud-Synchronisierungsspeichers verwendet.",
"Cloud Sync Storage": "Cloud-Speicher",
"Parallel Read": "Paralleles Lesen",
"Disable": "Deaktivieren",
"Enable": "Aktivieren",
"Upgrade to Readest Premium": "Upgrade auf Readest Premium",
"Show Source Text": "Quelltext anzeigen",
"Cross-Platform Sync": "Plattformübergreifende Synchronisierung",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Synchronisiere Bibliothek, Fortschritt und Notizen auf allen Geräten.",
"Customizable Reading": "Individuelles Lesen",
"AI Read Aloud": "KI-Vorlesen",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Höre Bücher mit natürlichen KI-Stimmen.",
"AI Translations": "KI-Übersetzungen",
"Connect with fellow readers and get help fast in our friendly community channels.": "Tausche dich mit anderen aus und erhalte Hilfe.",
"Unlimited AI Read Aloud Hours": "Unbegrenzte Vorlesezeit",
"Listen without limits—convert as much text as you like into immersive audio.": "Beliebig viel Text in Audio umwandeln.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Erweitere Übersetzungen und tägliches Kontingent.",
"DeepL Pro Access": "DeepL Pro-Zugang",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Erhalte schnellen, persönlichen Support.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Tageslimit erreicht. Upgrade nötig, um weiter zu übersetzen.",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Personalisieren Sie jedes Detail mit anpassbaren Schriften, Layouts, Themes und erweiterten Anzeigeeinstellungen für das perfekte Leseerlebnis.",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Übersetzen Sie jeden Text sofort mit der Kraft von Google, Azure oder DeepL—verstehen Sie Inhalte in jeder Sprache.",
"Includes All Plus Plan Benefits": "Enthält alle Plus-Plan-Vorteile",
"Early Feature Access": "Früher Zugang zu Features",
"Be the first to explore new features, updates, and innovations before anyone else.": "Entdecken Sie neue Features, Updates und Innovationen vor allen anderen.",
"Advanced AI Tools": "Erweiterte KI-Tools",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Nutzen Sie leistungsstarke KI-Tools für intelligenteres Lesen, Übersetzen und Content-Discovery.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Übersetzen Sie täglich bis zu 100.000 Zeichen mit der genauesten verfügbaren Übersetzungsmaschine.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Übersetzen Sie täglich bis zu 500.000 Zeichen mit der genauesten verfügbaren Übersetzungsmaschine.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Speichern und greifen Sie sicher auf Ihre gesamte Lesekollektion mit bis zu 5 GB Cloud-Speicher zu.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Speichern und greifen Sie sicher auf Ihre gesamte Lesekollektion mit bis zu 20 GB Cloud-Speicher zu.",
"Deleted cloud backup of the book: {{title}}": "Gelöschte Cloud-Sicherung des Buches: {{title}}",
"Failed to delete cloud backup of the book": "Fehler beim Löschen der Cloud-Sicherung des Buches",
"Are you sure to delete the cloud backup of the selected book?": "Sind Sie sicher, dass Sie die Cloud-Sicherung des ausgewählten Buches löschen möchten?",
"What's New in Readest": "Was gibt es Neues in Readest",
"Enter book title": "Buchtitel eingeben",
"Subtitle": "Untertitel",
"Enter book subtitle": "Buchuntertitel eingeben",
"Enter author name": "Autorennamen eingeben",
"Series": "Serie",
"Enter series name": "Seriennamen eingeben",
"Series Index": "Seriennummer",
"Enter series index": "Seriennummer eingeben",
"Total in Series": "Gesamt in Serie",
"Enter total books in series": "Gesamtanzahl der Bücher in Serie eingeben",
"Enter publisher": "Verlag eingeben",
"Publication Date": "Veröffentlichungsdatum",
"Identifier": "Identifikator",
"Enter book description": "Buchbeschreibung eingeben",
"Change cover image": "Titelbild ändern",
"Replace": "Ersetzen",
"Unlock cover": "Titelbild entsperren",
"Lock cover": "Titelbild sperren",
"Auto-Retrieve Metadata": "Metadaten automatisch abrufen",
"Auto-Retrieve": "Automatisch abrufen",
"Unlock all fields": "Alle Felder entsperren",
"Unlock All": "Alle entsperren",
"Lock all fields": "Alle Felder sperren",
"Lock All": "Alle sperren",
"Reset": "Zurücksetzen",
"Edit Metadata": "Metadaten bearbeiten",
"Locked": "Gesperrt",
"Select Metadata Source": "Metadatenquelle auswählen",
"Keep manual input": "Manuelle Eingabe beibehalten",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Belletristik, Wissenschaft, Geschichte",
"Select Cover Image": "Coverbild auswählen",
"Open Book in New Window": "Buch in neuem Fenster öffnen",
"Voices for {{lang}}": "Stimmen für {{lang}}",
"Yandex Translate": "Yandex Übersetzer",
"YYYY or YYYY-MM-DD": "YYYY oder YYYY-MM-DD",
"Restore Purchase": "Kauf wiederherstellen",
"No purchases found to restore.": "Keine Käufe zum Wiederherstellen gefunden.",
"Failed to restore purchases.": "Wiederherstellung der Käufe fehlgeschlagen.",
"Failed to manage subscription.": "Verwaltung des Abonnements fehlgeschlagen.",
"Failed to load subscription plans.": "Laden der Abonnementpläne fehlgeschlagen.",
"year": "Jahr",
"Failed to create checkout session": "Fehler beim Erstellen der Checkout-Sitzung",
"Storage": "Speicher",
"Terms of Service": "Allgemeine Geschäftsbedingungen",
"Privacy Policy": "Datenschutzbestimmungen",
"Disable Double Click": "Doppelklick deaktivieren"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Επιβεβαίωση διαγραφής",
"Copied to notebook": "Αντιγράφηκε στο σημειωματάριο",
"Copy": "Αντιγραφή",
"Custom CSS": "Προσαρμοσμένο CSS",
"Dark Mode": "Σκοτεινή λειτουργία",
"Default": "Προεπιλογή",
"Default Font": "Προεπιλεγμένη γραμματοσειρά",
@@ -28,7 +27,6 @@
"Dictionary": "Λεξικό",
"Download Readest": "Λήψη Readest",
"Edit": "Επεξεργασία",
"Enter your custom CSS here...": "Εισάγετε το προσαρμοσμένο CSS εδώ...",
"Excerpts": "Αποσπάσματα",
"Failed to import book(s): {{filenames}}": "Αποτυχία εισαγωγής βιβλίου(ων): {{filenames}}",
"Fast": "Γρήγορα",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Οριζόντια κατεύθυνση",
"Hyphenation": "Συλλαβισμός",
"Import Books": "Εισαγωγή βιβλίων",
"Language:": "Γλώσσα:",
"Layout": "Διάταξη",
"Light Mode": "Φωτεινή λειτουργία",
"Loading...": "Φόρτωση...",
@@ -70,8 +67,8 @@
"Paging Animation": "Κινούμενα σχέδια σελιδοποίησης",
"Paragraph": "Παράγραφος",
"Parallel Read": "Παράλληλη ανάγνωση",
"Published:": "Δημοσιεύτηκε:",
"Publisher:": "Εκδότης:",
"Published": "Δημοσιεύτηκε",
"Publisher": "Εκδότης",
"Reading Progress Synced": "Ο συγχρονισμός προόδου ανάγνωσης ολοκληρώθηκε",
"Reload Page": "Επαναφόρτωση σελίδας",
"Reveal in File Explorer": "Εμφάνιση στον εξερευνητή αρχείων",
@@ -96,7 +93,7 @@
"Slow": "Αργά",
"Solarized": "Solarized",
"Speak": "Ομιλία",
"Subjects:": "Θέματα:",
"Subjects": "Θέματα",
"System Fonts": "Γραμματοσειρές συστήματος",
"TTS not supported in this device": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται σε αυτή τη συσκευή",
"Theme Color": "Χρώμα θέματος",
@@ -105,7 +102,7 @@
"Translated Text": "Μεταφρασμένο κείμενο",
"Unknown": "Άγνωστο",
"Untitled": "Χωρίς τίτλο",
"Updated:": "Ενημερώθηκε:",
"Updated": "Ενημερώθηκε",
"Version {{version}}": "Έκδοση {{version}}",
"Vertical Direction": "Κάθετη κατεύθυνση",
"Welcome to your library. You can import your books here and read them anytime.": "Καλώς ήρθατε στη βιβλιοθήκη σας. Μπορείτε να εισάγετε τα βιβλία σας εδώ και να τα διαβάσετε οποιαδήποτε στιγμή.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Αποτυχία λήψης βιβλίου: {{title}}",
"Upload Book": "Ανέβασμα βιβλίου",
"Auto Upload Books to Cloud": "Αυτόματη αποθήκευση στο cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% της αποθήκευσης στο cloud χρησιμοποιήθηκε.",
"Storage": "Αποθήκευση",
"Book deleted: {{title}}": "Το βιβλίο με τίτλο {{title}} διαγράφηκε",
"Failed to delete book: {{title}}": "Αποτυχία διαγραφής βιβλίου: {{title}}",
"Check Updates on Start": "Αυτόματος έλεγχος ενημερώσεων",
@@ -161,7 +156,7 @@
"Create a Password": "Δημιουργία κωδικού πρόσβασης",
"Sign up": "Εγγραφή",
"Signing up...": "Εγγραφή...",
"Dont have an account? Sign up": "Δεν έχετε λογαριασμό; Εγγραφείτε",
"Don't have an account? Sign up": "Δεν έχετε λογαριασμό; Εγγραφείτε",
"Check your email for the confirmation link": "Ελέγξτε το email σας για τον σύνδεσμο επιβεβαίωσης",
"Signing in ...": "Σύνδεση ...",
"Send a magic link email": "Αποστολή email με μαγικό σύνδεσμο",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "Σύνδεση με GitHub",
"Account": "Λογαριασμός",
"Failed to delete user. Please try again later.": "Αποτυχία διαγραφής χρήστη. Παρακαλώ δοκιμάστε ξανά αργότερα.",
"Unlimited Offline/Online Reading": "Απεριόριστη ανάγνωση εκτός/εντός σύνδεσης",
"Unlimited Cloud Sync Devices": "Απεριόριστες συσκευές συγχρονισμού Cloud",
"Essential Text-to-Speech Voices": "Βασικές φωνές μετατροπής κειμένου σε ομιλία",
"DeepL Free Access": "Δωρεάν πρόσβαση DeepL",
"Community Support": "Υποστήριξη κοινότητας",
"500 MB Cloud Sync Space": "500 MB χώρος συγχρονισμού Cloud",
"Includes All Free Tier Benefits": "Περιλαμβάνει όλα τα οφέλη της δωρεάν βαθμίδας",
"AI Summaries": "Περιλήψεις AI",
"AI Translations": "Μεταφράσεις AI",
"Priority Support": "Υποστήριξη προτεραιότητας",
"DeepL Pro Access": "Πρόσβαση DeepL Pro",
"Expanded Text-to-Speech Voices": "Διευρυμένες φωνές μετατροπής κειμένου σε ομιλία",
"2000 MB Cloud Sync Space": "2000 MB χώρος συγχρονισμού Cloud",
"Includes All Plus Tier Benefits": "Περιλαμβάνει όλα τα οφέλη της βαθμίδας Plus",
"Unlimited AI Summaries": "Απεριόριστες περιλήψεις AI",
"Unlimited AI Translations": "Απεριόριστες μεταφράσεις AI",
"Unlimited DeepL Pro Access": "Απεριόριστη πρόσβαση DeepL Pro",
"Advanced AI Tools": "Προηγμένα εργαλεία AI",
"Early Feature Access": "Πρώιμη πρόσβαση σε λειτουργίες",
"10 GB Cloud Sync Space": "10 GB χώρος συγχρονισμού Cloud",
"Loading profile...": "Φόρτωση προφίλ...",
"Features": "Χαρακτηριστικά",
"Delete Account": "Διαγραφή λογαριασμού",
"Delete Your Account?": "Διαγραφή του λογαριασμού σας;",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. Όλα τα δεδομένα σας στο cloud θα διαγραφούν μόνιμα.",
"Delete Permanently": "Μόνιμη διαγραφή",
"Free Tier": "Δωρεάν βαθμίδα",
"Plus Tier": "Βαθμίδα Plus",
"Pro Tier": "Βαθμίδα Pro",
"RTL Direction": "Κατεύθυνση RTL",
"Maximum Column Height": "Μέγιστο ύψος στήλης",
"Maximum Column Width": "Μέγιστο πλάτος στήλης",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Κατεβάζω {{downloaded}} από {{contentLength}}",
"Download finished": "Η λήψη ολοκληρώθηκε",
"DOWNLOAD & INSTALL": "ΛΗΨΗ & ΕΓΚΑΤΑΣΤΑΣΗ",
"Changelog:": "Αλλαγές:",
"Changelog": "Αλλαγές",
"Software Update": "Ενημέρωση λογισμικού",
"Title": "Τίτλος",
"Date Read": "Ημερομηνία ανάγνωσης",
@@ -279,10 +252,9 @@
"Ascending": "Αύξουσα",
"Descending": "Φθίνουσα",
"Sort by...": "Ταξινόμηση κατά...",
"Added:": "Προστέθηκε:",
"Format:": "Μορφή:",
"Added": "Προστέθηκε",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Περιγραφή:",
"Description": "Περιγραφή",
"No description available": "Δεν είναι διαθέσιμη καμία περιγραφή",
"List": "Λίστα",
"Grid": "Πλέγμα",
@@ -299,7 +271,7 @@
"Checking for updates...": "Έλεγχος για ενημερώσεις...",
"Error checking for updates": "Σφάλμα κατά τον έλεγχο για ενημερώσεις",
"Details": "Πληρ.",
"File Size:": "Μέγεθος αρχείου:",
"File Size": "Μέγεθος αρχείου",
"Auto Detect": "Αυτόματη ανίχνευση",
"Next Section": "Επόμενη ενότητα",
"Previous Section": "Προηγούμενη ενότητα",
@@ -347,7 +319,6 @@
"Quota Exceeded": "Υπέρβαση ορίου",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% των ημερήσιων χαρακτήρων μετάφρασης χρησιμοποιήθηκαν.",
"Translation Characters": "Χαρακτήρες μετάφρασης",
"Daily translation quota reached. Select another translate service to proceed.": "Έχετε φτάσει το ημερήσιο όριο χαρακτήρων μετάφρασης. Επιλέξτε άλλη υπηρεσία μετάφρασης για να συνεχίσετε.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} φωνή",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} φωνές",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Κάτι πήγε στραβά. Μην ανησυχείτε, η ομάδα μας έχει ενημερωθεί και εργαζόμαστε για μια λύση.",
@@ -363,5 +334,132 @@
"Right Margin (px)": "Δεξί περιθώριο (px)",
"Left Margin (px)": "Αριστερό περιθώριο (px)",
"Column Gap (%)": "Απόσταση στηλών (%)",
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης"
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης",
"Translation Not Available": "Η μετάφραση δεν είναι διαθέσιμη",
"Custom Content CSS": "Προσαρμοσμένο CSS περιεχομένου",
"Enter CSS for book content styling...": "Εισαγάγετε CSS για στυλ περιεχομένου βιβλίου...",
"Custom Reader UI CSS": "Προσαρμοσμένο CSS διεπαφής αναγνώστη",
"Enter CSS for reader interface styling...": "Εισαγάγετε CSS για στυλ διεπαφής αναγνώστη...",
"Crop": "Περικοπή",
"Book Covers": "Εξώφυλλα βιβλίων",
"Fit": "Προσαρμογή",
"Reset {{settings}}": "Επαναφορά {{settings}}",
"Reset Settings": "Επαναφορά ρυθμίσεων",
"{{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",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Διαχείριση συνδρομής",
"Coming Soon": "Έρχεται σύντομα",
"Upgrade to {{plan}}": "Αναβάθμιση σε {{plan}}",
"Upgrade to Plus or Pro": "Αναβάθμιση σε Plus ή Pro",
"Current Plan": "Τρέχον πρόγραμμα",
"Plan Limits": "Όρια προγράμματος",
"Processing your payment...": "Επεξεργασία πληρωμής...",
"Please wait while we confirm your subscription.": "Παρακαλώ περιμένετε όσο επιβεβαιώνουμε τη συνδρομή σας.",
"Payment Processing": "Επεξεργασία πληρωμής",
"Your payment is being processed. This usually takes a few moments.": "Η πληρωμή σας επεξεργάζεται. Συνήθως διαρκεί λίγες στιγμές.",
"Payment Failed": "Η πληρωμή απέτυχε",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Δεν ήταν δυνατή η επεξεργασία της συνδρομής σας. Παρακαλώ προσπαθήστε ξανά ή επικοινωνήστε με την υποστήριξη αν το πρόβλημα συνεχιστεί.",
"Back to Profile": "Επιστροφή στο προφίλ",
"Subscription Successful!": "Η συνδρομή ολοκληρώθηκε!",
"Thank you for your subscription! Your payment has been processed successfully.": "Σας ευχαριστούμε για τη συνδρομή σας! Η πληρωμή ολοκληρώθηκε με επιτυχία.",
"Email:": "Email:",
"Plan:": "Πρόγραμμα:",
"Amount:": "Ποσό:",
"Subscription ID:": "ID συνδρομής:",
"Go to Library": "Μετάβαση στη βιβλιοθήκη",
"Need help? Contact our support team at support@readest.com": "Χρειάζεστε βοήθεια; Επικοινωνήστε με την υποστήριξη στο support@readest.com",
"Free Plan": "Δωρεάν πρόγραμμα",
"month": "μήνας",
"AI Translations (per day)": "Μεταφράσεις AI (ανά ημέρα)",
"Plus Plan": "Πρόγραμμα Plus",
"Includes All Free Plan Benefits": "Περιλαμβάνει όλα τα οφέλη του δωρεάν προγράμματος",
"Pro Plan": "Πρόγραμμα Pro",
"More AI Translations": "Περισσότερες μεταφράσεις AI",
"Complete Your Subscription": "Ολοκληρώστε τη συνδρομή σας",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% του χώρου συγχρονισμού στο cloud χρησιμοποιήθηκε.",
"Cloud Sync Storage": "Χώρος συγχρονισμού cloud",
"Disable": "Απενεργοποίηση",
"Enable": "Ενεργοποίηση",
"Upgrade to Readest Premium": "Αναβάθμιση στο Readest Premium",
"Show Source Text": "Εμφάνιση αρχικού κειμένου",
"Cross-Platform Sync": "Συγχρονισμός σε όλες τις συσκευές",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Συγχρονίστε βιβλία, πρόοδο και σημειώσεις σε όλες τις συσκευές.",
"Customizable Reading": "Προσαρμόσιμη ανάγνωση",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Ρυθμίστε γραμματοσειρές, διάταξη και θέματα όπως θέλετε.",
"AI Read Aloud": "Ανάγνωση με AI",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Ακούστε τα βιβλία σας με φυσικές φωνές AI.",
"AI Translations": "Μεταφράσεις με AI",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Μεταφράστε άμεσα με Google, Azure ή DeepL.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Συμμετέχετε στην κοινότητα για άμεση υποστήριξη.",
"Unlimited AI Read Aloud Hours": "Απεριόριστη ανάγνωση με AI",
"Listen without limits—convert as much text as you like into immersive audio.": "Απεριόριστη μετατροπή κειμένου σε ήχο.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Περισσότερες μεταφράσεις και δυνατότητες.",
"DeepL Pro Access": "Πρόσβαση στο DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Γρήγορη και άμεση υποστήριξη.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Έχετε φτάσει το ημερήσιο όριο. Κάντε αναβάθμιση για περισσότερες μεταφράσεις.",
"Includes All Plus Plan Benefits": "Περιλαμβάνει Όλα τα Οφέλη του Plus Πλάνου",
"Early Feature Access": "Πρόωρη Πρόσβαση σε Χαρακτηριστικά",
"Be the first to explore new features, updates, and innovations before anyone else.": "Εξερευνήστε πρώτοι νέα χαρακτηριστικά, ενημερώσεις και καινοτομίες.",
"Advanced AI Tools": "Προηγμένα Εργαλεία AI",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Αξιοποιήστε ισχυρά εργαλεία AI για εξυπνότερη ανάγνωση, μετάφραση και ανακάλυψη περιεχομένου.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Μεταφράστε έως 100.000 χαρακτήρες ημερησίως με τη πιο ακριβή μηχανή μετάφρασης.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Μεταφράστε έως 500.000 χαρακτήρες ημερησίως με τη πιο ακριβή μηχανή μετάφρασης.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Αποθηκεύστε και αποκτήστε πρόσβαση στη συλλογή ανάγνωσης με έως 5 GB ασφαλή cloud αποθήκευση.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Αποθηκεύστε και αποκτήστε πρόσβαση στη συλλογή ανάγνωσης με έως 20 GB ασφαλή cloud αποθήκευση.",
"Deleted cloud backup of the book: {{title}}": "Διαγράφηκε το αντίγραφο ασφαλείας του βιβλίου: {{title}}",
"Failed to delete cloud backup of the book": "Αποτυχία διαγραφής του αντιγράφου ασφαλείας του βιβλίου",
"Are you sure to delete the cloud backup of the selected book?": "Είστε σίγουροι ότι θέλετε να διαγράψετε το αντίγραφο ασφαλείας του επιλεγμένου βιβλίου;",
"What's New in Readest": "Τι νέο υπάρχει στο Readest",
"Enter book title": "Εισάγετε τίτλο βιβλίου",
"Subtitle": "Υπότιτλος",
"Enter book subtitle": "Εισάγετε υπότιτλο βιβλίου",
"Enter author name": "Εισάγετε όνομα συγγραφέα",
"Series": "Σειρά",
"Enter series name": "Εισάγετε όνομα σειράς",
"Series Index": "Αριθμός σειράς",
"Enter series index": "Εισάγετε αριθμό σειράς",
"Total in Series": "Σύνολο στη σειρά",
"Enter total books in series": "Εισάγετε συνολικό αριθμό βιβλίων στη σειρά",
"Enter publisher": "Εισάγετε εκδότη",
"Publication Date": "Ημερομηνία έκδοσης",
"Identifier": "Αναγνωριστικό",
"Enter book description": "Εισάγετε περιγραφή βιβλίου",
"Change cover image": "Αλλαγή εικόνας εξωφύλλου",
"Replace": "Αντικατάσταση",
"Unlock cover": "Ξεκλείδωμα εξωφύλλου",
"Lock cover": "Κλείδωμα εξωφύλλου",
"Auto-Retrieve Metadata": "Αυτόματη ανάκτηση μεταδεδομένων",
"Auto-Retrieve": "Αυτόματη ανάκτηση",
"Unlock all fields": "Ξεκλείδωμα όλων των πεδίων",
"Unlock All": "Ξεκλείδωμα όλων",
"Lock all fields": "Κλείδωμα όλων των πεδίων",
"Lock All": "Κλείδωμα όλων",
"Reset": "Επαναφορά",
"Edit Metadata": "Επεξεργασία μεταδεδομένων",
"Locked": "Κλειδωμένο",
"Select Metadata Source": "Επιλέξτε πηγή μεταδεδομένων",
"Keep manual input": "Διατήρηση χειροκίνητης εισαγωγής",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Μυθοπλασία, Επιστήμη, Ιστορία",
"Select Cover Image": "Επιλέξτε εικόνα εξωφύλλου",
"Open Book in New Window": "Άνοιγμα βιβλίου σε νέο παράθυρο",
"Voices for {{lang}}": "Φωνές για {{lang}}",
"Yandex Translate": "Μετάφραση Yandex",
"YYYY or YYYY-MM-DD": "YYYY ή YYYY-MM-DD",
"Restore Purchase": "Επαναφορά αγοράς",
"No purchases found to restore.": "Δεν βρέθηκαν αγορές για επαναφορά.",
"Failed to restore purchases.": "Αποτυχία επαναφοράς αγορών.",
"Failed to manage subscription.": "Αποτυχία διαχείρισης συνδρομής.",
"Failed to load subscription plans.": "Αποτυχία φόρτωσης σχεδίων συνδρομής.",
"year": "έτος",
"Failed to create checkout session": "Αποτυχία δημιουργίας συνεδρίας πληρωμής",
"Storage": "Αποθήκευση",
"Terms of Service": "Όροι χρήσης",
"Privacy Policy": "Πολιτική απορρήτου",
"Disable Double Click": "Απενεργοποίηση διπλού κλικ"
}
@@ -1,8 +1,13 @@
{
"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",
"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?",
"Search in {{count}} Book(s)..._one": "Search in {{count}} book...",
"Search in {{count}} Book(s)..._other": "Search in {{count}} books..."
"Search in {{count}} Book(s)..._other": "Search in {{count}} books...",
"{{count}} pages left in chapter_one": "{{count}} page left in chapter",
"{{count}} pages left in chapter_other": "{{count}} pages left in chapter"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmar eliminación",
"Copied to notebook": "Copiado al cuaderno",
"Copy": "Copiar",
"Custom CSS": "CSS personalizado",
"Dark Mode": "Modo oscuro",
"Default": "Predeterminado",
"Default Font": "Fuente predeterminada",
@@ -28,7 +27,6 @@
"Dictionary": "Diccionario",
"Download Readest": "Descargar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Introduce tu CSS personalizado aquí...",
"Excerpts": "Extractos",
"Failed to import book(s): {{filenames}}": "Error al importar libro(s): {{filenames}}",
"Fast": "Rápido",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Dirección horizontal",
"Hyphenation": "Guionización",
"Import Books": "Importar libros",
"Language:": "Idioma:",
"Layout": "Diseño",
"Light Mode": "Modo claro",
"Loading...": "Cargando...",
@@ -70,8 +67,8 @@
"Paging Animation": "Animación de paginación",
"Paragraph": "Párrafo",
"Parallel Read": "Lectura paralela",
"Published:": "Publicado:",
"Publisher:": "Editorial:",
"Published": "Publicado",
"Publisher": "Editorial",
"Reading Progress Synced": "Progreso de lectura sincronizado",
"Reload Page": "Recargar página",
"Reveal in File Explorer": "Mostrar en el Explorador de archivos",
@@ -96,7 +93,7 @@
"Slow": "Lento",
"Solarized": "Solarizado",
"Speak": "Leer",
"Subjects:": "Temas:",
"Subjects": "Temas",
"System Fonts": "Fuentes del sistema",
"TTS not supported in this device": "TTS no es compatible con este dispositivo",
"Theme Color": "Color del tema",
@@ -105,7 +102,7 @@
"Translated Text": "Texto traducido",
"Unknown": "Desconocido",
"Untitled": "Sin título",
"Updated:": "Actualizado:",
"Updated": "Actualizado",
"Version {{version}}": "Versión {{version}}",
"Vertical Direction": "Dirección vertical",
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenido a tu biblioteca. Puedes importar tus libros aquí y leerlos en cualquier momento.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Error al descargar libro: {{title}}",
"Upload Book": "Subir libro",
"Auto Upload Books to Cloud": "Subir libros automáticamente a la nube",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% del almacenamiento en la nube utilizado.",
"Storage": "Almacenamiento",
"Book deleted: {{title}}": "Libro eliminado: {{title}}",
"Failed to delete book: {{title}}": "Error al eliminar libro: {{title}}",
"Check Updates on Start": "Comprobar actualizaciones al iniciar",
@@ -161,7 +156,7 @@
"Create a Password": "Crear una contraseña",
"Sign up": "Registrarse",
"Signing up...": "Registrándose...",
"Dont have an account? Sign up": "¿No tienes una cuenta? Regístrate",
"Don't have an account? Sign up": "¿No tienes una cuenta? Regístrate",
"Check your email for the confirmation link": "Revisa tu correo electrónico para el enlace de confirmación",
"Signing in ...": "Iniciando sesión ...",
"Send a magic link email": "Enviar un enlace mágico por correo",
@@ -185,35 +180,13 @@
"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.",
"Unlimited Offline/Online Reading": "Lectura sin límites en línea/sin conexión",
"Unlimited Cloud Sync Devices": "Sincronización ilimitada de dispositivos en la nube",
"Essential Text-to-Speech Voices": "Voces esenciales de texto a voz",
"DeepL Free Access": "Acceso gratuito a DeepL",
"Community Support": "Soporte comunitario",
"500 MB Cloud Sync Space": "500 MB de espacio de sincronización en la nube",
"Includes All Free Tier Benefits": "Incluye todos los beneficios del nivel gratuito",
"AI Summaries": "Resúmenes con IA",
"AI Translations": "Traducciones con IA",
"Priority Support": "Soporte prioritario",
"DeepL Pro Access": "Acceso a DeepL Pro",
"Expanded Text-to-Speech Voices": "Voces ampliadas de texto a voz",
"2000 MB Cloud Sync Space": "2000 MB de espacio de sincronización en la nube",
"Includes All Plus Tier Benefits": "Incluye todos los beneficios del nivel Plus",
"Unlimited AI Summaries": "Resúmenes ilimitados con IA",
"Unlimited AI Translations": "Traducciones ilimitadas con IA",
"Unlimited DeepL Pro Access": "Acceso ilimitado a DeepL Pro",
"Advanced AI Tools": "Herramientas avanzadas de IA",
"Early Feature Access": "Acceso anticipado a funciones",
"10 GB Cloud Sync Space": "10 GB de espacio de sincronización en la nube",
"Loading profile...": "Cargando perfil...",
"Features": "Características",
"Delete Account": "Eliminar cuenta",
"Delete Your Account?": "¿Eliminar tu cuenta?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Esta acción no se puede deshacer. Todos tus datos en la nube se eliminarán permanentemente.",
"Delete Permanently": "Eliminar permanentemente",
"Free Tier": "Nivel gratuito",
"Plus Tier": "Nivel Plus",
"Pro Tier": "Nivel Pro",
"RTL Direction": "Dirección RTL",
"Maximum Column Height": "Altura máxima de columna",
"Maximum Column Width": "Ancho máximo de columna",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Descargando {{downloaded}} de {{contentLength}}",
"Download finished": "Descarga finalizada",
"DOWNLOAD & INSTALL": "DESCARGAR E INSTALAR",
"Changelog:": "Notas de la versión:",
"Changelog": "Notas de la versión",
"Software Update": "Actualización de software",
"Title": "Título",
"Date Read": "Fecha de lectura",
@@ -279,10 +252,9 @@
"Ascending": "Ascendente",
"Descending": "Descendente",
"Sort by...": "Ordenar por...",
"Added:": "Agregado:",
"Format:": "Formato:",
"Added": "Agregado",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Descripción:",
"Description": "Descripción",
"No description available": "No hay descripción disponible",
"List": "Lista",
"Grid": "Cuadrícula",
@@ -299,7 +271,7 @@
"Checking for updates...": "Comprobando actualizaciones...",
"Error checking for updates": "Error al comprobar actualizaciones",
"Details": "Detalles",
"File Size:": "Peso del archivo:",
"File Size": "Peso del archivo",
"Auto Detect": "Detección automática",
"Next Section": "Siguiente sección",
"Previous Section": "Sección anterior",
@@ -349,7 +321,6 @@
"Quota Exceeded": "Límite superado",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% de caracteres de traducción diarios utilizados.",
"Translation Characters": "Caracteres de traducción",
"Daily translation quota reached. Select another translate service to proceed.": "Cuota diaria de traducción alcanzada. Selecciona otro servicio de traducción para continuar.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} voz",
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} voces",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} voces",
@@ -366,5 +337,133 @@
"Right Margin (px)": "Margen derecho (px)",
"Left Margin (px)": "Margen izquierdo (px)",
"Column Gap (%)": "Espacio entre columnas (%)",
"Always Show Status Bar": "Mostrar siempre la barra de estado"
"Always Show Status Bar": "Mostrar siempre la barra de estado",
"Translation Not Available": "Traducción no disponible",
"Custom Content CSS": "CSS del contenido",
"Enter CSS for book content styling...": "Introduce CSS para el contenido del libro...",
"Custom Reader UI CSS": "CSS de la interfaz",
"Enter CSS for reader interface styling...": "Introduce CSS para la interfaz de lectura...",
"Crop": "Recortar",
"Book Covers": "Portadas de libros",
"Fit": "Encajar",
"Reset {{settings}}": "Restablecer {{settings}}",
"Reset Settings": "Restablecer configuración",
"{{count}} pages left in chapter_one": "{{count}} página restante en el capítulo",
"{{count}} pages left in chapter_many": "{{count}} páginas restantes en el capítulo",
"{{count}} pages left in chapter_other": "{{count}} páginas restantes en el capítulo",
"Show Remaining Pages": "Mostrar resto",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gestionar suscripción",
"Coming Soon": "Próximamente",
"Upgrade to {{plan}}": "Actualizar a {{plan}}",
"Upgrade to Plus or Pro": "Actualizar a Plus o Pro",
"Current Plan": "Plan actual",
"Plan Limits": "Límites del plan",
"Processing your payment...": "Procesando tu pago...",
"Please wait while we confirm your subscription.": "Espera mientras confirmamos tu suscripción.",
"Payment Processing": "Procesando pago",
"Payment Failed": "El pago ha fallado",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "No pudimos procesar tu suscripción. Inténtalo de nuevo o contacta con soporte si el problema persiste.",
"Back to Profile": "Volver al perfil",
"Subscription Successful!": "¡Suscripción exitosa!",
"Thank you for your subscription! Your payment has been processed successfully.": "¡Gracias por tu suscripción! Tu pago se ha procesado correctamente.",
"Email:": "Correo electrónico:",
"Plan:": "Plan:",
"Amount:": "Importe:",
"Subscription ID:": "ID de suscripción:",
"Go to Library": "Ir a la biblioteca",
"Need help? Contact our support team at support@readest.com": "¿Necesitas ayuda? Contacta con nuestro equipo de soporte en support@readest.com",
"Free Plan": "Plan gratuito",
"month": "mes",
"AI Translations (per day)": "Traducciones con IA (por día)",
"Plus Plan": "Plan Plus",
"Includes All Free Plan Benefits": "Incluye todos los beneficios del plan gratuito",
"Pro Plan": "Plan Pro",
"More AI Translations": "Más traducciones con IA",
"Your payment is being processed. This usually takes a few moments.": "Tu pago se está procesando. Normalmente tarda unos momentos.",
"Complete Your Subscription": "Completa tu suscripción",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% del espacio de sincronización en la nube utilizado.",
"Cloud Sync Storage": "Almacenamiento en la nube",
"Disable": "Desactivar",
"Enable": "Activar",
"Upgrade to Readest Premium": "Actualizar a Readest Premium",
"Show Source Text": "Mostrar texto fuente",
"Cross-Platform Sync": "Sincronización multiplataforma",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Sincroniza tu biblioteca, progreso y notas en todos tus dispositivos.",
"Customizable Reading": "Lectura personalizable",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Ajusta fuentes, diseño y temas a tu gusto.",
"AI Read Aloud": "Lectura en voz alta con IA",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Escucha tus libros con voces IA naturales.",
"AI Translations": "Traducciones con IA",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Traduce textos al instante con Google, Azure o DeepL.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Únete a la comunidad y recibe ayuda rápidamente.",
"Unlimited AI Read Aloud Hours": "Lectura IA ilimitada",
"Listen without limits—convert as much text as you like into immersive audio.": "Convierte texto en audio sin límites.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Más traducciones y funciones avanzadas.",
"DeepL Pro Access": "Acceso a DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Soporte rápido y prioritario.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Has alcanzado tu límite diario. Mejora tu plan para más traducciones.",
"Includes All Plus Plan Benefits": "Incluye Todos los Beneficios del Plan Plus",
"Early Feature Access": "Acceso Anticipado a Funciones",
"Be the first to explore new features, updates, and innovations before anyone else.": "Explora primero nuevas funciones, actualizaciones e innovaciones antes que nadie.",
"Advanced AI Tools": "Herramientas de IA Avanzadas",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Aprovecha herramientas de IA potentes para lectura inteligente, traducción y descubrimiento de contenido.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Traduce hasta 100,000 caracteres diarios con el motor de traducción más preciso disponible.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduce hasta 500,000 caracteres diarios con el motor de traducción más preciso disponible.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Almacena y accede de forma segura a toda tu colección de lectura con hasta 5 GB de almacenamiento en la nube.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Almacena y accede de forma segura a toda tu colección de lectura con hasta 20 GB de almacenamiento en la nube.",
"Deleted cloud backup of the book: {{title}}": "Respaldo en la nube del libro eliminado: {{title}}",
"Failed to delete cloud backup of the book": "Fallo al eliminar el respaldo en la nube del libro",
"Are you sure to delete the cloud backup of the selected book?": "¿Estás seguro de que deseas eliminar el respaldo en la nube del libro seleccionado?",
"What's New in Readest": "¿Qué hay de nuevo en Readest",
"Enter book title": "Ingresa el título del libro",
"Subtitle": "Subtítulo",
"Enter book subtitle": "Ingresa el subtítulo del libro",
"Enter author name": "Ingresa el nombre del autor",
"Series": "Serie",
"Enter series name": "Ingresa el nombre de la serie",
"Series Index": "Número de serie",
"Enter series index": "Ingresa el número de serie",
"Total in Series": "Total en la serie",
"Enter total books in series": "Ingresa el total de libros en la serie",
"Enter publisher": "Ingresa la editorial",
"Publication Date": "Fecha de publicación",
"Identifier": "Identificador",
"Enter book description": "Ingresa la descripción del libro",
"Change cover image": "Cambiar imagen de portada",
"Replace": "Reemplazar",
"Unlock cover": "Desbloquear portada",
"Lock cover": "Bloquear portada",
"Auto-Retrieve Metadata": "Recuperar metadatos automáticamente",
"Auto-Retrieve": "Recuperar automáticamente",
"Unlock all fields": "Desbloquear todos los campos",
"Unlock All": "Desbloquear todo",
"Lock all fields": "Bloquear todos los campos",
"Lock All": "Bloquear todo",
"Reset": "Restablecer",
"Edit Metadata": "Editar metadatos",
"Locked": "Bloqueado",
"Select Metadata Source": "Seleccionar fuente de metadatos",
"Keep manual input": "Mantener entrada manual",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Ficción, Ciencia, Historia",
"Select Cover Image": "Seleccionar imagen de portada",
"Open Book in New Window": "Abre el libro en una nueva ventana",
"Voices for {{lang}}": "Voces para {{lang}}",
"Yandex Translate": "Yandex Translate",
"YYYY or YYYY-MM-DD": "YYYY o YYYY-MM-DD",
"Restore Purchase": "Restaurar compra",
"No purchases found to restore.": "No se encontraron compras para restaurar.",
"Failed to restore purchases.": "Error al restaurar compras.",
"Failed to manage subscription.": "Error al gestionar la suscripción.",
"Failed to load subscription plans.": "Error al cargar los planes de suscripción.",
"year": "año",
"Failed to create checkout session": "Error al crear la sesión de pago",
"Storage": "Almacenamiento",
"Terms of Service": "Condiciones del servicio",
"Privacy Policy": "Política de privacidad",
"Disable Double Click": "Deshabilitar Doble Clic"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmer la suppression",
"Copied to notebook": "Copié dans le carnet de notes",
"Copy": "Copier",
"Custom CSS": "CSS personnalisé",
"Dark Mode": "Mode sombre",
"Default": "Par défaut",
"Default Font": "Police par défaut",
@@ -28,7 +27,6 @@
"Dictionary": "Dictionnaire",
"Download Readest": "Télécharger Readest",
"Edit": "Modifier",
"Enter your custom CSS here...": "Saisissez votre CSS personnalisé ici...",
"Excerpts": "Extraits",
"Failed to import book(s): {{filenames}}": "Échec de l'importation du/des livre(s) : {{filenames}}",
"Fast": "Rapide",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Direction horizontale",
"Hyphenation": "Césure",
"Import Books": "Importer des livres",
"Language:": "Langue :",
"Layout": "Mise en page",
"Light Mode": "Mode clair",
"Loading...": "Chargement...",
@@ -70,8 +67,8 @@
"Paging Animation": "Animation de page",
"Paragraph": "Paragraphe",
"Parallel Read": "Lecture parallèle",
"Published:": "Publié :",
"Publisher:": "Éditeur :",
"Published": "Publié",
"Publisher": "Éditeur",
"Reading Progress Synced": "Progression de lecture synchronisée",
"Reload Page": "Recharger la page",
"Reveal in File Explorer": "Afficher dans l'explorateur de fichiers",
@@ -96,7 +93,7 @@
"Slow": "Lent",
"Solarized": "Solarisé",
"Speak": "Lire",
"Subjects:": "Sujets :",
"Subjects": "Sujets",
"System Fonts": "Polices système",
"TTS not supported in this device": "La synthèse vocale n'est pas prise en charge sur cet appareil",
"Theme Color": "Couleur du thème",
@@ -105,7 +102,7 @@
"Translated Text": "Texte traduit",
"Unknown": "Inconnu",
"Untitled": "Sans titre",
"Updated:": "Mis à jour :",
"Updated": "Mis à jour",
"Version {{version}}": "Version {{version}}",
"Vertical Direction": "Direction verticale",
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenue dans votre bibliothèque. Vous pouvez importer vos livres ici et les lire à tout moment.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Échec du téléchargement du livre : {{title}}",
"Upload Book": "Télécharger un livre",
"Auto Upload Books to Cloud": "Télécharger automatiquement les livres dans le cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% de l'espace de stockage dans le cloud utilisé.",
"Storage": "Stockage",
"Book deleted: {{title}}": "Livre supprimé : {{title}}",
"Failed to delete book: {{title}}": "Échec de la suppression du livre : {{title}}",
"Check Updates on Start": "Vérifier les mises à jour au démarrage",
@@ -161,7 +156,6 @@
"Create a Password": "Créer un mot de passe",
"Sign up": "S'inscrire",
"Signing up...": "Inscription en cours...",
"Dont have an account? Sign up": "Vous n'avez pas de compte ? Inscrivez-vous",
"Check your email for the confirmation link": "Vérifiez votre e-mail pour le lien de confirmation",
"Signing in ...": "Connexion en cours ...",
"Send a magic link email": "Envoyer un e-mail avec un lien magique",
@@ -185,35 +179,13 @@
"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.",
"Unlimited Offline/Online Reading": "Lecture illimitée hors ligne/en ligne",
"Unlimited Cloud Sync Devices": "Synchronisation cloud sur appareils illimitée",
"Essential Text-to-Speech Voices": "Voix de synthèse vocale essentielles",
"DeepL Free Access": "Accès gratuit à DeepL",
"Community Support": "Support communautaire",
"500 MB Cloud Sync Space": "500 Mo d'espace de synchronisation cloud",
"Includes All Free Tier Benefits": "Inclut tous les avantages du niveau gratuit",
"AI Summaries": "Résumés par IA",
"AI Translations": "Traductions par IA",
"Priority Support": "Support prioritaire",
"DeepL Pro Access": "Accès à DeepL Pro",
"Expanded Text-to-Speech Voices": "Voix de synthèse vocale étendues",
"2000 MB Cloud Sync Space": "2000 Mo d'espace de synchronisation cloud",
"Includes All Plus Tier Benefits": "Inclut tous les avantages du niveau Plus",
"Unlimited AI Summaries": "Résumés par IA illimités",
"Unlimited AI Translations": "Traductions par IA illimitées",
"Unlimited DeepL Pro Access": "Accès illimité à DeepL Pro",
"Advanced AI Tools": "Outils IA avancés",
"Early Feature Access": "Accès anticipé aux fonctionnalités",
"10 GB Cloud Sync Space": "10 Go d'espace de synchronisation cloud",
"Loading profile...": "Chargement du profil...",
"Features": "Fonctionnalités",
"Delete Account": "Supprimer le compte",
"Delete Your Account?": "Supprimer votre compte ?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Cette action est irréversible. Toutes vos données dans le cloud seront définitivement supprimées.",
"Delete Permanently": "Supprimer définitivement",
"Free Tier": "Niveau gratuit",
"Plus Tier": "Niveau Plus",
"Pro Tier": "Niveau Pro",
"RTL Direction": "Direction RTL",
"Maximum Column Height": "Hauteur maximale de colonne",
"Maximum Column Width": "Largeur maximale de colonne",
@@ -270,7 +242,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Téléchargement {{downloaded}} sur {{contentLength}}",
"Download finished": "Téléchargement terminé",
"DOWNLOAD & INSTALL": "TÉLÉCHARGER ET INSTALLER",
"Changelog:": "Journal des modifications :",
"Changelog": "Journal des modifications",
"Software Update": "Mise à jour logicielle",
"Title": "Titre",
"Date Read": "Date de lecture",
@@ -279,10 +251,9 @@
"Ascending": "Ascendant",
"Descending": "Descendant",
"Sort by...": "Trier par...",
"Added:": "Ajouté :",
"Format:": "Format :",
"Added": "Ajouté",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Description :",
"Description": "Description",
"No description available": "Aucune description disponible",
"List": "Liste",
"Grid": "Grille",
@@ -299,7 +270,7 @@
"Checking for updates...": "Vérification des mises à jour...",
"Error checking for updates": "Erreur lors de la vérification des mises à jour",
"Details": "Détails",
"File Size:": "Taille du fichier :",
"File Size": "Taille du fichier",
"Auto Detect": "Détection automatique",
"Next Section": "Section suivante",
"Previous Section": "Section précédente",
@@ -349,7 +320,6 @@
"Quota Exceeded": "Quota dépassé",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% des caractères de traduction quotidiens utilisés.",
"Translation Characters": "Caractères de traduction",
"Daily translation quota reached. Select another translate service to proceed.": "Quota de traduction quotidien atteint. Sélectionnez un autre service de traduction pour continuer.",
"{{engine}}: {{count}} voices_one": "{{engine}} : {{count}} voix",
"{{engine}}: {{count}} voices_many": "{{engine}} : {{count}} voix",
"{{engine}}: {{count}} voices_other": "{{engine}} : {{count}} voix",
@@ -366,5 +336,134 @@
"Right Margin (px)": "Marge droite (px)",
"Left Margin (px)": "Marge gauche (px)",
"Column Gap (%)": "Espacement des colonnes (%)",
"Always Show Status Bar": "Afficher toujours la barre d'état"
"Always Show Status Bar": "Afficher toujours la barre d'état",
"Translation Not Available": "Traduction non disponible",
"Custom Content CSS": "CSS du contenu",
"Enter CSS for book content styling...": "Saisissez le CSS pour le contenu du livre...",
"Custom Reader UI CSS": "CSS de linterface",
"Enter CSS for reader interface styling...": "Saisissez le CSS pour linterface de lecture...",
"Crop": "Rogner",
"Book Covers": "Couvertures de livres",
"Fit": "Adapter",
"Reset {{settings}}": "Réinitialiser {{settings}}",
"Reset Settings": "Réinitialiser les paramètres",
"{{count}} pages left in chapter_one": "{{count}} page restant dans le chapitre",
"{{count}} pages left in chapter_many": "{{count}} pages restantes dans le chapitre",
"{{count}} pages left in chapter_other": "{{count}} pages restantes dans le chapitre",
"Show Remaining Pages": "Voir restantes",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gérer labonnement",
"Coming Soon": "Bientôt disponible",
"Upgrade to {{plan}}": "Passer à {{plan}}",
"Upgrade to Plus or Pro": "Passer à Plus ou Pro",
"Current Plan": "Abonnement actuel",
"Plan Limits": "Limites de labonnement",
"Processing your payment...": "Traitement de votre paiement...",
"Please wait while we confirm your subscription.": "Veuillez patienter pendant la confirmation de votre abonnement.",
"Payment Processing": "Traitement du paiement",
"Your payment is being processed. This usually takes a few moments.": "Votre paiement est en cours de traitement. Cela prend généralement quelques instants.",
"Payment Failed": "Le paiement a échoué",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Nous navons pas pu traiter votre abonnement. Veuillez réessayer ou contacter le support si le problème persiste.",
"Back to Profile": "Retour au profil",
"Subscription Successful!": "Abonnement réussi!",
"Thank you for your subscription! Your payment has been processed successfully.": "Merci pour votre abonnement! Votre paiement a été traité avec succès.",
"Email:": "E-mail:",
"Plan:": "Abonnement:",
"Amount:": "Montant:",
"Subscription ID:": "ID dabonnement:",
"Go to Library": "Aller à la bibliothèque",
"Need help? Contact our support team at support@readest.com": "Besoin daide? Contactez notre équipe à support@readest.com",
"Free Plan": "Abonnement gratuit",
"month": "mois",
"AI Translations (per day)": "Traductions IA (par jour)",
"Plus Plan": "Abonnement Plus",
"Includes All Free Plan Benefits": "Inclut tous les avantages de labonnement gratuit",
"Pro Plan": "Abonnement Pro",
"More AI Translations": "Plus de traductions IA",
"Complete Your Subscription": "Complétez votre abonnement",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% de lespace de synchronisation cloud utilisé.",
"Cloud Sync Storage": "Stockage de synchronisation cloud",
"Disable": "Désactiver",
"Enable": "Activer",
"Upgrade to Readest Premium": "Passer à Readest Premium",
"Show Source Text": "Afficher le texte source",
"Cross-Platform Sync": "Synchronisation multiplateforme",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Synchronisez bibliothèque, progrès et notes sur tous vos appareils.",
"Customizable Reading": "Lecture personnalisable",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Ajustez polices, thèmes et mise en page à votre goût.",
"AI Read Aloud": "Lecture vocale IA",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Écoutez vos livres avec des voix naturelles IA.",
"AI Translations": "Traductions IA",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Traduisez instantanément avec Google, Azure ou DeepL.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Rejoignez la communauté et recevez de laide rapidement.",
"Unlimited AI Read Aloud Hours": "Lecture IA illimitée",
"Listen without limits—convert as much text as you like into immersive audio.": "Convertissez le texte en audio sans limite.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Plus de traductions et doptions avancées.",
"DeepL Pro Access": "Accès DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Support prioritaire et réponses rapides.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Limite quotidienne atteinte. Passez à un plan supérieur pour continuer.",
"Includes All Plus Plan Benefits": "Inclut Tous les Avantages du Plan Plus",
"Early Feature Access": "Accès Anticipé aux Fonctionnalités",
"Be the first to explore new features, updates, and innovations before anyone else.": "Explorez en premier les nouvelles fonctionnalités, mises à jour et innovations.",
"Advanced AI Tools": "Outils IA Avancés",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Exploitez des outils IA puissants pour une lecture intelligente, traduction et découverte de contenu.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Traduisez jusqu'à 100 000 caractères par jour avec le moteur de traduction le plus précis.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduisez jusqu'à 500 000 caractères par jour avec le moteur de traduction le plus précis.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Stockez et accédez en sécurité à votre collection de lecture avec jusqu'à 5 GB de stockage cloud.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Stockez et accédez en sécurité à votre collection de lecture avec jusqu'à 20 GB de stockage cloud.",
"Deleted cloud backup of the book: {{title}}": "Suppression de la sauvegarde cloud du livre : {{title}}",
"Failed to delete cloud backup of the book": "Échec de la suppression de la sauvegarde cloud du livre",
"Are you sure to delete the cloud backup of the selected book?": "Êtes-vous sûr de vouloir supprimer la sauvegarde cloud du livre sélectionné ?",
"Don't have an account? Sign up": "Vous n'avez pas de compte ? Inscrivez-vous",
"What's New in Readest": "Quoi de neuf dans Readest",
"Enter book title": "Saisir le titre du livre",
"Subtitle": "Sous-titre",
"Enter book subtitle": "Saisir le sous-titre du livre",
"Enter author name": "Saisir le nom de l'auteur",
"Series": "Série",
"Enter series name": "Saisir le nom de la série",
"Series Index": "Numéro de série",
"Enter series index": "Saisir le numéro de série",
"Total in Series": "Total dans la série",
"Enter total books in series": "Saisir le nombre total de livres dans la série",
"Enter publisher": "Saisir l'éditeur",
"Publication Date": "Date de publication",
"Identifier": "Identifiant",
"Enter book description": "Saisir la description du livre",
"Change cover image": "Changer l'image de couverture",
"Replace": "Remplacer",
"Unlock cover": "Déverrouiller la couverture",
"Lock cover": "Verrouiller la couverture",
"Auto-Retrieve Metadata": "Récupérer les métadonnées automatiquement",
"Auto-Retrieve": "Récupération automatique",
"Unlock all fields": "Déverrouiller tous les champs",
"Unlock All": "Tout déverrouiller",
"Lock all fields": "Verrouiller tous les champs",
"Lock All": "Tout verrouiller",
"Reset": "Réinitialiser",
"Edit Metadata": "Modifier les métadonnées",
"Locked": "Verrouillé",
"Select Metadata Source": "Sélectionner la source des métadonnées",
"Keep manual input": "Conserver la saisie manuelle",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Fiction, Science, Histoire",
"Select Cover Image": "Sélectionner l'image de couverture",
"Open Book in New Window": "Ouvrir dans une nouvelle fenêtre",
"Voices for {{lang}}": "Voix pour {{lang}}",
"Yandex Translate": "Yandex Traduction",
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD",
"Restore Purchase": "Restaurer l'achat",
"No purchases found to restore.": "Aucun achat trouvé à restaurer.",
"Failed to restore purchases.": "Échec de la restauration des achats.",
"Failed to manage subscription.": "Échec de la gestion de l'abonnement.",
"Failed to load subscription plans.": "Échec du chargement des plans d'abonnement.",
"year": "année",
"Failed to create checkout session": "Échec de la création de la session de paiement",
"Storage": "Stockage",
"Terms of Service": "Conditions d'utilisation",
"Privacy Policy": "Politique de confidentialité",
"Disable Double Click": "Désactiver le double-clic"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "हटाने की पुष्टि करें",
"Copied to notebook": "नोटबुक में कॉपी किया गया",
"Copy": "कॉपी",
"Custom CSS": "कस्टम CSS",
"Dark Mode": "डार्क मोड",
"Default": "डिफ़ॉल्ट",
"Default Font": "डिफ़ॉल्ट फ़ॉन्ट",
@@ -28,7 +27,6 @@
"Dictionary": "शब्दकोश",
"Download Readest": "Readest डाउनलोड करें",
"Edit": "संपादित करें",
"Enter your custom CSS here...": "अपना कस्टम CSS यहां दर्ज करें...",
"Excerpts": "अंश",
"Failed to import book(s): {{filenames}}": "पुस्तक(ें) आयात करने में विफल: {{filenames}}",
"Fast": "तेज़",
@@ -48,7 +46,6 @@
"Horizontal Direction": "क्षैतिज दिशा",
"Hyphenation": "हाइफ़नेशन",
"Import Books": "पुस्तकें आयात करें",
"Language:": "भाषा:",
"Layout": "लेआउट",
"Light Mode": "लाइट मोड",
"Loading...": "लोड हो रहा है...",
@@ -70,8 +67,8 @@
"Paging Animation": "पेजिंग एनीमेशन",
"Paragraph": "पैराग्राफ",
"Parallel Read": "समानांतर पढ़ें",
"Published:": "प्रकाशित:",
"Publisher:": "प्रकाशक:",
"Published": "प्रकाशित",
"Publisher": "प्रकाशक",
"Reading Progress Synced": "पढ़ने की प्रगति सिंक की गई",
"Reload Page": "पृष्ठ रीलोड करें",
"Reveal in File Explorer": "फ़ाइल एक्सप्लोरर में दिखाएं",
@@ -96,7 +93,7 @@
"Slow": "धीमा",
"Solarized": "सोलराइज्ड",
"Speak": "बोलें",
"Subjects:": "विषय:",
"Subjects": "विषय",
"System Fonts": "सिस्टम फ़ॉन्ट्स",
"TTS not supported in this device": "यह डिवाइस TTS का समर्थन नहीं करता",
"Theme Color": "थीम रंग",
@@ -105,7 +102,7 @@
"Translated Text": "अनुवादित पाठ",
"Unknown": "अज्ञात",
"Untitled": "शीर्षकहीन",
"Updated:": "अपडेट किया गया:",
"Updated": "अपडेट किया गया",
"Version {{version}}": "संस्करण {{version}}",
"Vertical Direction": "ऊर्ध्वाधर दिशा",
"Welcome to your library. You can import your books here and read them anytime.": "आपकी लाइब्रेरी में आपका स्वागत है। आप यहां अपनी पुस्तकें आयात कर सकते हैं और कभी भी पढ़ सकते हैं।",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "पुस्तक डाउनलोड करने में विफल: {{title}}",
"Upload Book": "पुस्तक अपलोड करें",
"Auto Upload Books to Cloud": "पुस्तकें बादल में स्वचालित रूप से अपलोड करें",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% क्लाउड स्टोरेज का उपयोग किया गया है।",
"Storage": "स्टोरेज",
"Book deleted: {{title}}": "पुस्तक हटाई गई: {{title}}",
"Failed to delete book: {{title}}": "पुस्तक हटाने में विफल: {{title}}",
"Check Updates on Start": "शुरू में अपडेट जांचें",
@@ -161,7 +156,7 @@
"Create a Password": "पासवर्ड बनाएं",
"Sign up": "साइन अप करें",
"Signing up...": "साइन अप हो रहा है...",
"Dont have an account? Sign up": "कोई खाता नहीं है? साइन अप करें",
"Don't have an account? Sign up": "कोई खाता नहीं है? साइन अप करें",
"Check your email for the confirmation link": "पुष्टि लिंक के लिए अपना ईमेल जांचें",
"Signing in ...": "साइन इन हो रहा है ...",
"Send a magic link email": "जादू लिंक ईमेल भेजें",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "GitHub के साथ साइन इन करें",
"Account": "खाता",
"Failed to delete user. Please try again later.": "उपयोगकर्ता को हटाने में विफल। कृपया बाद में पुनः प्रयास करें।",
"Unlimited Offline/Online Reading": "असीमित ऑफलाइन/ऑनलाइन पढ़ना",
"Unlimited Cloud Sync Devices": "असीमित क्लाउड सिंक उपकरण",
"Essential Text-to-Speech Voices": "आवश्यक टेक्स्ट-टू-स्पीच आवाज़ें",
"DeepL Free Access": "DeepL मुफ्त पहुंच",
"Community Support": "सामुदायिक सहायता",
"500 MB Cloud Sync Space": "500 MB क्लाउड सिंक स्पेस",
"Includes All Free Tier Benefits": "सभी फ्री टियर लाभ शामिल हैं",
"AI Summaries": "AI सारांश",
"AI Translations": "AI अनुवाद",
"Priority Support": "प्राथमिकता सहायता",
"DeepL Pro Access": "DeepL प्रो पहुंच",
"Expanded Text-to-Speech Voices": "विस्तारित टेक्स्ट-टू-स्पीच आवाज़ें",
"2000 MB Cloud Sync Space": "2000 MB क्लाउड सिंक स्पेस",
"Includes All Plus Tier Benefits": "सभी प्लस टियर लाभ शामिल हैं",
"Unlimited AI Summaries": "असीमित AI सारांश",
"Unlimited AI Translations": "असीमित AI अनुवाद",
"Unlimited DeepL Pro Access": "असीमित DeepL प्रो पहुंच",
"Advanced AI Tools": "उन्नत AI टूल्स",
"Early Feature Access": "प्रारंभिक फीचर पहुंच",
"10 GB Cloud Sync Space": "10 GB क्लाउड सिंक स्पेस",
"Loading profile...": "प्रोफ़ाइल लोड हो रहा है...",
"Features": "विशेषताएं",
"Delete Account": "खाता हटाएं",
"Delete Your Account?": "अपना खाता हटाएं?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "यह क्रिया पूर्ववत नहीं की जा सकती। क्लाउड में आपका सभी डेटा स्थायी रूप से हटा दिया जाएगा।",
"Delete Permanently": "स्थायी रूप से हटाएं",
"Free Tier": "फ्री टियर",
"Plus Tier": "प्लस टियर",
"Pro Tier": "प्रो टियर",
"RTL Direction": "RTL दिशा",
"Maximum Column Height": "कॉलम की अधिकतम ऊँचाई",
"Maximum Column Width": "कॉलम की अधिकतम चौड़ाई",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "डाउनलोड कर रहा है {{downloaded}} का {{contentLength}}",
"Download finished": "डाउनलोड पूरा हुआ",
"DOWNLOAD & INSTALL": "डाउनलोड और इंस्टॉल करें",
"Changelog:": "चेंजलॉग:",
"Changelog": "चेंजलॉग",
"Software Update": "सॉफ़्टवेयर अपडेट",
"Title": "शीर्षक",
"Date Read": "पढ़ने की तारीख",
@@ -279,10 +252,9 @@
"Ascending": "आरोही",
"Descending": "अवरोही",
"Sort by...": "द्वारा क्रमबद्ध करें...",
"Added:": "जोड़ा गया:",
"Format:": "फॉर्मेट:",
"Added": "जोड़ा गया",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "विवरण:",
"Description": "विवरण",
"No description available": "कोई विवरण उपलब्ध नहीं है",
"List": "सूची",
"Grid": "ग्रिड",
@@ -299,7 +271,7 @@
"Checking for updates...": "अपडेट की जांच कर रहा है...",
"Error checking for updates": "अपडेट की जांच करते समय त्रुटि",
"Details": "विवरण",
"File Size:": "फ़ाइल आकार:",
"File Size": "फ़ाइल आकार",
"Auto Detect": "स्वचालित पहचान",
"Next Section": "अगला अनुभाग",
"Previous Section": "पिछला अनुभाग",
@@ -347,7 +319,6 @@
"Quota Exceeded": "सीमा पार",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% दैनिक अनुवाद वर्णों का उपयोग किया गया है।",
"Translation Characters": "अनुवाद वर्ण",
"Daily translation quota reached. Select another translate service to proceed.": "दैनिक अनुवाद कोटा पूरा हो गया है। आगे बढ़ने के लिए कोई अन्य अनुवाद सेवा चुनें।",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} आवाज़",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} आवाज़ें",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "कुछ गलत हो गया। चिंता न करें, हमारी टीम को सूचित कर दिया गया है और हम एक समाधान पर काम कर रहे हैं।",
@@ -363,5 +334,132 @@
"Right Margin (px)": "दायां हाशिया (px)",
"Left Margin (px)": "बायां हाशिया (px)",
"Column Gap (%)": "स्तंभों के बीच की दूरी (%)",
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं"
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं",
"Translation Not Available": "अनुवाद उपलब्ध नहीं है",
"Custom Content CSS": "सामग्री का CSS",
"Enter CSS for book content styling...": "पुस्तक सामग्री के लिए CSS दर्ज करें...",
"Custom Reader UI CSS": "इंटरफ़ेस का CSS",
"Enter CSS for reader interface styling...": "रीडर इंटरफ़ेस के लिए CSS दर्ज करें...",
"Crop": "काटें",
"Book Covers": "पुस्तक कवर",
"Fit": "फिट",
"Reset {{settings}}": "रीसेट {{settings}}",
"Reset Settings": "सेटिंग्स रीसेट करें",
"{{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",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "सदस्यता प्रबंधित करें",
"Coming Soon": "जल्द आ रहा है",
"Upgrade to {{plan}}": "{{plan}} में अपग्रेड करें",
"Upgrade to Plus or Pro": "Plus या Pro में अपग्रेड करें",
"Current Plan": "वर्तमान योजना",
"Plan Limits": "योजना की सीमाएँ",
"Processing your payment...": "आपका भुगतान संसाधित हो रहा है...",
"Please wait while we confirm your subscription.": "कृपया प्रतीक्षा करें, हम आपकी सदस्यता की पुष्टि कर रहे हैं।",
"Payment Processing": "भुगतान संसाधित हो रहा है",
"Your payment is being processed. This usually takes a few moments.": "आपका भुगतान संसाधित हो रहा है। इसमें कुछ क्षण लग सकते हैं।",
"Payment Failed": "भुगतान विफल",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "हम आपकी सदस्यता संसाधित नहीं कर सके। कृपया पुनः प्रयास करें या समस्या बने रहने पर सहायता से संपर्क करें।",
"Back to Profile": "प्रोफ़ाइल पर वापस जाएँ",
"Subscription Successful!": "सदस्यता सफल हुई!",
"Thank you for your subscription! Your payment has been processed successfully.": "आपकी सदस्यता के लिए धन्यवाद! आपका भुगतान सफलतापूर्वक संसाधित हो गया है।",
"Email:": "ईमेल:",
"Plan:": "योजना:",
"Amount:": "राशि:",
"Subscription ID:": "सदस्यता आईडी:",
"Go to Library": "लाइब्रेरी पर जाएँ",
"Need help? Contact our support team at support@readest.com": "मदद चाहिए? हमारी सहायता टीम से संपर्क करें: support@readest.com",
"Free Plan": "मुफ्त योजना",
"month": "महीना",
"AI Translations (per day)": "AI अनुवाद (प्रतिदिन)",
"Plus Plan": "Plus योजना",
"Includes All Free Plan Benefits": "सभी मुफ्त योजना के लाभ शामिल हैं",
"Pro Plan": "Pro योजना",
"More AI Translations": "और अधिक AI अनुवाद",
"Complete Your Subscription": "अपनी सदस्यता पूरी करें",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% क्लाउड सिंक स्पेस का उपयोग किया गया है।",
"Cloud Sync Storage": "क्लाउड सिंक स्टोरेज",
"Disable": "अक्षम करें",
"Enable": "सक्षम करें",
"Upgrade to Readest Premium": "Readest प्रीमियम में अपग्रेड करें",
"Show Source Text": "मूल पाठ दिखाएं",
"Cross-Platform Sync": "क्रॉस-प्लेटफ़ॉर्म सिंक",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "सभी डिवाइस पर लाइब्रेरी, प्रगति और नोट सिंक करें।",
"Customizable Reading": "अनुकूलन योग्य पढ़ाई",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "फॉन्ट, लेआउट और थीम अपनी पसंद से बदलें।",
"AI Read Aloud": "एआई वाचन",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "प्राकृतिक एआई आवाज़ों में किताबें सुनें।",
"AI Translations": "एआई अनुवाद",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Google, Azure या DeepL से तुरंत अनुवाद करें।",
"Connect with fellow readers and get help fast in our friendly community channels.": "समुदाय से जुड़ें और सहायता पाएं।",
"Unlimited AI Read Aloud Hours": "असीमित एआई वाचन",
"Listen without limits—convert as much text as you like into immersive audio.": "जितना चाहें उतना ऑडियो में सुनें।",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "अधिक अनुवाद और उन्नत विकल्प पाएं।",
"DeepL Pro Access": "DeepL प्रो एक्सेस",
"Enjoy faster responses and dedicated assistance whenever you need help.": "प्राथमिक सहायता पाएं।",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "दैनिक सीमा पूरी हुई। योजना बढ़ाएं।",
"Includes All Plus Plan Benefits": "सभी प्लस प्लान लाभ शामिल",
"Early Feature Access": "नई सुविधाओं की पहले पहुंच",
"Be the first to explore new features, updates, and innovations before anyone else.": "नई सुविधाएं, अपडेट और नवाचार सबसे पहले देखें।",
"Advanced AI Tools": "उन्नत AI उपकरण",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "स्मार्ट पढ़ने, अनुवाद और कंटेंट खोजने के लिए शक्तिशाली AI उपकरण का उपयोग करें।",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "सबसे सटीक अनुवाद इंजन के साथ दैनिक 1 लाख तक अक्षर अनुवाद करें।",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "सबसे सटीक अनुवाद इंजन के साथ दैनिक 5 लाख तक अक्षर अनुवाद करें।",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "5 GB सुरक्षित क्लाउड स्टोरेज के साथ अपना संपूर्ण पठन संग्रह संग्रहीत करें।",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "20 GB सुरक्षित क्लाउड स्टोरेज के साथ अपना संपूर्ण पठन संग्रह संग्रहीत करें।",
"Deleted cloud backup of the book: {{title}}": "पुस्तक का क्लाउड बैकअप हटाया गया: {{title}}",
"Failed to delete cloud backup of the book": "पुस्तक का क्लाउड बैकअप हटाने में विफल",
"Are you sure to delete the cloud backup of the selected book?": "क्या आप सुनिश्चित हैं कि आप चयनित पुस्तक का क्लाउड बैकअप हटाना चाहते हैं?",
"What's New in Readest": "Readest में नया क्या है",
"Enter book title": "पुस्तक का शीर्षक दर्ज करें",
"Subtitle": "उपशीर्षक",
"Enter book subtitle": "पुस्तक का उपशीर्षक दर्ज करें",
"Enter author name": "लेखक का नाम दर्ज करें",
"Series": "श्रृंखला",
"Enter series name": "श्रृंखला का नाम दर्ज करें",
"Series Index": "श्रृंखला संख्या",
"Enter series index": "श्रृंखला संख्या दर्ज करें",
"Total in Series": "श्रृंखला में कुल",
"Enter total books in series": "श्रृंखला में कुल पुस्तकों की संख्या दर्ज करें",
"Enter publisher": "प्रकाशक का नाम दर्ज करें",
"Publication Date": "प्रकाशन दिनांक",
"Identifier": "पहचानकर्ता",
"Enter book description": "पुस्तक का विवरण दर्ज करें",
"Change cover image": "कवर इमेज बदलें",
"Replace": "बदलें",
"Unlock cover": "कवर अनलॉक करें",
"Lock cover": "कवर लॉक करें",
"Auto-Retrieve Metadata": "मेटाडेटा स्वचालित रूप से प्राप्त करें",
"Auto-Retrieve": "स्वचालित प्राप्ति",
"Unlock all fields": "सभी फील्ड अनलॉक करें",
"Unlock All": "सभी अनलॉक करें",
"Lock all fields": "सभी फील्ड लॉक करें",
"Lock All": "सभी लॉक करें",
"Reset": "रीसेट",
"Edit Metadata": "मेटाडेटा संपादित करें",
"Locked": "लॉक है",
"Select Metadata Source": "मेटाडेटा स्रोत चुनें",
"Keep manual input": "मैन्युअल इनपुट रखें",
"Google Books": "गूगल बुक्स",
"Open Library": "ओपन लाइब्रेरी",
"Fiction, Science, History": "काल्पनिक, विज्ञान, इतिहास",
"Select Cover Image": "कवर इमेज चुनें",
"Open Book in New Window": "नई विंडो में पुस्तक खोलें",
"Voices for {{lang}}": "{{lang}} के लिए आवाज़ें",
"Yandex Translate": "यांडेक्स अनुवाद",
"YYYY or YYYY-MM-DD": "YYYY या YYYY-MM-DD",
"Restore Purchase": "खरीदारी पुनर्स्थापित करें",
"No purchases found to restore.": "पुनर्स्थापित करने के लिए कोई खरीदारी नहीं मिली।",
"Failed to restore purchases.": "खरीदारी पुनर्स्थापित करने में विफल।",
"Failed to manage subscription.": "सदस्यता प्रबंधित करने में विफल।",
"Failed to load subscription plans.": "सदस्यता योजनाएँ लोड करने में विफल।",
"year": "वर्ष",
"Failed to create checkout session": "चेकआउट सत्र बनाने में विफल",
"Storage": "भंडारण",
"Terms of Service": "सेवा की शर्तें",
"Privacy Policy": "गोपनीयता नीति",
"Disable Double Click": "डबल क्लिक अक्षम करें"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Konfirmasi Penghapusan",
"Copied to notebook": "Disalin ke buku catatan",
"Copy": "Salin",
"Custom CSS": "CSS Kustom",
"Dark Mode": "Mode Gelap",
"Default": "Default",
"Default Font": "Font Default",
@@ -28,7 +27,6 @@
"Dictionary": "Kamus",
"Download Readest": "Unduh Readest",
"Edit": "Edit",
"Enter your custom CSS here...": "Masukkan CSS kustom Anda di sini...",
"Excerpts": "Kutipan",
"Failed to import book(s): {{filenames}}": "Gagal mengimpor buku: {{filenames}}",
"Fast": "Cepat",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Arah Horizontal",
"Hyphenation": "Pemenggalan Kata",
"Import Books": "Impor Buku",
"Language:": "Bahasa:",
"Layout": "Tata Letak",
"Light Mode": "Mode Terang",
"Loading...": "Memuat...",
@@ -70,8 +67,8 @@
"Paging Animation": "Animasi Halaman",
"Paragraph": "Paragraf",
"Parallel Read": "Baca Paralel",
"Published:": "Diterbitkan:",
"Publisher:": "Penerbit:",
"Published": "Diterbitkan",
"Publisher": "Penerbit",
"Reading Progress Synced": "Progres membaca disinkronkan",
"Reload Page": "Muat Ulang Halaman",
"Reveal in File Explorer": "Tampilkan di File Explorer",
@@ -96,7 +93,7 @@
"Slow": "Lambat",
"Solarized": "Solarized",
"Speak": "Bicara",
"Subjects:": "Subjek:",
"Subjects": "Subjek",
"System Fonts": "Font Sistem",
"TTS not supported in this device": "TTS tidak didukung di perangkat ini",
"Theme Color": "Warna Tema",
@@ -105,7 +102,7 @@
"Translated Text": "Teks Terjemahan",
"Unknown": "Tidak Diketahui",
"Untitled": "Tanpa Judul",
"Updated:": "Diperbarui:",
"Updated": "Diperbarui",
"Version {{version}}": "Versi {{version}}",
"Vertical Direction": "Arah Vertikal",
"Welcome to your library. You can import your books here and read them anytime.": "Selamat datang di perpustakaan Anda. Anda dapat mengimpor buku-buku Anda di sini dan membacanya kapan saja.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Gagal mengunduh buku: {{title}}",
"Upload Book": "Unggah Buku",
"Auto Upload Books to Cloud": "Unggah Buku Otomatis ke Cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dari Penyimpanan Cloud Digunakan.",
"Storage": "Penyimpanan",
"Book deleted: {{title}}": "Buku dihapus: {{title}}",
"Failed to delete book: {{title}}": "Gagal menghapus buku: {{title}}",
"Check Updates on Start": "Periksa Pembaruan saat Memulai",
@@ -161,7 +156,7 @@
"Create a Password": "Buat kata sandi",
"Sign up": "Daftar",
"Signing up...": "Sedang mendaftar...",
"Dont have an account? Sign up": "Belum punya akun? Daftar",
"Don't have an account? Sign up": "Belum punya akun? Daftar",
"Check your email for the confirmation link": "Periksa email Anda untuk tautan konfirmasi",
"Signing in ...": "Sedang masuk ...",
"Send a magic link email": "Kirim email tautan ajaib",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "Masuk dengan GitHub",
"Account": "Akun",
"Failed to delete user. Please try again later.": "Gagal menghapus pengguna. Silakan coba lagi nanti.",
"Unlimited Offline/Online Reading": "Membaca Offline/Online Tanpa Batas",
"Unlimited Cloud Sync Devices": "Perangkat Sinkronisasi Cloud Tanpa Batas",
"Essential Text-to-Speech Voices": "Suara Text-to-Speech Esensial",
"DeepL Free Access": "Akses DeepL Gratis",
"Community Support": "Dukungan Komunitas",
"500 MB Cloud Sync Space": "Ruang Sinkronisasi Cloud 500 MB",
"Includes All Free Tier Benefits": "Termasuk Semua Manfaat Tingkat Gratis",
"AI Summaries": "Ringkasan AI",
"AI Translations": "Terjemahan AI",
"Priority Support": "Dukungan Prioritas",
"DeepL Pro Access": "Akses DeepL Pro",
"Expanded Text-to-Speech Voices": "Suara Text-to-Speech yang Diperluas",
"2000 MB Cloud Sync Space": "Ruang Sinkronisasi Cloud 2000 MB",
"Includes All Plus Tier Benefits": "Termasuk Semua Manfaat Tingkat Plus",
"Unlimited AI Summaries": "Ringkasan AI Tanpa Batas",
"Unlimited AI Translations": "Terjemahan AI Tanpa Batas",
"Unlimited DeepL Pro Access": "Akses DeepL Pro Tanpa Batas",
"Advanced AI Tools": "Alat AI Lanjutan",
"Early Feature Access": "Akses Fitur Awal",
"10 GB Cloud Sync Space": "Ruang Sinkronisasi Cloud 10 GB",
"Loading profile...": "Memuat profil...",
"Features": "Fitur",
"Delete Account": "Hapus Akun",
"Delete Your Account?": "Hapus Akun Anda?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Tindakan ini tidak dapat dibatalkan. Semua data Anda di cloud akan dihapus secara permanen.",
"Delete Permanently": "Hapus Secara Permanen",
"Free Tier": "Tingkat Gratis",
"Plus Tier": "Tingkat Plus",
"Pro Tier": "Tingkat Pro",
"RTL Direction": "Arah RTL",
"Maximum Column Height": "Tinggi Kolom Maksimum",
"Maximum Column Width": "Lebar Kolom Maksimum",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Mengunduh {{downloaded}} dari {{contentLength}}",
"Download finished": "Unduhan selesai",
"DOWNLOAD & INSTALL": "UNDUH & INSTAL",
"Changelog:": "Catatan Perubahan:",
"Changelog": "Catatan Perubahan",
"Software Update": "Pembaruan Perangkat Lunak",
"Title": "Judul",
"Date Read": "Tanggal Dibaca",
@@ -279,10 +252,9 @@
"Ascending": "Naik",
"Descending": "Turun",
"Sort by...": "Urutkan berdasarkan...",
"Added:": "Ditambahkan:",
"Format:": "Format:",
"Added": "Ditambahkan",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Deskripsi:",
"Description": "Deskripsi",
"No description available": "Tidak ada deskripsi yang tersedia",
"List": "Daftar",
"Grid": "Jala",
@@ -299,7 +271,7 @@
"Checking for updates...": "Memeriksa pembaruan...",
"Error checking for updates": "Kesalahan saat memeriksa pembaruan",
"Details": "Detail",
"File Size:": "Ukuran File:",
"File Size": "Ukuran File",
"Auto Detect": "Deteksi Otomatis",
"Next Section": "Bagian Selanjutnya",
"Previous Section": "Bagian Sebelumnya",
@@ -345,7 +317,6 @@
"Quota Exceeded": "Kuota terlampaui",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dari Karakter Terjemahan Harian Digunakan.",
"Translation Characters": "Karakter Terjemahan",
"Daily translation quota reached. Select another translate service to proceed.": "Kuota terjemahan harian tercapai. Pilih layanan terjemahan lain untuk melanjutkan.",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} suara",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Terjadi kesalahan. Jangan khawatir, tim kami telah diberitahu dan kami sedang bekerja untuk memperbaikinya.",
"Error Details:": "Rincian Kesalahan:",
@@ -360,5 +331,131 @@
"Right Margin (px)": "Margin Kanan (px)",
"Left Margin (px)": "Margin Kiri (px)",
"Column Gap (%)": "Jarak Antar Kolom (%)",
"Always Show Status Bar": "Selalu Tampilkan Bilah Status"
"Always Show Status Bar": "Selalu Tampilkan Bilah Status",
"Translation Not Available": "Terjemahan Tidak Tersedia",
"Custom Content CSS": "CSS konten",
"Enter CSS for book content styling...": "Masukkan CSS untuk gaya konten buku...",
"Custom Reader UI CSS": "CSS antarmuka",
"Enter CSS for reader interface styling...": "Masukkan CSS untuk gaya antarmuka pembaca...",
"Crop": "Potong",
"Book Covers": "Sampul Buku",
"Fit": "Pas",
"Reset {{settings}}": "Atur Ulang {{settings}}",
"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",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Kelola Langganan",
"Coming Soon": "Segera Hadir",
"Upgrade to {{plan}}": "Tingkatkan ke {{plan}}",
"Upgrade to Plus or Pro": "Tingkatkan ke Plus atau Pro",
"Current Plan": "Paket Saat Ini",
"Plan Limits": "Batas Paket",
"Processing your payment...": "Memproses pembayaran Anda...",
"Please wait while we confirm your subscription.": "Mohon tunggu saat kami mengonfirmasi langganan Anda.",
"Payment Processing": "Memproses Pembayaran",
"Your payment is being processed. This usually takes a few moments.": "Pembayaran Anda sedang diproses. Ini biasanya memerlukan beberapa saat.",
"Payment Failed": "Pembayaran Gagal",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Kami tidak dapat memproses langganan Anda. Silakan coba lagi atau hubungi dukungan jika masalah berlanjut.",
"Back to Profile": "Kembali ke Profil",
"Subscription Successful!": "Langganan Berhasil!",
"Thank you for your subscription! Your payment has been processed successfully.": "Terima kasih atas langganan Anda! Pembayaran Anda telah berhasil diproses.",
"Email:": "Email:",
"Plan:": "Paket:",
"Amount:": "Jumlah:",
"Subscription ID:": "ID Langganan:",
"Go to Library": "Pergi ke Perpustakaan",
"Need help? Contact our support team at support@readest.com": "Perlu bantuan? Hubungi tim dukungan kami di support@readest.com",
"Free Plan": "Paket Gratis",
"month": "bulan",
"AI Translations (per day)": "Terjemahan AI (per hari)",
"Plus Plan": "Paket Plus",
"Includes All Free Plan Benefits": "Termasuk Semua Manfaat Paket Gratis",
"Pro Plan": "Paket Pro",
"More AI Translations": "Lebih Banyak Terjemahan AI",
"Complete Your Subscription": "Selesaikan Langganan Anda",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% ruang sinkronisasi cloud terpakai",
"Cloud Sync Storage": "Penyimpanan Sinkronisasi Cloud",
"Disable": "Nonaktifkan",
"Enable": "Aktifkan",
"Upgrade to Readest Premium": "Tingkatkan ke Readest Premium",
"Show Source Text": "Perlihatkan Teks Sumber",
"Cross-Platform Sync": "Sinkronisasi Lintas Platform",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Sinkronkan pustaka, kemajuan, dan catatan di semua perangkat Anda.",
"Customizable Reading": "Membaca yang Dapat Disesuaikan",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Atur font, tata letak, dan tema sesuai keinginan.",
"AI Read Aloud": "Pembacaan AI",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Dengarkan buku dengan suara AI alami.",
"AI Translations": "Terjemahan AI",
"Connect with fellow readers and get help fast in our friendly community channels.": "Terhubung dengan komunitas dan dapatkan bantuan cepat.",
"Unlimited AI Read Aloud Hours": "Waktu Baca AI Tanpa Batas",
"Listen without limits—convert as much text as you like into immersive audio.": "Dengarkan sebanyak yang Anda mau tanpa batas.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Dapatkan lebih banyak kuota dan fitur terjemahan lanjutan.",
"DeepL Pro Access": "Akses DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Nikmati dukungan prioritas kapan saja.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Kuota harian habis. Tingkatkan paket Anda untuk lanjut.",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Terjemahkan teks apa pun secara instan dengan kekuatan Google, Azure, atau DeepL—pahami konten dalam bahasa apa pun.",
"Includes All Plus Plan Benefits": "Termasuk Semua Manfaat Paket Plus",
"Early Feature Access": "Akses Fitur Lebih Awal",
"Be the first to explore new features, updates, and innovations before anyone else.": "Jadi yang pertama menjelajahi fitur baru, pembaruan, dan inovasi.",
"Advanced AI Tools": "Alat AI Canggih",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Manfaatkan alat AI canggih untuk membaca cerdas, terjemahan, dan penemuan konten.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Terjemahkan hingga 100.000 karakter harian dengan mesin terjemahan paling akurat.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Terjemahkan hingga 500.000 karakter harian dengan mesin terjemahan paling akurat.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Simpan dan akses koleksi bacaan lengkap dengan aman menggunakan penyimpanan cloud 5 GB.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Simpan dan akses koleksi bacaan lengkap dengan aman menggunakan penyimpanan cloud 20 GB.",
"Deleted cloud backup of the book: {{title}}": "Cadangan cloud buku dihapus: {{title}}",
"Failed to delete cloud backup of the book": "Cadangan cloud buku gagal dihapus",
"Are you sure to delete the cloud backup of the selected book?": "Apakah Anda yakin ingin menghapus cadangan cloud dari buku yang dipilih?",
"What's New in Readest": "Yang Baru di Readest",
"Enter book title": "Masukkan judul buku",
"Subtitle": "Subjudul",
"Enter book subtitle": "Masukkan subjudul buku",
"Enter author name": "Masukkan nama penulis",
"Series": "Seri",
"Enter series name": "Masukkan nama seri",
"Series Index": "Nomor seri",
"Enter series index": "Masukkan nomor seri",
"Total in Series": "Total dalam seri",
"Enter total books in series": "Masukkan total buku dalam seri",
"Enter publisher": "Masukkan penerbit",
"Publication Date": "Tanggal terbit",
"Identifier": "Pengidentifikasi",
"Enter book description": "Masukkan deskripsi buku",
"Change cover image": "Ubah gambar sampul",
"Replace": "Ganti",
"Unlock cover": "Buka kunci sampul",
"Lock cover": "Kunci sampul",
"Auto-Retrieve Metadata": "Ambil metadata otomatis",
"Auto-Retrieve": "Ambil otomatis",
"Unlock all fields": "Buka kunci semua field",
"Unlock All": "Buka semua",
"Lock all fields": "Kunci semua field",
"Lock All": "Kunci semua",
"Reset": "Reset",
"Edit Metadata": "Edit metadata",
"Locked": "Terkunci",
"Select Metadata Source": "Pilih sumber metadata",
"Keep manual input": "Pertahankan input manual",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Fiksi, Sains, Sejarah",
"Select Cover Image": "Pilihan Gambar Sampul",
"Open Book in New Window": "Buka Buku di Jendela Baru",
"Voices for {{lang}}": "Suara untuk {{lang}}",
"Yandex Translate": "Yandex Translate",
"YYYY or YYYY-MM-DD": "YYYY atau YYYY-MM-DD",
"Restore Purchase": "Pulihkan Pembelian",
"No purchases found to restore.": "Tidak ada pembelian yang ditemukan untuk dipulihkan.",
"Failed to restore purchases.": "Gagal memulihkan pembelian.",
"Failed to manage subscription.": "Gagal mengelola langganan.",
"Failed to load subscription plans.": "Gagal memuat rencana langganan.",
"year": "tahun",
"Failed to create checkout session": "Gagal membuat sesi checkout",
"Storage": "Penyimpanan",
"Terms of Service": "Ketentuan Layanan",
"Privacy Policy": "Kebijakan Privasi",
"Disable Double Click": "Nonaktifkan Klik Ganda"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Conferma eliminazione",
"Copied to notebook": "Copiato nel quaderno",
"Copy": "Copia",
"Custom CSS": "CSS personalizzato",
"Dark Mode": "Modalità scura",
"Default": "Predefinito",
"Default Font": "Font predefinito",
@@ -28,7 +27,6 @@
"Dictionary": "Dizionario",
"Download Readest": "Scarica Readest",
"Edit": "Modifica",
"Enter your custom CSS here...": "Inserisci qui il tuo CSS personalizzato...",
"Excerpts": "Estratti",
"Failed to import book(s): {{filenames}}": "Impossibile importare il/i libro/i: {{filenames}}",
"Fast": "Veloce",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Direzione orizzontale",
"Hyphenation": "Sillabazione",
"Import Books": "Importa libri",
"Language:": "Lingua:",
"Layout": "Layout",
"Light Mode": "Modalità chiara",
"Loading...": "Caricamento...",
@@ -70,8 +67,8 @@
"Paging Animation": "Animazione cambio pagina",
"Paragraph": "Paragrafo",
"Parallel Read": "Lettura parallela",
"Published:": "Pubblicato:",
"Publisher:": "Editore:",
"Published": "Pubblicato",
"Publisher": "Editore",
"Reading Progress Synced": "Progresso lettura sincronizzato",
"Reload Page": "Ricarica pagina",
"Reveal in File Explorer": "Mostra in Esplora file",
@@ -96,7 +93,7 @@
"Slow": "Lento",
"Solarized": "Solarizzato",
"Speak": "Leggi",
"Subjects:": "Argomenti:",
"Subjects": "Argomenti",
"System Fonts": "Font di sistema",
"TTS not supported in this device": "TTS non supportato su questo dispositivo",
"Theme Color": "Colore tema",
@@ -105,7 +102,7 @@
"Translated Text": "Testo tradotto",
"Unknown": "Sconosciuto",
"Untitled": "Senza titolo",
"Updated:": "Aggiornato:",
"Updated": "Aggiornato",
"Version {{version}}": "Versione {{version}}",
"Vertical Direction": "Direzione verticale",
"Welcome to your library. You can import your books here and read them anytime.": "Benvenuto nella tua biblioteca. Puoi importare i tuoi libri qui e leggerli in qualsiasi momento.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Download libro non riuscito: {{title}}",
"Upload Book": "Carica libro",
"Auto Upload Books to Cloud": "Carica libri automaticamente su cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dello spazio di archiviazione cloud utilizzato.",
"Storage": "Archiviazione",
"Book deleted: {{title}}": "Libro eliminato: {{title}}",
"Failed to delete book: {{title}}": "Eliminazione libro non riuscita: {{title}}",
"Check Updates on Start": "Controlla aggiornamenti all'avvio",
@@ -161,7 +156,7 @@
"Create a Password": "Crea una password",
"Sign up": "Registrati",
"Signing up...": "Registrazione in corso...",
"Dont have an account? Sign up": "Non hai un account? Registrati",
"Don't have an account? Sign up": "Non hai un account? Registrati",
"Check your email for the confirmation link": "Controlla la tua email per il link di conferma",
"Signing in ...": "Accesso in corso ...",
"Send a magic link email": "Invia un'email con link magico",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "Accedi con GitHub",
"Account": "Account",
"Failed to delete user. Please try again later.": "Impossibile eliminare l'utente. Riprova più tardi.",
"Unlimited Offline/Online Reading": "Lettura offline/online illimitata",
"Unlimited Cloud Sync Devices": "Sincronizzazione cloud su dispositivi illimitati",
"Essential Text-to-Speech Voices": "Voci essenziali di sintesi vocale",
"DeepL Free Access": "Accesso gratuito a DeepL",
"Community Support": "Supporto della community",
"500 MB Cloud Sync Space": "500 MB di spazio di sincronizzazione cloud",
"Includes All Free Tier Benefits": "Include tutti i vantaggi del piano gratuito",
"AI Summaries": "Riassunti AI",
"AI Translations": "Traduzioni AI",
"Priority Support": "Supporto prioritario",
"DeepL Pro Access": "Accesso a DeepL Pro",
"Expanded Text-to-Speech Voices": "Voci di sintesi vocale ampliate",
"2000 MB Cloud Sync Space": "2000 MB di spazio di sincronizzazione cloud",
"Includes All Plus Tier Benefits": "Include tutti i vantaggi del piano Plus",
"Unlimited AI Summaries": "Riassunti AI illimitati",
"Unlimited AI Translations": "Traduzioni AI illimitate",
"Unlimited DeepL Pro Access": "Accesso illimitato a DeepL Pro",
"Advanced AI Tools": "Strumenti AI avanzati",
"Early Feature Access": "Accesso anticipato alle funzionalità",
"10 GB Cloud Sync Space": "10 GB di spazio di sincronizzazione cloud",
"Loading profile...": "Caricamento profilo...",
"Features": "Funzionalità",
"Delete Account": "Elimina account",
"Delete Your Account?": "Eliminare il tuo account?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Questa azione non può essere annullata. Tutti i tuoi dati nel cloud verranno eliminati definitivamente.",
"Delete Permanently": "Elimina definitivamente",
"Free Tier": "Piano gratuito",
"Plus Tier": "Piano Plus",
"Pro Tier": "Piano Pro",
"RTL Direction": "Direzione RTL",
"Maximum Column Height": "Altezza massima colonna",
"Maximum Column Width": "Larghezza massima colonna",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Scaricando {{downloaded}} di {{contentLength}}",
"Download finished": "Download completato",
"DOWNLOAD & INSTALL": "SCARICA E INSTALLA",
"Changelog:": "Note di rilascio:",
"Changelog": "Note di rilascio",
"Software Update": "Aggiornamento software",
"Title": "Titolo",
"Date Read": "Data di lettura",
@@ -279,10 +252,9 @@
"Ascending": "Crescente",
"Descending": "Decrescente",
"Sort by...": "Ordina per...",
"Added:": "Aggiunto:",
"Format:": "Formato:",
"Added": "Aggiunto",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Descrizione:",
"Description": "Descrizione",
"No description available": "Nessuna descrizione disponibile",
"List": "Elenco",
"Grid": "Griglia",
@@ -299,7 +271,7 @@
"Checking for updates...": "Controllo aggiornamenti...",
"Error checking for updates": "Errore durante il controllo degli aggiornamenti",
"Details": "Dettagli",
"File Size:": "Dimensione file:",
"File Size": "Dimensione file",
"Auto Detect": "Rilevamento automatico",
"Next Section": "Sezione successiva",
"Previous Section": "Sezione precedente",
@@ -349,7 +321,6 @@
"Quota Exceeded": "Limite superato",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dei caratteri di traduzione giornalieri utilizzati.",
"Translation Characters": "Caratteri di traduzione",
"Daily translation quota reached. Select another translate service to proceed.": "Quota di traduzione giornaliera raggiunta. Seleziona un altro servizio di traduzione per procedere.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} voce",
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} voci",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} voci",
@@ -366,5 +337,133 @@
"Right Margin (px)": "Margine Destro (px)",
"Left Margin (px)": "Margine Sinistro (px)",
"Column Gap (%)": "Spazio tra Colonne (%)",
"Always Show Status Bar": "Mostra sempre la barra di stato"
"Always Show Status Bar": "Mostra sempre la barra di stato",
"Translation Not Available": "Traduzione non disponibile",
"Custom Content CSS": "CSS contenuto",
"Enter CSS for book content styling...": "Inserisci il CSS per il contenuto del libro...",
"Custom Reader UI CSS": "CSS interfaccia",
"Enter CSS for reader interface styling...": "Inserisci il CSS per l'interfaccia di lettura...",
"Crop": "Ritaglia",
"Book Covers": "Copertine libri",
"Fit": "Adatta",
"Reset {{settings}}": "Reimposta {{settings}}",
"Reset Settings": "Reimposta impostazioni",
"{{count}} pages left in chapter_one": "{{count}} pagina rimasta nel capitolo",
"{{count}} pages left in chapter_many": "{{count}} pagine rimaste nel capitolo",
"{{count}} pages left in chapter_other": "{{count}} pagine rimaste nel capitolo",
"Show Remaining Pages": "Mostra pagine rimanenti",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gestisci Abbonamento",
"Coming Soon": "Prossimamente",
"Upgrade to {{plan}}": "Aggiorna a {{plan}}",
"Upgrade to Plus or Pro": "Aggiorna a Plus o Pro",
"Current Plan": "Piano Attuale",
"Plan Limits": "Limiti del Piano",
"Processing your payment...": "Elaborazione del pagamento...",
"Please wait while we confirm your subscription.": "Attendi mentre confermiamo il tuo abbonamento.",
"Payment Processing": "Elaborazione Pagamento",
"Your payment is being processed. This usually takes a few moments.": "Il tuo pagamento è in elaborazione. Di solito richiede pochi istanti.",
"Payment Failed": "Pagamento Fallito",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Non è stato possibile elaborare l'abbonamento. Riprova o contatta il supporto se il problema persiste.",
"Back to Profile": "Torna al Profilo",
"Subscription Successful!": "Abbonamento Avvenuto con Successo!",
"Thank you for your subscription! Your payment has been processed successfully.": "Grazie per l'abbonamento! Il pagamento è stato elaborato con successo.",
"Email:": "Email:",
"Plan:": "Piano:",
"Amount:": "Importo:",
"Subscription ID:": "ID Abbonamento:",
"Go to Library": "Vai alla Libreria",
"Need help? Contact our support team at support@readest.com": "Hai bisogno di aiuto? Contatta il nostro supporto a support@readest.com",
"Free Plan": "Piano Gratuito",
"month": "mese",
"AI Translations (per day)": "Traduzioni AI (al giorno)",
"Plus Plan": "Piano Plus",
"Includes All Free Plan Benefits": "Include Tutti i Vantaggi del Piano Gratuito",
"Pro Plan": "Piano Pro",
"More AI Translations": "Più Traduzioni AI",
"Complete Your Subscription": "Completa l'abbonamento",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% dello spazio cloud usato",
"Cloud Sync Storage": "Spazio cloud",
"Disable": "Disabilita",
"Enable": "Abilita",
"Upgrade to Readest Premium": "Aggiorna a Readest Premium",
"Show Source Text": "Mostra Testo Originale",
"Cross-Platform Sync": "Sincronizzazione Multipiattaforma",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Sincronizza libreria, progressi e note su tutti i tuoi dispositivi.",
"Customizable Reading": "Lettura Personalizzabile",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Personalizza font, layout e temi come preferisci.",
"AI Read Aloud": "Lettura AI",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Ascolta i libri con voci AI naturali.",
"AI Translations": "Traduzioni AI",
"Connect with fellow readers and get help fast in our friendly community channels.": "Connettiti con la community e ricevi supporto rapido.",
"Unlimited AI Read Aloud Hours": "Lettura AI Illimitata",
"Listen without limits—convert as much text as you like into immersive audio.": "Ascolta senza limiti quanto vuoi.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Sblocca traduzioni avanzate e più utilizzo giornaliero.",
"DeepL Pro Access": "Accesso DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Ricevi supporto prioritario quando serve.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Quota giornaliera esaurita. Passa a un piano superiore per continuare.",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Traduci qualsiasi testo istantaneamente con la potenza di Google, Azure o DeepL—comprendi contenuti in qualsiasi lingua.",
"Includes All Plus Plan Benefits": "Include Tutti i Vantaggi del Piano Plus",
"Early Feature Access": "Accesso Anticipato alle Funzionalità",
"Be the first to explore new features, updates, and innovations before anyone else.": "Esplora per primo nuove funzionalità, aggiornamenti e innovazioni.",
"Advanced AI Tools": "Strumenti AI Avanzati",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Sfrutta potenti strumenti AI per lettura intelligente, traduzione e scoperta di contenuti.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Traduci fino a 100.000 caratteri al giorno con il motore di traduzione più accurato.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduci fino a 500.000 caratteri al giorno con il motore di traduzione più accurato.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Archivia e accedi in sicurezza alla tua intera collezione di lettura con fino a 5 GB di storage cloud.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Archivia e accedi in sicurezza alla tua intera collezione di lettura con fino a 20 GB di storage cloud.",
"Deleted cloud backup of the book: {{title}}": "Backup cloud del libro eliminato: {{title}}",
"Failed to delete cloud backup of the book": "Impossibile eliminare il backup cloud del libro",
"Are you sure to delete the cloud backup of the selected book?": "Sei sicuro di voler eliminare il backup cloud del libro selezionato?",
"What's New in Readest": "Novità in Readest",
"Enter book title": "Inserisci il titolo del libro",
"Subtitle": "Sottotitolo",
"Enter book subtitle": "Inserisci il sottotitolo del libro",
"Enter author name": "Inserisci il nome dell'autore",
"Series": "Serie",
"Enter series name": "Inserisci il nome della serie",
"Series Index": "Numero della serie",
"Enter series index": "Inserisci il numero della serie",
"Total in Series": "Totale nella serie",
"Enter total books in series": "Inserisci il numero totale di libri nella serie",
"Enter publisher": "Inserisci l'editore",
"Publication Date": "Data di pubblicazione",
"Identifier": "Identificatore",
"Enter book description": "Inserisci la descrizione del libro",
"Change cover image": "Cambia immagine di copertina",
"Replace": "Sostituisci",
"Unlock cover": "Sblocca copertina",
"Lock cover": "Blocca copertina",
"Auto-Retrieve Metadata": "Recupera metadati automaticamente",
"Auto-Retrieve": "Recupero automatico",
"Unlock all fields": "Sblocca tutti i campi",
"Unlock All": "Sblocca tutto",
"Lock all fields": "Blocca tutti i campi",
"Lock All": "Blocca tutto",
"Reset": "Ripristina",
"Edit Metadata": "Modifica metadati",
"Locked": "Bloccato",
"Select Metadata Source": "Seleziona fonte metadati",
"Keep manual input": "Mantieni input manuale",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Narrativa, Scienza, Storia",
"Select Cover Image": "Seleziona immagine di copertina",
"Open Book in New Window": "Apri libro in una nuova finestra",
"Voices for {{lang}}": "Voci per {{lang}}",
"Yandex Translate": "Yandex Translate",
"YYYY or YYYY-MM-DD": "AAAA o AAAA-MM-GG",
"Restore Purchase": "Ripristina acquisto",
"No purchases found to restore.": "Nessun acquisto trovato da ripristinare.",
"Failed to restore purchases.": "Impossibile ripristinare gli acquisti.",
"Failed to manage subscription.": "Impossibile gestire l'abbonamento.",
"Failed to load subscription plans.": "Impossibile caricare i piani di abbonamento.",
"year": "anno",
"Failed to create checkout session": "Impossibile creare la sessione di checkout",
"Storage": "Archiviazione",
"Terms of Service": "Termini di servizio",
"Privacy Policy": "Informativa sulla privacy",
"Disable Double Click": "Disabilita Doppio Clic"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "削除の確認",
"Copied to notebook": "ノートブックにコピー",
"Copy": "コピー",
"Custom CSS": "カスタムCSS",
"Dark Mode": "ダークモード",
"Default": "デフォルト",
"Default Font": "デフォルトフォント",
@@ -28,7 +27,6 @@
"Dictionary": "辞書",
"Download Readest": "Readestをダウンロード",
"Edit": "編集",
"Enter your custom CSS here...": "ここにカスタムCSSを入力...",
"Excerpts": "抜粋",
"Failed to import book(s): {{filenames}}": "書籍のインポートに失敗しました:{{filenames}}",
"Fast": "高速",
@@ -48,7 +46,6 @@
"Horizontal Direction": "横書",
"Hyphenation": "ハイフネーション",
"Import Books": "書籍をインポート",
"Language:": "言語:",
"Layout": "レイアウト",
"Light Mode": "ライトモード",
"Loading...": "読み込み中...",
@@ -70,8 +67,8 @@
"Paging Animation": "ページめくりアニメーション",
"Paragraph": "段落",
"Parallel Read": "並列読書",
"Published:": "出版日",
"Publisher:": "出版社",
"Published": "出版日",
"Publisher": "出版社",
"Reading Progress Synced": "読書進捗が同期されました",
"Reload Page": "ページを再読み込み",
"Reveal in File Explorer": "エクスプローラーで表示",
@@ -96,7 +93,7 @@
"Slow": "低速",
"Solarized": "ソーラライズド",
"Speak": "読み上げ",
"Subjects:": "主題",
"Subjects": "主題",
"System Fonts": "システムフォント",
"TTS not supported in this device": "このデバイスではTTSがサポートされていません",
"Theme Color": "テーマカラー",
@@ -105,7 +102,7 @@
"Translated Text": "翻訳されたテキスト",
"Unknown": "不明",
"Untitled": "無題",
"Updated:": "更新日",
"Updated": "更新日",
"Version {{version}}": "バージョン {{version}}",
"Vertical Direction": "縦書",
"Welcome to your library. You can import your books here and read them anytime.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "書籍のダウンロードに失敗しました:{{title}}",
"Upload Book": "書籍をアップロード",
"Auto Upload Books to Cloud": "書籍を自動的にクラウドにアップロード",
"{{percentage}}% of Cloud Storage Used.": "クラウドストレージの{{percentage}}%が使用されています。",
"Storage": "ストレージ",
"Book deleted: {{title}}": "書籍が削除されました:{{title}}",
"Failed to delete book: {{title}}": "書籍の削除に失敗しました:{{title}}",
"Check Updates on Start": "開始時に更新を確認",
@@ -161,7 +156,7 @@
"Create a Password": "パスワードを作成",
"Sign up": "サインアップ",
"Signing up...": "サインアップ中...",
"Dont have an account? Sign up": "アカウントをお持ちでないですか? サインアップ",
"Don't have an account? Sign up": "アカウントをお持ちでないですか? サインアップ",
"Check your email for the confirmation link": "確認リンクのためにメールを確認してください",
"Signing in ...": "サインイン中 ...",
"Send a magic link email": "マジックリンクメールを送信",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "GitHubでサインイン",
"Account": "アカウント",
"Failed to delete user. Please try again later.": "ユーザーの削除に失敗しました。後ほど再試行してください。",
"Unlimited Offline/Online Reading": "無制限のオフライン/オンライン読書",
"Unlimited Cloud Sync Devices": "無制限のクラウド同期デバイス",
"Essential Text-to-Speech Voices": "基本的な音声合成ボイス",
"DeepL Free Access": "DeepL無料アクセス",
"Community Support": "コミュニティサポート",
"500 MB Cloud Sync Space": "500 MBのクラウド同期スペース",
"Includes All Free Tier Benefits": "無料プランの全特典を含む",
"AI Summaries": "AI要約",
"AI Translations": "AI翻訳",
"Priority Support": "優先サポート",
"DeepL Pro Access": "DeepL Proアクセス",
"Expanded Text-to-Speech Voices": "拡張された音声合成ボイス",
"2000 MB Cloud Sync Space": "2000 MBのクラウド同期スペース",
"Includes All Plus Tier Benefits": "Plusプランの全特典を含む",
"Unlimited AI Summaries": "無制限のAI要約",
"Unlimited AI Translations": "無制限のAI翻訳",
"Unlimited DeepL Pro Access": "無制限のDeepL Proアクセス",
"Advanced AI Tools": "高度なAIツール",
"Early Feature Access": "新機能への早期アクセス",
"10 GB Cloud Sync Space": "10 GBのクラウド同期スペース",
"Loading profile...": "プロフィール読み込み中...",
"Features": "機能",
"Delete Account": "アカウント削除",
"Delete Your Account?": "アカウントを削除しますか?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "この操作は元に戻せません。クラウド上のすべてのデータが完全に削除されます。",
"Delete Permanently": "完全に削除",
"Free Tier": "無料プラン",
"Plus Tier": "Plusプラン",
"Pro Tier": "Proプラン",
"RTL Direction": "RTL方向",
"Maximum Column Height": "最大列高",
"Maximum Column Width": "最大列幅",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "ダウンロード中 {{downloaded}} / {{contentLength}}",
"Download finished": "ダウンロード完了",
"DOWNLOAD & INSTALL": "更新",
"Changelog:": "変更ログ",
"Changelog": "変更ログ",
"Software Update": "ソフトウェアの更新",
"Title": "タイトル",
"Date Read": "読書日",
@@ -279,10 +252,9 @@
"Ascending": "昇順",
"Descending": "降順",
"Sort by...": "並べ替え...",
"Added:": "追加日",
"Format:": "形式:",
"Added": "追加日",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "説明",
"Description": "説明",
"No description available": "説明はありません",
"List": "リスト",
"Grid": "グリッド",
@@ -299,7 +271,7 @@
"Checking for updates...": "更新を確認中...",
"Error checking for updates": "更新の確認中にエラーが発生しました",
"Details": "詳細",
"File Size:": "ファイルサイズ",
"File Size": "ファイルサイズ",
"Auto Detect": "自動検出",
"Next Section": "次のセクション",
"Previous Section": "前のセクション",
@@ -345,7 +317,6 @@
"Quota Exceeded": "上限超過",
"{{percentage}}% of Daily Translation Characters Used.": "日次翻訳文字数の使用量は{{percentage}}%です。",
"Translation Characters": "翻訳文字数",
"Daily translation quota reached. Select another translate service to proceed.": "日次翻訳クォータに達しました。続行するには別の翻訳サービスを選択してください。",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}}音声",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "何か問題が発生しました。心配しないでください。私たちのチームが通知を受けており、修正に取り組んでいます。",
"Error Details:": "エラーの詳細:",
@@ -360,5 +331,131 @@
"Right Margin (px)": "右マージン (px)",
"Left Margin (px)": "左マージン (px)",
"Column Gap (%)": "列間の間隔 (%)",
"Always Show Status Bar": "ステータスバーを常に表示"
"Always Show Status Bar": "ステータスバーを常に表示",
"Translation Not Available": "翻訳は利用できません",
"Custom Content CSS": "コンテンツCSS",
"Enter CSS for book content styling...": "本のコンテンツ用CSSを入力...",
"Custom Reader UI CSS": "リーダーUIのCSS",
"Enter CSS for reader interface styling...": "リーダーインターフェース用CSSを入力...",
"Crop": "トリミング",
"Book Covers": "書籍の表紙",
"Fit": "フィット",
"Reset {{settings}}": "{{settings}}をリセット",
"Reset Settings": "設定をリセット",
"{{count}} pages left in chapter_other": "{{count}} ページ残り",
"Show Remaining Pages": "残りを表示",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "サブスクリプションを管理",
"Coming Soon": "近日公開",
"Upgrade to {{plan}}": "{{plan}} にアップグレード",
"Upgrade to Plus or Pro": "Plus または Pro にアップグレード",
"Current Plan": "現在のプラン",
"Plan Limits": "プランの上限",
"Processing your payment...": "お支払いを処理中...",
"Please wait while we confirm your subscription.": "サブスクリプションを確認しています。しばらくお待ちください。",
"Payment Processing": "支払いを処理中",
"Your payment is being processed. This usually takes a few moments.": "お支払いを処理しています。通常、数秒かかります。",
"Payment Failed": "支払いに失敗しました",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "サブスクリプションを処理できませんでした。再度お試しいただくか、問題が続く場合はサポートにお問い合わせください。",
"Back to Profile": "プロフィールに戻る",
"Subscription Successful!": "サブスクリプションが完了しました!",
"Thank you for your subscription! Your payment has been processed successfully.": "ご登録ありがとうございます!お支払いが正常に処理されました。",
"Email:": "メール:",
"Plan:": "プラン:",
"Amount:": "金額:",
"Subscription ID:": "サブスクリプションID:",
"Go to Library": "ライブラリへ移動",
"Need help? Contact our support team at support@readest.com": "サポートが必要ですか? support@readest.com までお問い合わせください。",
"Free Plan": "無料プラン",
"month": "月",
"AI Translations (per day)": "AI翻訳(1日あたり)",
"Plus Plan": "Plusプラン",
"Includes All Free Plan Benefits": "無料プランの全特典を含む",
"Pro Plan": "Proプラン",
"More AI Translations": "さらに多くのAI翻訳",
"Complete Your Subscription": "サブスクリプションを完了する",
"{{percentage}}% of Cloud Sync Space Used.": "クラウド同期スペースの使用量は{{percentage}}%です。",
"Cloud Sync Storage": "クラウド同期容量",
"Disable": "無効にする",
"Enable": "有効にする",
"Upgrade to Readest Premium": "Readest Premiumにアップグレード",
"Show Source Text": "原文を表示",
"Cross-Platform Sync": "クロスプラットフォーム同期",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "ライブラリや進捗、メモを全デバイスで同期。",
"Customizable Reading": "カスタマイズ可能な読書",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "フォントやレイアウト、テーマを自由に調整。",
"AI Read Aloud": "AI音声読み上げ",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "自然なAI音声で本を聴く。",
"AI Translations": "AI翻訳",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Google、Azure、DeepLで即時翻訳。",
"Connect with fellow readers and get help fast in our friendly community channels.": "コミュニティで交流・サポートを利用。",
"Unlimited AI Read Aloud Hours": "無制限のAI読み上げ",
"Listen without limits—convert as much text as you like into immersive audio.": "制限なくテキストを音声化。",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "高度な翻訳機能を利用可能に。",
"DeepL Pro Access": "DeepL Pro利用",
"Enjoy faster responses and dedicated assistance whenever you need help.": "優先サポートを利用可能。",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "翻訳上限に達しました。アップグレードで継続可能です。",
"Includes All Plus Plan Benefits": "Plusプランの全特典を含む",
"Early Feature Access": "新機能への先行アクセス",
"Be the first to explore new features, updates, and innovations before anyone else.": "新機能、アップデート、革新を誰よりも先に体験。",
"Advanced AI Tools": "高度なAIツール",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "スマートな読書、翻訳、コンテンツ発見のための強力なAIツールを活用。",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "最も正確な翻訳エンジンで1日最大10万文字を翻訳。",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "最も正確な翻訳エンジンで1日最大50万文字を翻訳。",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "最大5GBの安全なクラウドストレージで読書コレクション全体を保存・アクセス。",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "最大20GBの安全なクラウドストレージで読書コレクション全体を保存・アクセス。",
"Deleted cloud backup of the book: {{title}}": "書籍のクラウドバックアップを削除しました:{{title}}",
"Failed to delete cloud backup of the book": "書籍のクラウドバックアップを削除できませんでした",
"Are you sure to delete the cloud backup of the selected book?": "選択した書籍のクラウドバックアップを削除してもよろしいですか?",
"What's New in Readest": "Readestの新機能",
"Enter book title": "本のタイトルを入力",
"Subtitle": "サブタイトル",
"Enter book subtitle": "本のサブタイトルを入力",
"Enter author name": "著者名を入力",
"Series": "シリーズ",
"Enter series name": "シリーズ名を入力",
"Series Index": "シリーズ番号",
"Enter series index": "シリーズ番号を入力",
"Total in Series": "シリーズ総数",
"Enter total books in series": "シリーズの総冊数を入力",
"Enter publisher": "出版社を入力",
"Publication Date": "出版日",
"Identifier": "識別子",
"Enter book description": "本の説明を入力",
"Change cover image": "表紙画像を変更",
"Replace": "置き換え",
"Unlock cover": "表紙のロックを解除",
"Lock cover": "表紙をロック",
"Auto-Retrieve Metadata": "メタデータを自動取得",
"Auto-Retrieve": "自動取得",
"Unlock all fields": "すべてのフィールドのロックを解除",
"Unlock All": "すべてロック解除",
"Lock all fields": "すべてのフィールドをロック",
"Lock All": "すべてロック",
"Reset": "リセット",
"Edit Metadata": "メタデータを編集",
"Locked": "ロック中",
"Select Metadata Source": "メタデータソースを選択",
"Keep manual input": "手動入力を保持",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "小説、科学、歴史",
"Select Cover Image": "表紙画像を選択",
"Open Book in New Window": "新しいウィンドウで本を開く",
"Voices for {{lang}}": "{{lang}}の音声",
"Yandex Translate": "Yandex翻訳",
"YYYY or YYYY-MM-DD": "YYYYまたはYYYY-MM-DD",
"Restore Purchase": "購入を復元",
"No purchases found to restore.": "復元する購入が見つかりません。",
"Failed to restore purchases.": "購入の復元に失敗しました。",
"Failed to manage subscription.": "サブスクリプションの管理に失敗しました。",
"Failed to load subscription plans.": "サブスクリプションプランの読み込みに失敗しました。",
"year": "年",
"Failed to create checkout session": "チェックアウトセッションの作成に失敗しました",
"Storage": "ストレージ",
"Terms of Service": "利用規約",
"Privacy Policy": "プライバシーポリシー",
"Disable Double Click": "ダブルクリック無効"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "삭제 확인",
"Copied to notebook": "노트북에 복사됨",
"Copy": "복사",
"Custom CSS": "사용자 정의 CSS",
"Dark Mode": "다크 모드",
"Default": "기본값",
"Default Font": "기본 글꼴",
@@ -28,7 +27,6 @@
"Dictionary": "사전",
"Download Readest": "Readest 다운로드",
"Edit": "편집",
"Enter your custom CSS here...": "여기에 사용자 정의 CSS를 입력하세요...",
"Excerpts": "발췌",
"Failed to import book(s): {{filenames}}": "책 가져오기 실패: {{filenames}}",
"Fast": "빠름",
@@ -48,7 +46,6 @@
"Horizontal Direction": "수평 방향",
"Hyphenation": "하이픈 넣기",
"Import Books": "책 가져오기",
"Language:": "언어:",
"Layout": "레이아웃",
"Light Mode": "라이트 모드",
"Loading...": "로딩 중...",
@@ -70,8 +67,8 @@
"Paging Animation": "페이지 넘김 애니메이션",
"Paragraph": "단락",
"Parallel Read": "병렬 읽기",
"Published:": "출판일:",
"Publisher:": "출판사:",
"Published": "출판일",
"Publisher": "출판사",
"Reading Progress Synced": "읽기 진행 상황 동기화됨",
"Reload Page": "페이지 새로고침",
"Reveal in File Explorer": "파일 탐색기에서 표시",
@@ -96,7 +93,7 @@
"Slow": "느림",
"Solarized": "솔라라이즈드",
"Speak": "말하기",
"Subjects:": "주제:",
"Subjects": "주제",
"System Fonts": "시스템 글꼴",
"TTS not supported in this device": "이 장치에서 TTS가 지원되지 않음",
"Theme Color": "테마 색상",
@@ -105,7 +102,7 @@
"Translated Text": "번역된 텍스트",
"Unknown": "알 수 없음",
"Untitled": "제목 없음",
"Updated:": "업데이트일:",
"Updated": "업데이트일",
"Version {{version}}": "버전 {{version}}",
"Vertical Direction": "수직 방향",
"Welcome to your library. You can import your books here and read them anytime.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "책 다운로드 실패: {{title}}",
"Upload Book": "책 업로드",
"Auto Upload Books to Cloud": "책 자동으로 클라우드에 업로드",
"{{percentage}}% of Cloud Storage Used.": "클라우드 저장소 사용량 {{percentage}}%.",
"Storage": "저장소",
"Book deleted: {{title}}": "책 삭제됨: {{title}}",
"Failed to delete book: {{title}}": "책 삭제 실패: {{title}}",
"Check Updates on Start": "시작 시 업데이트 확인",
@@ -161,7 +156,7 @@
"Create a Password": "비밀번호 만들기",
"Sign up": "회원가입",
"Signing up...": "회원가입 중...",
"Dont have an account? Sign up": "계정이 없습니까? 회원가입",
"Don't have an account? Sign up": "계정이 없습니까? 회원가입",
"Check your email for the confirmation link": "확인 링크를 위해 이메일을 확인하세요",
"Signing in ...": "로그인 중 ...",
"Send a magic link email": "마법의 링크 이메일 보내기",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "GitHub로 로그인",
"Account": "계정",
"Failed to delete user. Please try again later.": "사용자 삭제에 실패했습니다. 나중에 다시 시도해 주세요.",
"Unlimited Offline/Online Reading": "무제한 오프라인/온라인 읽기",
"Unlimited Cloud Sync Devices": "무제한 클라우드 동기화 기기",
"Essential Text-to-Speech Voices": "필수 텍스트 음성 변환 목소리",
"DeepL Free Access": "DeepL 무료 접근",
"Community Support": "커뮤니티 지원",
"500 MB Cloud Sync Space": "500 MB 클라우드 동기화 공간",
"Includes All Free Tier Benefits": "무료 등급의 모든 혜택 포함",
"AI Summaries": "AI 요약",
"AI Translations": "AI 번역",
"Priority Support": "우선 지원",
"DeepL Pro Access": "DeepL 프로 접근",
"Expanded Text-to-Speech Voices": "확장된 텍스트 음성 변환 목소리",
"2000 MB Cloud Sync Space": "2000 MB 클라우드 동기화 공간",
"Includes All Plus Tier Benefits": "플러스 등급의 모든 혜택 포함",
"Unlimited AI Summaries": "무제한 AI 요약",
"Unlimited AI Translations": "무제한 AI 번역",
"Unlimited DeepL Pro Access": "무제한 DeepL 프로 접근",
"Advanced AI Tools": "고급 AI 도구",
"Early Feature Access": "신기능 조기 접근",
"10 GB Cloud Sync Space": "10 GB 클라우드 동기화 공간",
"Loading profile...": "프로필 로딩 중...",
"Features": "기능",
"Delete Account": "계정 삭제",
"Delete Your Account?": "계정을 삭제하시겠습니까?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "이 작업은 취소할 수 없습니다. 클라우드에 있는 모든 데이터가 영구적으로 삭제됩니다.",
"Delete Permanently": "영구 삭제",
"Free Tier": "무료 등급",
"Plus Tier": "플러스 등급",
"Pro Tier": "프로 등급",
"RTL Direction": "RTL 방향",
"Maximum Column Height": "최대 열 높이",
"Maximum Column Width": "최대 열 너비",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "다운로드 중 {{downloaded}} / {{contentLength}}",
"Download finished": "다운로드 완료",
"DOWNLOAD & INSTALL": "다운로드 및 설치",
"Changelog:": "변경 로그:",
"Changelog": "변경 로그",
"Software Update": "소프트웨어 업데이트",
"Title": "제목",
"Date Read": "읽은 날짜",
@@ -279,10 +252,9 @@
"Ascending": "오름차순",
"Descending": "내림차순",
"Sort by...": "정렬 기준...",
"Added:": "추가됨:",
"Format:": "형식:",
"Added": "추가됨",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "설명:",
"Description": "설명",
"No description available": "설명 없음",
"List": "목록",
"Grid": "격자",
@@ -299,7 +271,7 @@
"Checking for updates...": "업데이트 확인 중...",
"Error checking for updates": "업데이트 확인 중 오류 발생",
"Details": "세부정보",
"File Size:": "파일 크기:",
"File Size": "파일 크기",
"Auto Detect": "자동 감지",
"Next Section": "다음 섹션",
"Previous Section": "이전 섹션",
@@ -345,7 +317,6 @@
"Quota Exceeded": "한도 초과",
"{{percentage}}% of Daily Translation Characters Used.": "일일 번역 문자 사용량 {{percentage}}%.",
"Translation Characters": "번역 문자",
"Daily translation quota reached. Select another translate service to proceed.": "일일 번역 할당량에 도달했습니다. 계속 진행하려면 다른 번역 서비스를 선택하세요.",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}}개의 음성",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "문제가 발생했습니다. 걱정하지 마세요, 저희 팀이 이미 알림을 받았으며 해결 작업을 진행 중입니다.",
"Error Details:": "오류 세부사항:",
@@ -360,5 +331,131 @@
"Right Margin (px)": "오른쪽 여백 (px)",
"Left Margin (px)": "왼쪽 여백 (px)",
"Column Gap (%)": "열 간격 (%)",
"Always Show Status Bar": "상태 표시줄 항상 표시"
"Always Show Status Bar": "상태 표시줄 항상 표시",
"Translation Not Available": "번역을 사용할 수 없음",
"Custom Content CSS": "콘텐츠 CSS",
"Enter CSS for book content styling...": "도서 콘텐츠 스타일을 위한 CSS 입력...",
"Custom Reader UI CSS": "리더 UI CSS",
"Enter CSS for reader interface styling...": "리더 인터페이스 스타일용 CSS 입력...",
"Crop": "자르기",
"Book Covers": "책 표지",
"Fit": "맞춤",
"Reset {{settings}}": "{{settings}} 재설정",
"Reset Settings": "설정 재설정",
"{{count}} pages left in chapter_other": "남은 페이지 {{count}}장",
"Show Remaining Pages": "남은 페이지 보기",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "구독 관리",
"Coming Soon": "곧 출시 예정",
"Upgrade to {{plan}}": "{{plan}}로 업그레이드",
"Upgrade to Plus or Pro": "Plus 또는 Pro로 업그레이드",
"Current Plan": "현재 요금제",
"Plan Limits": "요금제 한도",
"Processing your payment...": "결제를 처리 중입니다...",
"Please wait while we confirm your subscription.": "구독을 확인하는 중입니다. 잠시만 기다려주세요.",
"Payment Processing": "결제 처리 중",
"Your payment is being processed. This usually takes a few moments.": "결제가 처리되고 있습니다. 잠시만 기다려주세요.",
"Payment Failed": "결제 실패",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "구독을 처리할 수 없습니다. 다시 시도하거나 문제가 계속되면 지원팀에 문의해주세요.",
"Back to Profile": "프로필로 돌아가기",
"Subscription Successful!": "구독이 완료되었습니다!",
"Thank you for your subscription! Your payment has been processed successfully.": "구독해주셔서 감사합니다! 결제가 성공적으로 처리되었습니다.",
"Email:": "이메일:",
"Plan:": "요금제:",
"Amount:": "금액:",
"Subscription ID:": "구독 ID:",
"Go to Library": "라이브러리로 이동",
"Need help? Contact our support team at support@readest.com": "도움이 필요하시면 support@readest.com으로 문의해주세요.",
"Free Plan": "무료 요금제",
"month": "월",
"AI Translations (per day)": "AI 번역 (일 기준)",
"Plus Plan": "Plus 요금제",
"Includes All Free Plan Benefits": "무료 요금제의 모든 혜택 포함",
"Pro Plan": "Pro 요금제",
"More AI Translations": "더 많은 AI 번역",
"Complete Your Subscription": "구독 완료",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}%의 클라우드 동기화 공간 사용됨.",
"Cloud Sync Storage": "클라우드 저장 공간",
"Disable": "비활성화",
"Enable": "활성화",
"Upgrade to Readest Premium": "Readest Premium으로 업그레이드",
"Show Source Text": "원본 텍스트 표시",
"Cross-Platform Sync": "크로스 플랫폼 동기화",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "라이브러리와 진행 상황을 모든 기기에서 동기화합니다.",
"Customizable Reading": "맞춤형 읽기",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "폰트, 레이아웃, 테마를 자유롭게 조정하세요.",
"AI Read Aloud": "AI 음성 읽기",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "자연스러운 AI 음성으로 책을 들어보세요.",
"AI Translations": "AI 번역",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Google, Azure, DeepL로 즉시 번역합니다.",
"Connect with fellow readers and get help fast in our friendly community channels.": "커뮤니티에서 도움과 소통을 즐기세요.",
"Unlimited AI Read Aloud Hours": "무제한 AI 음성 읽기",
"Listen without limits—convert as much text as you like into immersive audio.": "제한 없이 텍스트를 음성으로 변환하세요.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "고급 번역 기능을 더 많이 이용하세요.",
"DeepL Pro Access": "DeepL Pro 이용",
"Enjoy faster responses and dedicated assistance whenever you need help.": "우선 지원을 이용하세요.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "일일 번역 한도에 도달했습니다. 업그레이드하여 계속 사용하세요.",
"Includes All Plus Plan Benefits": "모든 플러스 플랜 혜택 포함",
"Early Feature Access": "신기능 우선 액세스",
"Be the first to explore new features, updates, and innovations before anyone else.": "새로운 기능, 업데이트, 혁신을 누구보다 먼저 체험하세요.",
"Advanced AI Tools": "고급 AI 도구",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "스마트한 독서, 번역, 콘텐츠 발견을 위한 강력한 AI 도구를 활용하세요.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "가장 정확한 번역 엔진으로 매일 최대 10만 자를 번역하세요.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "가장 정확한 번역 엔진으로 매일 최대 50만 자를 번역하세요.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "최대 5GB의 안전한 클라우드 저장소로 전체 독서 컬렉션을 저장하고 액세스하세요.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "최대 20GB의 안전한 클라우드 저장소로 전체 독서 컬렉션을 저장하고 액세스하세요.",
"Deleted cloud backup of the book: {{title}}": "책의 클라우드 백업이 삭제됨: {{title}}",
"Failed to delete cloud backup of the book": "책의 클라우드 백업을 삭제하지 못했습니다.",
"Are you sure to delete the cloud backup of the selected book?": "선택한 책의 클라우드 백업을 삭제하시겠습니까?",
"What's New in Readest": "Readest의 새로운 기능",
"Enter book title": "도서 제목 입력",
"Subtitle": "부제목",
"Enter book subtitle": "도서 부제목 입력",
"Enter author name": "저자명 입력",
"Series": "시리즈",
"Enter series name": "시리즈명 입력",
"Series Index": "시리즈 번호",
"Enter series index": "시리즈 번호 입력",
"Total in Series": "시리즈 총권수",
"Enter total books in series": "시리즈 총 도서 수 입력",
"Enter publisher": "출판사 입력",
"Publication Date": "출간일",
"Identifier": "식별자",
"Enter book description": "도서 설명 입력",
"Change cover image": "표지 이미지 변경",
"Replace": "교체",
"Unlock cover": "표지 잠금 해제",
"Lock cover": "표지 잠금",
"Auto-Retrieve Metadata": "메타데이터 자동 검색",
"Auto-Retrieve": "자동 검색",
"Unlock all fields": "모든 필드 잠금 해제",
"Unlock All": "모두 잠금 해제",
"Lock all fields": "모든 필드 잠금",
"Lock All": "모두 잠금",
"Reset": "초기화",
"Edit Metadata": "메타데이터 편집",
"Locked": "잠김",
"Select Metadata Source": "메타데이터 소스 선택",
"Keep manual input": "수동 입력 유지",
"Google Books": "구글 북스",
"Open Library": "오픈 라이브러리",
"Fiction, Science, History": "소설, 과학, 역사",
"Select Cover Image": "표지 이미지 선택",
"Open Book in New Window": "새 창에서 책 열기",
"Voices for {{lang}}": "{{lang}}의 음성",
"Yandex Translate": "Yandex 번역",
"YYYY or YYYY-MM-DD": "YYYY 또는 YYYY-MM-DD",
"Restore Purchase": "구매 복원",
"No purchases found to restore.": "복원할 구매가 없습니다.",
"Failed to restore purchases.": "구매 복원에 실패했습니다.",
"Failed to manage subscription.": "구독 관리에 실패했습니다.",
"Failed to load subscription plans.": "구독 플랜 로드에 실패했습니다.",
"year": "년",
"Failed to create checkout session": "체크아웃 세션 생성 실패",
"Storage": "저장소",
"Terms of Service": "서비스 약관",
"Privacy Policy": "개인정보 보호정책",
"Disable Double Click": "더블 클릭 비활성화"
}
@@ -57,8 +57,6 @@
"Import Books": "Boeken importeren",
"Select Multiple Books": "Meerdere boeken selecteren",
"Select Books": "Boeken selecteren",
"Storage": "Opslag",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% van cloudopslag gebruikt.",
"Logged in as {{userDisplayName}}": "Ingelogd als {{userDisplayName}}",
"Logged in": "Ingelogd",
"Account": "Account",
@@ -189,8 +187,6 @@
"Volume Keys for Page Flip": "Volumetoetsen voor pagina omslaan",
"Clicks for Page Flip": "Klikken voor pagina omslaan",
"Swap Clicks Area": "Klikgebieden omwisselen",
"Custom CSS": "Aangepaste CSS",
"Enter your custom CSS here...": "Voer hier uw aangepaste CSS in...",
"Apply": "Toepassen",
"Font": "Lettertype",
"Layout": "Opmaak",
@@ -227,31 +223,9 @@
"Fast": "Snel",
"Reading Progress Synced": "Leesvoortgang gesynchroniseerd",
"Failed to delete user. Please try again later.": "Gebruiker verwijderen mislukt. Probeer het later opnieuw.",
"Free Tier": "Gratis niveau",
"Community Support": "Community-ondersteuning",
"DeepL Free Access": "DeepL gratis toegang",
"Essential Text-to-Speech Voices": "Essentiële tekst-naar-spraak stemmen",
"Unlimited Offline/Online Reading": "Onbeperkt offline/online lezen",
"Unlimited Cloud Sync Devices": "Onbeperkt aantal cloud-synchronisatie apparaten",
"500 MB Cloud Sync Space": "500 MB cloud-synchronisatieruimte",
"Plus Tier": "Plus niveau",
"Includes All Free Tier Benefits": "Inclusief alle voordelen van het gratis niveau",
"Priority Support": "Prioriteitsondersteuning",
"AI Summaries": "AI-samenvattingen",
"AI Translations": "AI-vertalingen",
"DeepL Pro Access": "DeepL Pro toegang",
"Expanded Text-to-Speech Voices": "Uitgebreide tekst-naar-spraak stemmen",
"2000 MB Cloud Sync Space": "2000 MB cloud-synchronisatieruimte",
"Pro Tier": "Pro niveau",
"Includes All Plus Tier Benefits": "Inclusief alle voordelen van het Plus niveau",
"Unlimited AI Summaries": "Onbeperkte AI-samenvattingen",
"Unlimited AI Translations": "Onbeperkte AI-vertalingen",
"Unlimited DeepL Pro Access": "Onbeperkte DeepL Pro toegang",
"Advanced AI Tools": "Geavanceerde AI-tools",
"Early Feature Access": "Vroege toegang tot functies",
"10 GB Cloud Sync Space": "10 GB cloud-synchronisatieruimte",
"Loading profile...": "Profiel laden...",
"Features": "Functies",
"Sign Out": "Uitloggen",
"Delete Account": "Account verwijderen",
"Delete Your Account?": "Uw account verwijderen?",
@@ -264,15 +238,13 @@
"Error checking for updates": "Fout bij controleren op updates",
"Book Details": "Boekdetails",
"Unknown": "Onbekend",
"Publisher:": "Uitgever:",
"Published:": "Gepubliceerd:",
"Updated:": "Bijgewerkt:",
"Added:": "Toegevoegd:",
"Language:": "Taal:",
"Subjects:": "Onderwerpen:",
"Format:": "Formaat:",
"File Size:": "Bestandsgrootte:",
"Description:": "Beschrijving:",
"Publisher": "Uitgever",
"Published": "Gepubliceerd",
"Updated": "Bijgewerkt",
"Added": "Toegevoegd",
"Subjects": "Onderwerpen",
"File Size": "Bestandsgrootte",
"Description": "Beschrijving",
"No description available": "Geen beschrijving beschikbaar",
"Drop to Import Books": "Sleep om boeken te importeren",
"A new version of Readest is available!": "Een nieuwe versie van Readest is beschikbaar!",
@@ -281,7 +253,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Downloaden {{downloaded}} van {{contentLength}}",
"Download finished": "Download voltooid",
"DOWNLOAD & INSTALL": "DOWNLOADEN & INSTALLEREN",
"Changelog:": "Wijzigingslogboek:",
"Changelog": "Wijzigingslogboek",
"Software Update": "Software-update",
"LXGW WenKai GB Screen": "LXGW WenKai GB Scherm",
"LXGW WenKai TC": "LXGW WenKai TC",
@@ -299,7 +271,7 @@
"Reveal in Finder": "Tonen in Finder",
"Reveal in File Explorer": "Tonen in Verkenner",
"Reveal in Folder": "Tonen in map",
"Dont have an account? Sign up": "Nog geen account? Registreer u",
"Don't have an account? Sign up": "Nog geen account? Registreer u",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "De hele wereld is een toneel,\nEn alle mannen en vrouwen slechts spelers;\nZij hebben hun uitgangen en hun entrees,\nEn één man speelt in zijn tijd vele rollen,\nZijn daden zijnde zeven leeftijden.\n\n— William Shakespeare",
"Next Section": "Volgende sectie",
"Previous Section": "Vorige sectie",
@@ -347,7 +319,6 @@
"Quota Exceeded": "Limiet overschreden",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% van de dagelijkse vertaaltekens gebruikt.",
"Translation Characters": "Vertaaltekens",
"Daily translation quota reached. Select another translate service to proceed.": "Dagelijkse vertaalquotum bereikt. Selecteer een andere vertaalservice om door te gaan.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} stem",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} stemmen",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Er is iets misgegaan. Maak je geen zorgen, ons team is op de hoogte gebracht en we werken aan een oplossing.",
@@ -363,5 +334,132 @@
"Right Margin (px)": "Rechter marge (px)",
"Left Margin (px)": "Linker marge (px)",
"Column Gap (%)": "Kolomafstand (%)",
"Always Show Status Bar": "Statusbalk altijd weergeven"
"Always Show Status Bar": "Statusbalk altijd weergeven",
"Translation Not Available": "Vertaling niet beschikbaar",
"Custom Content CSS": "Aangepaste inhoud-CSS",
"Enter CSS for book content styling...": "Voer CSS in voor de opmaak van de boekinhoud...",
"Custom Reader UI CSS": "Aangepaste interface-CSS",
"Enter CSS for reader interface styling...": "Voer CSS in voor de opmaak van de leesinterface...",
"Crop": "Bijsnijden",
"Book Covers": "Boekomslagen",
"Fit": "Passend maken",
"Reset {{settings}}": "Reset {{settings}}",
"Reset Settings": "Instellingen resetten",
"{{count}} pages left in chapter_one": "Nog {{count}} pagina in hoofdstuk",
"{{count}} pages left in chapter_other": "Nog {{count}} paginas in hoofdstuk",
"Show Remaining Pages": "Toon resterende paginas",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Abonnement beheren",
"Coming Soon": "Binnenkort beschikbaar",
"Upgrade to {{plan}}": "Upgrade naar {{plan}}",
"Upgrade to Plus or Pro": "Upgrade naar Plus of Pro",
"Current Plan": "Huidig abonnement",
"Plan Limits": "Abonnementslimieten",
"Processing your payment...": "Je betaling wordt verwerkt...",
"Please wait while we confirm your subscription.": "Even geduld terwijl we je abonnement bevestigen.",
"Payment Processing": "Betaling verwerken",
"Your payment is being processed. This usually takes a few moments.": "Je betaling wordt verwerkt. Dit duurt meestal een paar seconden.",
"Payment Failed": "Betaling mislukt",
"Back to Profile": "Terug naar profiel",
"Subscription Successful!": "Abonnement succesvol!",
"Email:": "E-mail:",
"Plan:": "Abonnement:",
"Amount:": "Bedrag:",
"Subscription ID:": "Abonnements-ID:",
"Go to Library": "Ga naar bibliotheek",
"Need help? Contact our support team at support@readest.com": "Hulp nodig? Neem contact op met support via support@readest.com",
"Free Plan": "Gratis abonnement",
"month": "maand",
"AI Translations (per day)": "AI-vertalingen (per dag)",
"Plus Plan": "Plus-abonnement",
"Includes All Free Plan Benefits": "Bevat alle voordelen van het gratis abonnement",
"Pro Plan": "Pro-abonnement",
"More AI Translations": "Meer AI-vertalingen",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "We konden je abonnement niet verwerken. Probeer het opnieuw of neem contact op met support als het probleem blijft bestaan.",
"Thank you for your subscription! Your payment has been processed successfully.": "Bedankt voor je abonnement! Je betaling is succesvol verwerkt.",
"Complete Your Subscription": "Voltooi uw abonnement",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% van de cloud-synchronisatie ruimte gebruikt.",
"Cloud Sync Storage": "Cloud Sync Opslag",
"Disable": "Uitschakelen",
"Enable": "Inschakelen",
"Upgrade to Readest Premium": "Upgrade naar Readest Premium",
"Show Source Text": "Toon brontekst",
"Cross-Platform Sync": "Cross-Platform Synchronisatie",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Synchroniseer naadloos je bibliotheek, voortgang, markeringen en notities op al je apparaten—verlies nooit meer je plek.",
"Customizable Reading": "Aanpasbaar Lezen",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Personaliseer elk detail met aanpasbare lettertypen, lay-outs, thema's en geavanceerde weergave-instellingen voor de perfecte leeservaring.",
"AI Read Aloud": "AI Voorlezen",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Geniet van handsfree lezen met natuurlijk klinkende AI-stemmen die je boeken tot leven brengen.",
"AI Translations": "AI Vertalingen",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Vertaal elke tekst direct met de kracht van Google, Azure of DeepL—begrijp inhoud in elke taal.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Maak contact met medelezers en krijg snel hulp in onze vriendelijke community kanalen.",
"Unlimited AI Read Aloud Hours": "Onbeperkte AI Voorlees Uren",
"Listen without limits—convert as much text as you like into immersive audio.": "Luister zonder limieten—zet zoveel tekst als je wilt om in meeslepende audio.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Ontsluit verbeterde vertaalfuncties met meer dagelijks gebruik en geavanceerde opties.",
"DeepL Pro Access": "DeepL Pro Toegang",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Geniet van snellere reacties en toegewijde hulp wanneer je hulp nodig hebt.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Dagelijks vertaalquotum bereikt. Upgrade je abonnement om AI vertalingen te blijven gebruiken.",
"Includes All Plus Plan Benefits": "Inclusief Alle Plus Plan Voordelen",
"Early Feature Access": "Vroege Toegang tot Functies",
"Be the first to explore new features, updates, and innovations before anyone else.": "Ontdek als eerste nieuwe functies, updates en innovaties.",
"Advanced AI Tools": "Geavanceerde AI Tools",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Benut krachtige AI tools voor slimmer lezen, vertalen en content ontdekking.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Vertaal dagelijks tot 100.000 tekens met de meest nauwkeurige vertaalengine.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Vertaal dagelijks tot 500.000 tekens met de meest nauwkeurige vertaalengine.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Sla je hele leesverzameling veilig op en benader deze met tot 5 GB cloudopslag.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Sla je hele leesverzameling veilig op en benader deze met tot 20 GB cloudopslag.",
"Deleted cloud backup of the book: {{title}}": "Verwijderde cloudback-up van het boek: {{title}}",
"Failed to delete cloud backup of the book": "Verwijderen van cloudback-up van het boek is mislukt.",
"Are you sure to delete the cloud backup of the selected book?": "Weet je zeker dat je de cloudback-up van het geselecteerde boek wilt verwijderen?",
"What's New in Readest": "Wat is er nieuw in Readest",
"Enter book title": "Voer boektitel in",
"Subtitle": "Ondertitel",
"Enter book subtitle": "Voer boek ondertitel in",
"Enter author name": "Voer auteursnaam in",
"Series": "Serie",
"Enter series name": "Voer serienaam in",
"Series Index": "Serie nummer",
"Enter series index": "Voer serie nummer in",
"Total in Series": "Totaal in serie",
"Enter total books in series": "Voer totaal aantal boeken in serie in",
"Enter publisher": "Voer uitgever in",
"Publication Date": "Publicatiedatum",
"Identifier": "Identificatie",
"Enter book description": "Voer boekbeschrijving in",
"Change cover image": "Wijzig omslagafbeelding",
"Replace": "Vervangen",
"Unlock cover": "Omslag ontgrendelen",
"Lock cover": "Omslag vergrendelen",
"Auto-Retrieve Metadata": "Metadata automatisch ophalen",
"Auto-Retrieve": "Automatisch ophalen",
"Unlock all fields": "Alle velden ontgrendelen",
"Unlock All": "Alles ontgrendelen",
"Lock all fields": "Alle velden vergrendelen",
"Lock All": "Alles vergrendelen",
"Reset": "Herstellen",
"Edit Metadata": "Metadata bewerken",
"Locked": "Vergrendeld",
"Select Metadata Source": "Selecteer metadata bron",
"Keep manual input": "Handmatige invoer behouden",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Fictie, Wetenschap, Geschiedenis",
"Select Cover Image": "Selecteer omslagafbeelding",
"Open Book in New Window": "Open boek in nieuw venster",
"Voices for {{lang}}": "Stemmen voor {{lang}}",
"Yandex Translate": "Yandex Vertalen",
"YYYY or YYYY-MM-DD": "YYYY of YYYY-MM-DD",
"Restore Purchase": "Herstel aankoop",
"No purchases found to restore.": "Geen aankopen gevonden om te herstellen.",
"Failed to restore purchases.": "Herstellen van aankopen is mislukt.",
"Failed to manage subscription.": "Beheren van abonnement is mislukt.",
"Failed to load subscription plans.": "Laden van abonnement plannen is mislukt.",
"year": "jaar",
"Failed to create checkout session": "Kon de afreken sessie niet aanmaken",
"Storage": "Opslag",
"Terms of Service": "Gebruiksvoorwaarden",
"Privacy Policy": "Privacybeleid",
"Disable Double Click": "Dubbelklikken uitschakelen"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Potwierdź usunięcie",
"Copied to notebook": "Skopiowano do notatnika",
"Copy": "Kopiuj",
"Custom CSS": "Własny CSS",
"Dark Mode": "Tryb ciemny",
"Default": "Domyślne",
"Default Font": "Domyślna czcionka",
@@ -28,7 +27,6 @@
"Dictionary": "Słownik",
"Download Readest": "Pobierz Readest",
"Edit": "Edytuj",
"Enter your custom CSS here...": "Wprowadź własny CSS tutaj...",
"Excerpts": "Fragmenty",
"Failed to import book(s): {{filenames}}": "Nie udało się zaimportować książek: {{filenames}}",
"Fast": "Szybko",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Kierunek poziomy",
"Hyphenation": "Dzielenie wyrazów",
"Import Books": "Importuj książki",
"Language:": "Język:",
"Layout": "Układ",
"Light Mode": "Tryb jasny",
"Loading...": "Ładowanie...",
@@ -70,8 +67,8 @@
"Paging Animation": "Animacja przewracania stron",
"Paragraph": "Akapit",
"Parallel Read": "Czytanie równoległe",
"Published:": "Opublikowano:",
"Publisher:": "Wydawca:",
"Published": "Opublikowano",
"Publisher": "Wydawca",
"Reading Progress Synced": "Zsynchronizowano postęp czytania",
"Reload Page": "Odśwież stronę",
"Reveal in File Explorer": "Pokaż w Eksploratorze plików",
@@ -96,7 +93,7 @@
"Slow": "Wolno",
"Solarized": "Słoneczny",
"Speak": "Czytaj",
"Subjects:": "Tematy:",
"Subjects": "Tematy",
"System Fonts": "Czcionki systemowe",
"TTS not supported in this device": "TTS nie jest obsługiwane na tym urządzeniu",
"Theme Color": "Kolor motywu",
@@ -105,7 +102,7 @@
"Translated Text": "Przetłumaczony tekst",
"Unknown": "Nieznane",
"Untitled": "Bez tytułu",
"Updated:": "Zaktualizowano:",
"Updated": "Zaktualizowano",
"Version {{version}}": "Wersja {{version}}",
"Vertical Direction": "Kierunek pionowy",
"Welcome to your library. You can import your books here and read them anytime.": "Witaj w swojej bibliotece. Możesz tutaj zaimportować swoje książki i czytać je w dowolnym momencie.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Nie udało się pobrać książki: {{title}}",
"Upload Book": "Prześlij książkę",
"Auto Upload Books to Cloud": "Automatycznie przesyłaj książki do chmury",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% przestrzeni dyskowej w chmurze użytej.",
"Storage": "Przechowywanie",
"Book deleted: {{title}}": "Książka usunięta: {{title}}",
"Failed to delete book: {{title}}": "Nie udało się usunąć książki: {{title}}",
"Check Updates on Start": "Sprawdź aktualizacje przy starcie",
@@ -161,7 +156,7 @@
"Create a Password": "Utwórz hasło",
"Sign up": "Zarejestruj się",
"Signing up...": "Rejestracja...",
"Dont have an account? Sign up": "Nie masz konta? Zarejestruj się",
"Don't have an account? Sign up": "Nie masz konta? Zarejestruj się",
"Check your email for the confirmation link": "Sprawdź swój email, aby uzyskać link potwierdzający",
"Signing in ...": "Logowanie ...",
"Send a magic link email": "Wyślij link magiczny na email",
@@ -185,35 +180,13 @@
"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.",
"Unlimited Offline/Online Reading": "Nieograniczone czytanie offline/online",
"Unlimited Cloud Sync Devices": "Nieograniczona liczba urządzeń synchronizowanych w chmurze",
"Essential Text-to-Speech Voices": "Podstawowe głosy zamiany tekstu na mowę",
"DeepL Free Access": "Darmowy dostęp do DeepL",
"Community Support": "Wsparcie społeczności",
"500 MB Cloud Sync Space": "500 MB przestrzeni synchronizacji w chmurze",
"Includes All Free Tier Benefits": "Zawiera wszystkie korzyści darmowego planu",
"AI Summaries": "Podsumowania AI",
"AI Translations": "Tłumaczenia AI",
"Priority Support": "Priorytetowe wsparcie",
"DeepL Pro Access": "Dostęp do DeepL Pro",
"Expanded Text-to-Speech Voices": "Rozszerzone głosy zamiany tekstu na mowę",
"2000 MB Cloud Sync Space": "2000 MB przestrzeni synchronizacji w chmurze",
"Includes All Plus Tier Benefits": "Zawiera wszystkie korzyści planu Plus",
"Unlimited AI Summaries": "Nieograniczone podsumowania AI",
"Unlimited AI Translations": "Nieograniczone tłumaczenia AI",
"Unlimited DeepL Pro Access": "Nieograniczony dostęp do DeepL Pro",
"Advanced AI Tools": "Zaawansowane narzędzia AI",
"Early Feature Access": "Wczesny dostęp do nowych funkcji",
"10 GB Cloud Sync Space": "10 GB przestrzeni synchronizacji w chmurze",
"Loading profile...": "Ładowanie profilu...",
"Features": "Funkcje",
"Delete Account": "Usuń konto",
"Delete Your Account?": "Usunąć Twoje konto?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Tej operacji nie można cofnąć. Wszystkie Twoje dane w chmurze zostaną trwale usunięte.",
"Delete Permanently": "Usuń trwale",
"Free Tier": "Plan darmowy",
"Plus Tier": "Plan Plus",
"Pro Tier": "Plan Pro",
"RTL Direction": "Kierunek RTL",
"Maximum Column Height": "Maksymalna wysokość kolumny",
"Maximum Column Width": "Maksymalna szerokość kolumny",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Pobieranie {{downloaded}} z {{contentLength}}",
"Download finished": "Pobieranie zakończone",
"DOWNLOAD & INSTALL": "POBIERZ I ZAINSTALUJ",
"Changelog:": "Dziennik zmian:",
"Changelog": "Dziennik zmian",
"Software Update": "Aktualizacja oprogramowania",
"Title": "Tytuł",
"Date Read": "Data przeczytania",
@@ -279,10 +252,9 @@
"Ascending": "Rosnąco",
"Descending": "Malejąco",
"Sort by...": "Sortuj według...",
"Added:": "Data dodania:",
"Format:": "Format:",
"Added": "Data dodania",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Opis:",
"Description": "Opis",
"No description available": "Brak opisu",
"List": "Lista",
"Grid": "Siatka",
@@ -299,7 +271,7 @@
"Checking for updates...": "Sprawdzanie aktualizacji...",
"Error checking for updates": "Błąd podczas sprawdzania aktualizacji",
"Details": "Szczegóły",
"File Size:": "Rozmiar pliku:",
"File Size": "Rozmiar pliku",
"Auto Detect": "Automatyczne wykrywanie",
"Next Section": "Następna sekcja",
"Previous Section": "Poprzednia sekcja",
@@ -351,7 +323,6 @@
"Quota Exceeded": "Limit przekroczony",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% użytych znaków tłumaczenia dziennego.",
"Translation Characters": "Znaki tłumaczenia",
"Daily translation quota reached. Select another translate service to proceed.": "Przekroczono dzienny limit znaków tłumaczenia. Wybierz inną usługę tłumaczenia, aby kontynuować.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} głos",
"{{engine}}: {{count}} voices_few": "{{engine}}: {{count}} głosy",
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} głosów",
@@ -369,5 +340,134 @@
"Right Margin (px)": "Prawy margines (px)",
"Left Margin (px)": "Lewy margines (px)",
"Column Gap (%)": "Odstęp między kolumnami (%)",
"Always Show Status Bar": "Zawsze pokazuj pasek stanu"
"Always Show Status Bar": "Zawsze pokazuj pasek stanu",
"Translation Not Available": "Brak dostępnego tłumaczenia",
"Custom Content CSS": "CSS treści",
"Enter CSS for book content styling...": "Wprowadź CSS dla stylu treści książki...",
"Custom Reader UI CSS": "CSS interfejsu",
"Enter CSS for reader interface styling...": "Wprowadź CSS dla stylu interfejsu czytnika...",
"Crop": "Przytnij",
"Book Covers": "Okładki książek",
"Fit": "Pasuje",
"Reset {{settings}}": "Zresetuj {{settings}}",
"Reset Settings": "Zresetuj ustawienia",
"{{count}} pages left in chapter_one": "Pozostała {{count}} strona w rozdziale",
"{{count}} pages left in chapter_few": "Pozostały {{count}} strony w rozdziale",
"{{count}} pages left in chapter_many": "Pozostało {{count}} stron w rozdziale",
"{{count}} pages left in chapter_other": "Pozostało {{count}} stron w rozdziale",
"Show Remaining Pages": "Pokaż pozostałe strony",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Zarządzaj subskrypcją",
"Coming Soon": "Wkrótce dostępne",
"Upgrade to {{plan}}": "Uaktualnij do {{plan}}",
"Upgrade to Plus or Pro": "Uaktualnij do Plus lub Pro",
"Current Plan": "Obecny plan",
"Plan Limits": "Limity planu",
"Processing your payment...": "Przetwarzanie płatności...",
"Please wait while we confirm your subscription.": "Proszę czekać, potwierdzamy subskrypcję.",
"Payment Processing": "Przetwarzanie płatności",
"Your payment is being processed. This usually takes a few moments.": "Twoja płatność jest przetwarzana. Zwykle trwa to kilka chwil.",
"Payment Failed": "Płatność nie powiodła się",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Nie udało się przetworzyć subskrypcji. Spróbuj ponownie lub skontaktuj się z pomocą techniczną, jeśli problem będzie się powtarzać.",
"Back to Profile": "Powrót do profilu",
"Subscription Successful!": "Subskrypcja zakończona sukcesem!",
"Thank you for your subscription! Your payment has been processed successfully.": "Dziękujemy za subskrypcję! Twoja płatność została pomyślnie przetworzona.",
"Email:": "E-mail:",
"Plan:": "Plan:",
"Amount:": "Kwota:",
"Subscription ID:": "ID subskrypcji:",
"Go to Library": "Przejdź do biblioteki",
"Need help? Contact our support team at support@readest.com": "Potrzebujesz pomocy? Skontaktuj się z naszym zespołem wsparcia: support@readest.com",
"Free Plan": "Bezpłatny plan",
"month": "miesiąc",
"AI Translations (per day)": "Tłumaczenia AI (na dzień)",
"Plus Plan": "Plan Plus",
"Includes All Free Plan Benefits": "Zawiera wszystkie korzyści planu darmowego",
"Pro Plan": "Plan Pro",
"More AI Translations": "Więcej tłumaczeń AI",
"Complete Your Subscription": "Ukończ subskrypcję",
"{{percentage}}% of Cloud Sync Space Used.": "Użyto {{percentage}}% przestrzeni synchronizacji w chmurze.",
"Cloud Sync Storage": "Miejsce w chmurze",
"Disable": "Wyłącz",
"Enable": "Włącz",
"Upgrade to Readest Premium": "Uaktualnij do Readest Premium",
"Show Source Text": "Pokaż tekst źródłowy",
"Cross-Platform Sync": "Synchronizacja Między Platformami",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Synchronizuj płynnie swoją bibliotekę, postęp, zaznaczenia i notatki na wszystkich urządzeniach—nigdy więcej nie zgub swojego miejsca.",
"Customizable Reading": "Personalizowane Czytanie",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Personalizuj każdy szczegół dzięki regulowanym czcionkom, układom, motywom i zaawansowanym ustawieniom wyświetlania dla idealnego doświadczenia czytania.",
"AI Read Aloud": "AI Czytanie Na Głos",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Ciesz się czytaniem bez użycia rąk dzięki naturalnie brzmiącym głosom AI, które ożywiają twoje książki.",
"AI Translations": "Tłumaczenia AI",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Tłumacz dowolny tekst natychmiast dzięki mocy Google, Azure lub DeepL—rozumiej treści w każdym języku.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Połącz się z innymi czytelnikami i uzyskaj szybką pomoc na naszych przyjaznych kanałach społeczności.",
"Unlimited AI Read Aloud Hours": "Nielimitowane Godziny AI Czytania Na Głos",
"Listen without limits—convert as much text as you like into immersive audio.": "Słuchaj bez ograniczeń—przekształć tyle tekstu, ile chcesz, w wciągające audio.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Odblokuj ulepszone możliwości tłumaczenia z większym dziennym użyciem i zaawansowanymi opcjami.",
"DeepL Pro Access": "Dostęp do DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Ciesz się szybszymi odpowiedziami i dedykowaną pomocą, kiedy tylko potrzebujesz wsparcia.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Osiągnięto dzienny limit tłumaczeń. Uaktualnij swój plan, aby kontynuować korzystanie z tłumaczeń AI.",
"Includes All Plus Plan Benefits": "Zawiera Wszystkie Korzyści Planu Plus",
"Early Feature Access": "Wcześniejszy Dostęp do Funkcji",
"Be the first to explore new features, updates, and innovations before anyone else.": "Odkrywaj nowe funkcje, aktualizacje i innowacje przed innymi.",
"Advanced AI Tools": "Zaawansowane Narzędzia AI",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Wykorzystuj potężne narzędzia AI do inteligentnego czytania, tłumaczenia i odkrywania treści.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Tłumacz do 100 000 znaków dziennie z najdokładniejszą dostępną maszyną tłumaczeniową.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Tłumacz do 500 000 znaków dziennie z najdokładniejszą dostępną maszyną tłumaczeniową.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Bezpiecznie przechowuj i uzyskuj dostęp do całej kolekcji czytelniczej z do 5 GB pamięci w chmurze.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Bezpiecznie przechowuj i uzyskuj dostęp do całej kolekcji czytelniczej z do 20 GB pamięci w chmurze.",
"Deleted cloud backup of the book: {{title}}": "Usunięto kopię zapasową książki w chmurze: {{title}}",
"Failed to delete cloud backup of the book": "Nie udało się usunąć kopii zapasowej książki w chmurze.",
"Are you sure to delete the cloud backup of the selected book?": "Czy na pewno chcesz usunąć kopię zapasową chmury wybranej książki?",
"What's New in Readest": "Co nowego w Readest",
"Enter book title": "Wprowadź tytuł książki",
"Subtitle": "Podtytuł",
"Enter book subtitle": "Wprowadź podtytuł książki",
"Enter author name": "Wprowadź imię autora",
"Series": "Seria",
"Enter series name": "Wprowadź nazwę serii",
"Series Index": "Numer w serii",
"Enter series index": "Wprowadź numer w serii",
"Total in Series": "Łącznie w serii",
"Enter total books in series": "Wprowadź łączną liczbę książek w serii",
"Enter publisher": "Wprowadź wydawcę",
"Publication Date": "Data publikacji",
"Identifier": "Identyfikator",
"Enter book description": "Wprowadź opis książki",
"Change cover image": "Zmień obraz okładki",
"Replace": "Zastąp",
"Unlock cover": "Odblokuj okładkę",
"Lock cover": "Zablokuj okładkę",
"Auto-Retrieve Metadata": "Automatycznie pobierz metadane",
"Auto-Retrieve": "Automatyczne pobieranie",
"Unlock all fields": "Odblokuj wszystkie pola",
"Unlock All": "Odblokuj wszystko",
"Lock all fields": "Zablokuj wszystkie pola",
"Lock All": "Zablokuj wszystko",
"Reset": "Resetuj",
"Edit Metadata": "Edytuj metadane",
"Locked": "Zablokowane",
"Select Metadata Source": "Wybierz źródło metadanych",
"Keep manual input": "Zachowaj wprowadzenie ręczne",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Beletrystyka, Nauka, Historia",
"Select Cover Image": "Zaznacz obraz okładki",
"Open Book in New Window": "Otwórz książkę w nowym oknie",
"Voices for {{lang}}": "{{lang}} Głosy",
"Yandex Translate": "Yandex Tłumacz",
"YYYY or YYYY-MM-DD": "YYYY lub YYYY-MM-DD",
"Restore Purchase": "Przywróć zakup",
"No purchases found to restore.": "Nie znaleziono zakupów do przywrócenia.",
"Failed to restore purchases.": "Nie udało się przywrócić zakupów.",
"Failed to manage subscription.": "Nie udało się zarządzać subskrypcją.",
"Failed to load subscription plans.": "Nie udało się załadować planów subskrypcyjnych.",
"year": "rok",
"Failed to create checkout session": "Nie udało się utworzyć sesji zakupu",
"Storage": "Przechowywanie",
"Terms of Service": "Warunki korzystania z usługi",
"Privacy Policy": "Polityka prywatności",
"Disable Double Click": "Wyłącz podwójne kliknięcie"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Confirmar Exclusão",
"Copied to notebook": "Copiado para o caderno",
"Copy": "Copiar",
"Custom CSS": "CSS Personalizado",
"Dark Mode": "Modo Escuro",
"Default": "Padrão",
"Default Font": "Fonte Padrão",
@@ -28,7 +27,6 @@
"Dictionary": "Dicionário",
"Download Readest": "Baixar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Insira seu CSS personalizado aqui...",
"Excerpts": "Trechos",
"Failed to import book(s): {{filenames}}": "Falha ao importar livro(s): {{filenames}}",
"Fast": "Rápido",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Direção Horizontal",
"Hyphenation": "Hifenização",
"Import Books": "Importar Livros",
"Language:": "Idioma:",
"Layout": "Layout",
"Light Mode": "Modo Claro",
"Loading...": "Carregando...",
@@ -70,8 +67,8 @@
"Paging Animation": "Animação de Página",
"Paragraph": "Parágrafo",
"Parallel Read": "Leitura Paralela",
"Published:": "Publicado:",
"Publisher:": "Editora:",
"Published": "Publicado",
"Publisher": "Editora",
"Reading Progress Synced": "Progresso de leitura sincronizado",
"Reload Page": "Recarregar Página",
"Reveal in File Explorer": "Mostrar no Explorador de Arquivos",
@@ -96,7 +93,7 @@
"Slow": "Lento",
"Solarized": "Solarizado",
"Speak": "Falar",
"Subjects:": "Assuntos:",
"Subjects": "Assuntos",
"System Fonts": "Fontes do Sistema",
"TTS not supported in this device": "TTS não suportado neste dispositivo",
"Theme Color": "Cor do Tema",
@@ -105,7 +102,7 @@
"Translated Text": "Texto Traduzido",
"Unknown": "Desconhecido",
"Untitled": "Sem Título",
"Updated:": "Atualizado:",
"Updated": "Atualizado",
"Version {{version}}": "Versão {{version}}",
"Vertical Direction": "Direção Vertical",
"Welcome to your library. You can import your books here and read them anytime.": "Bem-vindo à sua biblioteca. Você pode importar seus livros aqui e lê-los a qualquer momento.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Falha ao baixar livro: {{title}}",
"Upload Book": "Enviar Livro",
"Auto Upload Books to Cloud": "Enviar Livros para a Nuvem Automaticamente",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% do Armazenamento na Nuvem Usado.",
"Storage": "Armazenamento",
"Book deleted: {{title}}": "Livro excluído: {{title}}",
"Failed to delete book: {{title}}": "Falha ao excluir livro: {{title}}",
"Check Updates on Start": "Verificar Atualizações ao Iniciar",
@@ -161,7 +156,7 @@
"Create a Password": "Criar uma senha",
"Sign up": "Cadastrar-se",
"Signing up...": "Cadastrando...",
"Dont have an account? Sign up": "Não tem uma conta? Cadastre-se",
"Don't have an account? Sign up": "Não tem uma conta? Cadastre-se",
"Check your email for the confirmation link": "Verifique seu e-mail para o link de confirmação",
"Signing in ...": "Entrando ...",
"Send a magic link email": "Enviar um link mágico por e-mail",
@@ -185,35 +180,13 @@
"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.",
"Unlimited Offline/Online Reading": "Leitura offline/online ilimitada",
"Unlimited Cloud Sync Devices": "Sincronização ilimitada de dispositivos na nuvem",
"Essential Text-to-Speech Voices": "Vozes essenciais de texto para fala",
"DeepL Free Access": "Acesso gratuito ao DeepL",
"Community Support": "Suporte da comunidade",
"500 MB Cloud Sync Space": "500 MB de espaço de sincronização na nuvem",
"Includes All Free Tier Benefits": "Inclui todos os benefícios do plano gratuito",
"AI Summaries": "Resumos de IA",
"AI Translations": "Traduções de IA",
"Priority Support": "Suporte prioritário",
"DeepL Pro Access": "Acesso ao DeepL Pro",
"Expanded Text-to-Speech Voices": "Vozes ampliadas de texto para fala",
"2000 MB Cloud Sync Space": "2000 MB de espaço de sincronização na nuvem",
"Includes All Plus Tier Benefits": "Inclui todos os benefícios do plano Plus",
"Unlimited AI Summaries": "Resumos de IA ilimitados",
"Unlimited AI Translations": "Traduções de IA ilimitadas",
"Unlimited DeepL Pro Access": "Acesso ilimitado ao DeepL Pro",
"Advanced AI Tools": "Ferramentas avançadas de IA",
"Early Feature Access": "Acesso antecipado a recursos",
"10 GB Cloud Sync Space": "10 GB de espaço de sincronização na nuvem",
"Loading profile...": "Carregando perfil...",
"Features": "Recursos",
"Delete Account": "Excluir conta",
"Delete Your Account?": "Excluir sua conta?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Esta ação não pode ser desfeita. Todos os seus dados na nuvem serão excluídos permanentemente.",
"Delete Permanently": "Excluir permanentemente",
"Free Tier": "Plano gratuito",
"Plus Tier": "Plano Plus",
"Pro Tier": "Plano Pro",
"RTL Direction": "Direção RTL",
"Maximum Column Height": "Altura Máxima da Coluna",
"Maximum Column Width": "Largura Máxima da Coluna",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Baixando {{downloaded}} de {{contentLength}}",
"Download finished": "Download concluído",
"DOWNLOAD & INSTALL": "BAIXAR E INSTALAR",
"Changelog:": "Notas de versão:",
"Changelog": "Notas de versão",
"Software Update": "Atualização de Software",
"Title": "Título",
"Date Read": "Data de leitura",
@@ -279,10 +252,9 @@
"Ascending": "Crescente",
"Descending": "Decrescente",
"Sort by...": "Ordenar por...",
"Added:": "Adicionado:",
"Format:": "Formato:",
"Added": "Adicionado",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Descrição:",
"Description": "Descrição",
"No description available": "Nenhuma descrição disponível",
"List": "Lista",
"Grid": "Grade",
@@ -299,7 +271,7 @@
"Checking for updates...": "Verificando atualizações...",
"Error checking for updates": "Erro ao verificar atualizações",
"Details": "Detalhes",
"File Size:": "Tamanho do arquivo:",
"File Size": "Tamanho do arquivo",
"Auto Detect": "Detectar Automaticamente",
"Next Section": "Próxima seção",
"Previous Section": "Seção anterior",
@@ -349,7 +321,6 @@
"Quota Exceeded": "Limite excedido",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% dos caracteres de tradução diários usados.",
"Translation Characters": "Caracteres de Tradução",
"Daily translation quota reached. Select another translate service to proceed.": "Quota diária de tradução atingida. Selecione outro serviço de tradução para continuar.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} voz",
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} vozes",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} vozes",
@@ -366,5 +337,133 @@
"Right Margin (px)": "Margem direita (px)",
"Left Margin (px)": "Margem esquerda (px)",
"Column Gap (%)": "Espaço entre colunas (%)",
"Always Show Status Bar": "Exibir sempre a barra de status"
"Always Show Status Bar": "Exibir sempre a barra de status",
"Translation Not Available": "Tradução não disponível",
"Custom Content CSS": "CSS do conteúdo",
"Enter CSS for book content styling...": "Insira o CSS para o estilo do conteúdo do livro...",
"Custom Reader UI CSS": "CSS da interface",
"Enter CSS for reader interface styling...": "Insira o CSS para o estilo da interface de leitura...",
"Crop": "Cortar",
"Book Covers": "Capas de Livros",
"Fit": "Encaixar",
"Reset {{settings}}": "Redefinir {{settings}}",
"Reset Settings": "Redefinir Configurações",
"{{count}} pages left in chapter_one": "Falta {{count}} página neste capítulo",
"{{count}} pages left in chapter_many": "Faltam {{count}} páginas neste capítulo",
"{{count}} pages left in chapter_other": "Faltam {{count}} páginas neste capítulo",
"Show Remaining Pages": "Mostrar páginas restantes",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Gerenciar assinatura",
"Coming Soon": "Em breve",
"Upgrade to {{plan}}": "Atualizar para {{plan}}",
"Upgrade to Plus or Pro": "Atualizar para Plus ou Pro",
"Current Plan": "Plano atual",
"Plan Limits": "Limites do plano",
"Processing your payment...": "Processando seu pagamento...",
"Please wait while we confirm your subscription.": "Por favor, aguarde enquanto confirmamos sua assinatura.",
"Payment Processing": "Processando pagamento",
"Your payment is being processed. This usually takes a few moments.": "Seu pagamento está sendo processado. Isso normalmente leva alguns instantes.",
"Payment Failed": "Falha no pagamento",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Não foi possível processar sua assinatura. Tente novamente ou entre em contato com o suporte se o problema persistir.",
"Back to Profile": "Voltar ao perfil",
"Subscription Successful!": "Assinatura realizada com sucesso!",
"Thank you for your subscription! Your payment has been processed successfully.": "Obrigado pela sua assinatura! Seu pagamento foi processado com sucesso.",
"Email:": "E-mail:",
"Plan:": "Plano:",
"Amount:": "Valor:",
"Subscription ID:": "ID da assinatura:",
"Go to Library": "Ir para a biblioteca",
"Need help? Contact our support team at support@readest.com": "Precisa de ajuda? Entre em contato com nossa equipe de suporte: support@readest.com",
"Free Plan": "Plano gratuito",
"month": "mês",
"AI Translations (per day)": "Traduções com IA (por dia)",
"Plus Plan": "Plano Plus",
"Includes All Free Plan Benefits": "Inclui todos os benefícios do plano gratuito",
"Pro Plan": "Plano Pro",
"More AI Translations": "Mais traduções com IA",
"Complete Your Subscription": "Complete sua assinatura",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% do espaço de sincronização na nuvem utilizado.",
"Cloud Sync Storage": "Armazenamento na Nuvem",
"Disable": "Desativar",
"Enable": "Ativar",
"Upgrade to Readest Premium": "Atualizar para Readest Premium",
"Show Source Text": "Mostrar Texto Fonte",
"Cross-Platform Sync": "Sincronização Multiplataforma",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Sincronize perfeitamente sua biblioteca, progresso, destaques e notas em todos os seus dispositivos—nunca mais perca seu lugar.",
"Customizable Reading": "Leitura Personalizável",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Personalize cada detalhe com fontes ajustáveis, layouts, temas e configurações avançadas de exibição para a experiência de leitura perfeita.",
"AI Read Aloud": "Leitura em Voz Alta por IA",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Desfrute da leitura sem usar as mãos com vozes de IA com som natural que dão vida aos seus livros.",
"AI Translations": "Traduções por IA",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Traduza qualquer texto instantaneamente com o poder do Google, Azure ou DeepL—entenda conteúdo em qualquer idioma.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Conecte-se com outros leitores e obtenha ajuda rapidamente em nossos canais comunitários amigáveis.",
"Unlimited AI Read Aloud Hours": "Horas Ilimitadas de Leitura por IA",
"Listen without limits—convert as much text as you like into immersive audio.": "Ouça sem limites—converta tanto texto quanto quiser em áudio envolvente.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Desbloqueie recursos aprimorados de tradução com mais uso diário e opções avançadas.",
"DeepL Pro Access": "Acesso ao DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Desfrute de respostas mais rápidas e assistência dedicada sempre que precisar de ajuda.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Cota diária de tradução atingida. Atualize seu plano para continuar usando traduções por IA.",
"Includes All Plus Plan Benefits": "Inclui Todos os Benefícios do Plano Plus",
"Early Feature Access": "Acesso Antecipado a Recursos",
"Be the first to explore new features, updates, and innovations before anyone else.": "Explore primeiro novos recursos, atualizações e inovações antes de qualquer um.",
"Advanced AI Tools": "Ferramentas de IA Avançadas",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Aproveite ferramentas de IA poderosas para leitura inteligente, tradução e descoberta de conteúdo.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Traduza até 100.000 caracteres diariamente com o motor de tradução mais preciso disponível.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Traduza até 500.000 caracteres diariamente com o motor de tradução mais preciso disponível.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Armazene e acesse com segurança sua coleção completa de leitura com até 5 GB de armazenamento na nuvem.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Armazene e acesse com segurança sua coleção completa de leitura com até 20 GB de armazenamento na nuvem.",
"Deleted cloud backup of the book: {{title}}": "Backup na nuvem do livro excluído: {{title}}",
"Failed to delete cloud backup of the book": "Falha ao excluir o backup na nuvem do livro.",
"Are you sure to delete the cloud backup of the selected book?": "Tem certeza de que deseja excluir o backup na nuvem do livro selecionado?",
"What's New in Readest": "Novidades no Readest",
"Enter book title": "Digite o título do livro",
"Subtitle": "Subtítulo",
"Enter book subtitle": "Digite o subtítulo do livro",
"Enter author name": "Digite o nome do autor",
"Series": "Série",
"Enter series name": "Digite o nome da série",
"Series Index": "Número da série",
"Enter series index": "Digite o número da série",
"Total in Series": "Total na série",
"Enter total books in series": "Digite o total de livros na série",
"Enter publisher": "Digite a editora",
"Publication Date": "Data de publicação",
"Identifier": "Identificador",
"Enter book description": "Digite a descrição do livro",
"Change cover image": "Alterar imagem da capa",
"Replace": "Substituir",
"Unlock cover": "Desbloquear capa",
"Lock cover": "Bloquear capa",
"Auto-Retrieve Metadata": "Recuperar metadados automaticamente",
"Auto-Retrieve": "Recuperação automática",
"Unlock all fields": "Desbloquear todos os campos",
"Unlock All": "Desbloquear tudo",
"Lock all fields": "Bloquear todos os campos",
"Lock All": "Bloquear tudo",
"Reset": "Redefinir",
"Edit Metadata": "Editar metadados",
"Locked": "Bloqueado",
"Select Metadata Source": "Selecionar fonte de metadados",
"Keep manual input": "Manter entrada manual",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Ficção, Ciência, História",
"Select Cover Image": "Selecionar imagem da capa",
"Open Book in New Window": "Abrir livro em nova janela",
"Voices for {{lang}}": "Vozes para {{lang}}",
"Yandex Translate": "Yandex Translate",
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD",
"Restore Purchase": "Restaurar Compra",
"No purchases found to restore.": "Nenhuma compra encontrada para restaurar.",
"Failed to restore purchases.": "Falha ao restaurar compras.",
"Failed to manage subscription.": "Falha ao gerenciar assinatura.",
"Failed to load subscription plans.": "Falha ao carregar planos de assinatura.",
"year": "ano",
"Failed to create checkout session": "Falha ao criar sessão de checkout",
"Storage": "Armazenamento",
"Terms of Service": "Termos de Serviço",
"Privacy Policy": "Política de Privacidade",
"Disable Double Click": "Desativar Clique Duplo"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Подтвердить удаление",
"Copied to notebook": "Скопировано в блокнот",
"Copy": "Копировать",
"Custom CSS": "Пользовательский CSS",
"Dark Mode": "Тёмная тема",
"Default": "По умолчанию",
"Default Font": "Шрифт по умолчанию",
@@ -28,7 +27,6 @@
"Dictionary": "Словарь",
"Download Readest": "Скачать Readest",
"Edit": "Редактировать",
"Enter your custom CSS here...": "Введите ваш пользовательский CSS здесь...",
"Excerpts": "Отрывки",
"Failed to import book(s): {{filenames}}": "Не удалось импортировать книгу(и): {{filenames}}",
"Fast": "Быстро",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Горизонтальное направление",
"Hyphenation": "Перенос слов",
"Import Books": "Импорт книг",
"Language:": "Язык:",
"Layout": "Макет",
"Light Mode": "Светлая тема",
"Loading...": "Загрузка...",
@@ -70,8 +67,8 @@
"Paging Animation": "Анимация перелистывания",
"Paragraph": "Абзац",
"Parallel Read": "Параллельное чтение",
"Published:": "Опубликовано:",
"Publisher:": "Издатель:",
"Published": "Опубликовано",
"Publisher": "Издатель",
"Reading Progress Synced": "Синхронизирован прогресс чтения",
"Reload Page": "Перезагрузить страницу",
"Reveal in File Explorer": "Показать в проводнике",
@@ -96,7 +93,7 @@
"Slow": "Медленно",
"Solarized": "Солнечный",
"Speak": "Произнести",
"Subjects:": "Темы:",
"Subjects": "Темы",
"System Fonts": "Системные шрифты",
"TTS not supported in this device": "TTS не поддерживается на этом устройстве",
"Theme Color": "Цвет темы",
@@ -105,7 +102,7 @@
"Translated Text": "Переведённый текст",
"Unknown": "Неизвестно",
"Untitled": "Без названия",
"Updated:": "Обновлено:",
"Updated": "Обновлено",
"Version {{version}}": "Версия {{version}}",
"Vertical Direction": "Вертикальное направление",
"Welcome to your library. You can import your books here and read them anytime.": "Добро пожаловать в вашу библиотеку. Вы можете импортировать сюда книги и читать их в любое время.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Не удалось скачать книгу: {{title}}",
"Upload Book": "Загрузить книгу",
"Auto Upload Books to Cloud": "Автоматическая загрузка книг в облако",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% использовано облачного хранилища.",
"Storage": "Хранилище",
"Book deleted: {{title}}": "Книга удалена: {{title}}",
"Failed to delete book: {{title}}": "Не удалось удалить книгу: {{title}}",
"Check Updates on Start": "Проверять обновления при запуске",
@@ -161,7 +156,7 @@
"Create a Password": "Создать пароль",
"Sign up": "Зарегистрироваться",
"Signing up...": "Регистрация...",
"Dont have an account? Sign up": "Нет аккаунта? Зарегистрируйтесь",
"Don't have an account? Sign up": "Нет аккаунта? Зарегистрируйтесь",
"Check your email for the confirmation link": "Проверьте свою почту для подтверждающей ссылки",
"Signing in ...": "Вход ...",
"Send a magic link email": "Отправить магическую ссылку по электронной почте",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "Войти через GitHub",
"Account": "Аккаунт",
"Failed to delete user. Please try again later.": "Не удалось удалить пользователя. Пожалуйста, повторите попытку позже.",
"Unlimited Offline/Online Reading": "Безлимитное чтение офлайн/онлайн",
"Unlimited Cloud Sync Devices": "Неограниченное количество устройств для облачной синхронизации",
"Essential Text-to-Speech Voices": "Основные голоса для преобразования текста в речь",
"DeepL Free Access": "Бесплатный доступ к DeepL",
"Community Support": "Поддержка сообщества",
"500 MB Cloud Sync Space": "500 МБ облачного пространства для синхронизации",
"Includes All Free Tier Benefits": "Включает все преимущества бесплатного тарифа",
"AI Summaries": "AI-резюме",
"AI Translations": "AI-переводы",
"Priority Support": "Приоритетная поддержка",
"DeepL Pro Access": "Доступ к DeepL Pro",
"Expanded Text-to-Speech Voices": "Расширенный набор голосов для преобразования текста в речь",
"2000 MB Cloud Sync Space": "2000 МБ облачного пространства для синхронизации",
"Includes All Plus Tier Benefits": "Включает все преимущества тарифа Plus",
"Unlimited AI Summaries": "Безлимитные AI-резюме",
"Unlimited AI Translations": "Безлимитные AI-переводы",
"Unlimited DeepL Pro Access": "Неограниченный доступ к DeepL Pro",
"Advanced AI Tools": "Продвинутые AI-инструменты",
"Early Feature Access": "Ранний доступ к новым функциям",
"10 GB Cloud Sync Space": "10 ГБ облачного пространства для синхронизации",
"Loading profile...": "Загрузка профиля...",
"Features": "Функции",
"Delete Account": "Удалить аккаунт",
"Delete Your Account?": "Удалить ваш аккаунт?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Это действие нельзя отменить. Все ваши данные в облаке будут удалены безвозвратно.",
"Delete Permanently": "Удалить навсегда",
"Free Tier": "Бесплатный тариф",
"Plus Tier": "Тариф Plus",
"Pro Tier": "Тариф Pro",
"RTL Direction": "Направление RTL",
"Maximum Column Height": "Максимальная высота колонки",
"Maximum Column Width": "Максимальная ширина колонки",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Скачано {{downloaded}} из {{contentLength}}",
"Download finished": "Загрузка завершена",
"DOWNLOAD & INSTALL": "СКАЧАТЬ И УСТАНОВИТЬ",
"Changelog:": "Изменения:",
"Changelog": "Изменения",
"Software Update": "Обновление программного обеспечения",
"Title": "Название",
"Date Read": "Дата прочтения",
@@ -279,10 +252,9 @@
"Ascending": "По возрастанию",
"Descending": "По убыванию",
"Sort by...": "Сортировать по...",
"Added:": "Добавлено:",
"Format:": "Формат:",
"Added": "Добавлено",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Описание:",
"Description": "Описание",
"No description available": "Нет доступного описания",
"List": "Список",
"Grid": "Сетка",
@@ -299,7 +271,7 @@
"Checking for updates...": "Проверка обновлений...",
"Error checking for updates": "Ошибка при проверке обновлений",
"Details": "Инфо",
"File Size:": "Размер файла:",
"File Size": "Размер файла",
"Auto Detect": "Автоопределение",
"Next Section": "Следующий раздел",
"Previous Section": "Предыдущий раздел",
@@ -351,7 +323,6 @@
"Quota Exceeded": "Лимит превышен",
"{{percentage}}% of Daily Translation Characters Used.": "Использовано {{percentage}}% от суточного лимита символов перевода.",
"Translation Characters": "Символы перевода",
"Daily translation quota reached. Select another translate service to proceed.": "Достигнут суточный лимит символов перевода. Выберите другой сервис перевода для продолжения.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} голос",
"{{engine}}: {{count}} voices_few": "{{engine}}: {{count}} голоса",
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} голосов",
@@ -369,5 +340,134 @@
"Right Margin (px)": "Правый отступ (пикс)",
"Left Margin (px)": "Левый отступ (пикс)",
"Column Gap (%)": "Промежуток между колонками (%)",
"Always Show Status Bar": "Всегда показывать панель состояния"
"Always Show Status Bar": "Всегда показывать панель состояния",
"Translation Not Available": "Перевод недоступен",
"Custom Content CSS": "CSS содержимого",
"Enter CSS for book content styling...": "Введите CSS для оформления содержимого книги...",
"Custom Reader UI CSS": "CSS интерфейса",
"Enter CSS for reader interface styling...": "Введите CSS для оформления интерфейса ридера...",
"Crop": "Обрезать",
"Book Covers": "Обложки книг",
"Fit": "Подогнать",
"Reset {{settings}}": "Сбросить {{settings}}",
"Reset Settings": "Сбросить настройки",
"{{count}} pages left in chapter_one": "Осталась {{count}} страница в главе",
"{{count}} pages left in chapter_few": "Осталось {{count}} страницы в главе",
"{{count}} pages left in chapter_many": "Осталось {{count}} страниц в главе",
"{{count}} pages left in chapter_other": "Осталось {{count}} страниц в главе",
"Show Remaining Pages": "Показать оставшиеся страницы",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Управление подпиской",
"Coming Soon": "Скоро будет",
"Upgrade to {{plan}}": "Обновить до {{plan}}",
"Upgrade to Plus or Pro": "Обновить до Plus или Pro",
"Current Plan": "Текущий план",
"Plan Limits": "Лимиты плана",
"Processing your payment...": "Обработка платежа...",
"Please wait while we confirm your subscription.": "Пожалуйста, подождите, пока мы подтверждаем вашу подписку.",
"Payment Processing": "Обработка платежа",
"Your payment is being processed. This usually takes a few moments.": "Ваш платеж обрабатывается. Обычно это занимает несколько секунд.",
"Payment Failed": "Не удалось провести платеж",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Не удалось обработать подписку. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой, если проблема сохраняется.",
"Back to Profile": "Назад в профиль",
"Subscription Successful!": "Подписка успешно оформлена!",
"Thank you for your subscription! Your payment has been processed successfully.": "Спасибо за подписку! Ваш платеж успешно обработан.",
"Email:": "Электронная почта:",
"Plan:": "План:",
"Amount:": "Сумма:",
"Subscription ID:": "ID подписки:",
"Go to Library": "Перейти в библиотеку",
"Need help? Contact our support team at support@readest.com": "Нужна помощь? Свяжитесь с нашей поддержкой: support@readest.com",
"Free Plan": "Бесплатный план",
"month": "месяц",
"AI Translations (per day)": "Переводы ИИ (в день)",
"Plus Plan": "План Plus",
"Includes All Free Plan Benefits": "Включает все преимущества бесплатного плана",
"Pro Plan": "План Pro",
"More AI Translations": "Больше переводов ИИ",
"Complete Your Subscription": "Завершите подписку",
"{{percentage}}% of Cloud Sync Space Used.": "Использовано {{percentage}}% облачного пространства для синхронизации.",
"Cloud Sync Storage": "Облачное хранилище",
"Disable": "Отключить",
"Enable": "Включить",
"Upgrade to Readest Premium": "Обновить до Readest Premium",
"Show Source Text": "Показать исходный текст",
"Cross-Platform Sync": "Кроссплатформенная Синхронизация",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Беспрепятственно синхронизируйте свою библиотеку, прогресс, заметки и выделения на всех устройствах—больше никогда не теряйте свое место.",
"Customizable Reading": "Настраиваемое Чтение",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Персонализируйте каждую деталь с настраиваемыми шрифтами, макетами, темами и расширенными настройками отображения для идеального чтения.",
"AI Read Aloud": "Озвучивание ИИ",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Наслаждайтесь чтением без рук с естественно звучащими голосами ИИ, которые оживляют ваши книги.",
"AI Translations": "Переводы ИИ",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Переводите любой текст мгновенно с помощью Google, Azure или DeepL—понимайте содержание на любом языке.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Общайтесь с другими читателями и получайте быструю помощь в наших дружелюбных каналах сообщества.",
"Unlimited AI Read Aloud Hours": "Неограниченные Часы Озвучивания ИИ",
"Listen without limits—convert as much text as you like into immersive audio.": "Слушайте без ограничений—преобразуйте столько текста, сколько хотите, в захватывающее аудио.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Разблокируйте улучшенные возможности перевода с большим ежедневным использованием и расширенными опциями.",
"DeepL Pro Access": "Доступ к DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Наслаждайтесь более быстрыми ответами и персональной помощью, когда вам нужна поддержка.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Достигнута дневная квота переводов. Обновите план, чтобы продолжить использовать переводы ИИ.",
"Includes All Plus Plan Benefits": "Включает Все Преимущества Plus Плана",
"Early Feature Access": "Ранний Доступ к Функциям",
"Be the first to explore new features, updates, and innovations before anyone else.": "Исследуйте новые функции, обновления и инновации первыми.",
"Advanced AI Tools": "Продвинутые ИИ Инструменты",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Используйте мощные ИИ инструменты для умного чтения, перевода и поиска контента.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Переводите до 100 000 символов ежедневно с самым точным доступным движком перевода.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Переводите до 500 000 символов ежедневно с самым точным доступным движком перевода.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Безопасно храните и получайте доступ ко всей коллекции чтения с до 5 ГБ облачного хранилища.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Безопасно храните и получайте доступ ко всей коллекции чтения с до 20 ГБ облачного хранилища.",
"Deleted cloud backup of the book: {{title}}": "Удалено облачное резервное копирование книги: {{title}}",
"Failed to delete cloud backup of the book": "Не удалось удалить облачное резервное копирование книги.",
"Are you sure to delete the cloud backup of the selected book?": "Вы уверены, что хотите удалить облачное резервное копирование выбранной книги?",
"What's New in Readest": "Что нового в Readest",
"Enter book title": "Введите название книги",
"Subtitle": "Подзаголовок",
"Enter book subtitle": "Введите подзаголовок книги",
"Enter author name": "Введите имя автора",
"Series": "Серия",
"Enter series name": "Введите название серии",
"Series Index": "Номер в серии",
"Enter series index": "Введите номер в серии",
"Total in Series": "Всего в серии",
"Enter total books in series": "Введите общее количество книг в серии",
"Enter publisher": "Введите издательство",
"Publication Date": "Дата публикации",
"Identifier": "Идентификатор",
"Enter book description": "Введите описание книги",
"Change cover image": "Изменить обложку",
"Replace": "Заменить",
"Unlock cover": "Разблокировать обложку",
"Lock cover": "Заблокировать обложку",
"Auto-Retrieve Metadata": "Автоматически получить метаданные",
"Auto-Retrieve": "Автоматическое получение",
"Unlock all fields": "Разблокировать все поля",
"Unlock All": "Разблокировать всё",
"Lock all fields": "Заблокировать все поля",
"Lock All": "Заблокировать всё",
"Reset": "Сбросить",
"Edit Metadata": "Редактировать метаданные",
"Locked": "Заблокировано",
"Select Metadata Source": "Выберите источник метаданных",
"Keep manual input": "Сохранить ручной ввод",
"Google Books": "Google Книги",
"Open Library": "Open Library",
"Fiction, Science, History": "Художественная литература, Наука, История",
"Select Cover Image": "Выбрать изображение обложки",
"Open Book in New Window": "Открыть книгу в новом окне",
"Voices for {{lang}}": "Голоса для {{lang}}",
"Yandex Translate": "Yandex Translate",
"YYYY or YYYY-MM-DD": "YYYY или YYYY-MM-DD",
"Restore Purchase": "Восстановить покупку",
"No purchases found to restore.": "Не найдено покупок для восстановления.",
"Failed to restore purchases.": "Не удалось восстановить покупки.",
"Failed to manage subscription.": "Не удалось управлять подпиской.",
"Failed to load subscription plans.": "Не удалось загрузить планы подписки.",
"year": "год",
"Failed to create checkout session": "Не удалось создать сессию оформления заказа",
"Storage": "Хранилище",
"Terms of Service": "Условия использования",
"Privacy Policy": "Политика конфиденциальности",
"Disable Double Click": "Отключить двойной клик"
}
@@ -0,0 +1,461 @@
{
"Email address": "อีเมล",
"Your Password": "รหัสผ่าน",
"Your email address": "อีเมลของคุณ",
"Your password": "รหัสผ่านของคุณ",
"Sign in": "เข้าสู่ระบบ",
"Signing in...": "กำลังเข้าสู่ระบบ...",
"Sign in with {{provider}}": "เข้าสู่ระบบด้วย {{provider}}",
"Already have an account? Sign in": "มีบัญชีแล้ว? เข้าสู่ระบบ",
"Create a Password": "สร้างรหัสผ่าน",
"Sign up": "สมัครสมาชิก",
"Signing up...": "กำลังสมัครสมาชิก...",
"Check your email for the confirmation link": "ตรวจสอบอีเมลเพื่อยืนยัน",
"Signing in ...": "กำลังเข้าสู่ระบบ...",
"Send a magic link email": "ส่งลิงก์เข้าสู่ระบบ",
"Check your email for the magic link": "ตรวจสอบอีเมลเพื่อรับลิงก์",
"Send reset password instructions": "ส่งคำแนะนำรีเซ็ตรหัส",
"Sending reset instructions ...": "กำลังส่งคำแนะนำ...",
"Forgot your password?": "ลืมรหัสผ่าน?",
"Check your email for the password reset link": "ตรวจสอบอีเมลเพื่อรีเซ็ตรหัส",
"Phone number": "เบอร์โทรศัพท์",
"Your phone number": "เบอร์โทรศัพท์ของคุณ",
"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": "อัปเดตรหัสผ่าน",
"Updating password ...": "กำลังอัปเดตรหัสผ่าน...",
"Your password has been updated": "รหัสผ่านได้รับการอัปเดตแล้ว",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "เกิดข้อผิดพลาด ทีมงานได้รับแจ้งแล้วและกำลังแก้ไข",
"Error Details:": "รายละเอียดข้อผิดพลาด:",
"Try Again": "ลองใหม่",
"Go Back": "กลับ",
"Your Library": "คลังหนังสือ",
"Need help?": "ต้องการความช่วยเหลือ?",
"Contact Support": "ติดต่อฝ่ายสนับสนุน",
"Open": "เปิด",
"Group": "กลุ่ม",
"Details": "รายละเอียด",
"Delete": "ลบ",
"Cancel": "ยกเลิก",
"Confirm Deletion": "ยืนยันการลบ",
"Are you sure to delete {{count}} selected book(s)?_other": "ยืนยันลบหนังสือ {{count}} เล่ม?",
"Deselect Book": "ยกเลิกเลือกหนังสือ",
"Select Book": "เลือกหนังสือ",
"Show Book Details": "แสดงรายละเอียดหนังสือ",
"Download Book": "ดาวน์โหลดหนังสือ",
"Upload Book": "อัปโหลดหนังสือ",
"Deselect Group": "ยกเลิกเลือกกลุ่ม",
"Select Group": "เลือกกลุ่ม",
"Untitled Group": "กลุ่มไม่มีชื่อ",
"Group Books": "จัดกลุ่มหนังสือ",
"Remove From Group": "ลบออกจากกลุ่ม",
"Create New Group": "สร้างกลุ่มใหม่",
"Save": "บันทึก",
"Confirm": "ยืนยัน",
"From Local File": "จากไฟล์ในเครื่อง",
"Search in {{count}} Book(s)..._other": "ค้นหาใน {{count}} เล่ม...",
"Search Books...": "ค้นหาหนังสือ...",
"Clear Search": "ล้างการค้นหา",
"Import Books": "นำเข้าหนังสือ",
"Select Multiple Books": "เลือกหลายเล่ม",
"Select Books": "เลือกหนังสือ",
"Deselect": "ยกเลิกเลือก",
"Select All": "เลือกทั้งหมด",
"Logged in as {{userDisplayName}}": "เข้าสู่ระบบในชื่อ {{userDisplayName}}",
"Logged in": "เข้าสู่ระบบแล้ว",
"Account": "บัญชี",
"Sign In": "เข้าสู่ระบบ",
"Auto Upload Books to Cloud": "อัปโหลดหนังสืออัตโนมัติ",
"Auto Import on File Open": "นำเข้าอัตโนมัติเมื่อเปิดไฟล์",
"Open Last Book on Start": "เปิดหนังสือล่าสุดเมื่อเริ่มใช้",
"Check Updates on Start": "ตรวจสอบอัปเดตเมื่อเริ่มใช้",
"Fullscreen": "เต็มจอ",
"Always on Top": "แสดงด้านบนเสมอ",
"Always Show Status Bar": "แสดงแถบสถานะเสมอ",
"Keep Screen Awake": "เปิดหน้าจอค้าง",
"Reload Page": "โหลดหน้าใหม่",
"Dark Mode": "โหมดมืด",
"Light Mode": "โหมดสว่าง",
"Auto Mode": "โหมดอัตโนมัติ",
"Upgrade to Readest Premium": "อัปเกรดเป็น Readest Premium",
"Download Readest": "ดาวน์โหลด Readest",
"About Readest": "เกี่ยวกับ Readest",
"Help improve Readest": "ช่วยพัฒนา Readest",
"Sharing anonymized statistics": "แชร์สถิติแบบไม่ระบุตัวตน",
"List": "รายการ",
"Grid": "ตาราง",
"Crop": "ครอบตัด",
"Fit": "ปรับพอดี",
"Title": "ชื่อเรื่อง",
"Author": "ผู้แต่ง",
"Format": "รูปแบบ",
"Date Read": "วันที่อ่าน",
"Date Added": "วันที่เพิ่ม",
"Ascending": "น้อยไปมาก",
"Descending": "มากไปน้อย",
"Book Covers": "ปกหนังสือ",
"Sort by...": "เรียงตาม...",
"No supported files found. Supported formats: {{formats}}": "ไม่พบไฟล์ที่รองรับ รูปแบบที่รองรับ: {{formats}}",
"No chapters detected.": "ไม่พบบท",
"Failed to parse the EPUB file.": "ไม่สามารถอ่านไฟล์ EPUB",
"This book format is not supported.": "ไม่รองรับรูปแบบหนังสือนี้",
"Failed to import book(s): {{filenames}}": "ไม่สามารถนำเข้าหนังสือ: {{filenames}}",
"Book uploaded: {{title}}": "อัปโหลดหนังสือแล้ว: {{title}}",
"Insufficient storage quota": "พื้นที่จัดเก็บไม่เพียงพอ",
"Failed to upload book: {{title}}": "ไม่สามารถอัปโหลดหนังสือ: {{title}}",
"Book downloaded: {{title}}": "ดาวน์โหลดหนังสือแล้ว: {{title}}",
"Failed to download book: {{title}}": "ไม่สามารถดาวน์โหลดหนังสือ: {{title}}",
"Book deleted: {{title}}": "ลบหนังสือแล้ว: {{title}}",
"Failed to delete book: {{title}}": "ไม่สามารถลบหนังสือ: {{title}}",
"Deleted cloud backup of the book: {{title}}": "ลบสำรองบนคลาวด์แล้ว: {{title}}",
"Failed to delete cloud backup of the book": "ไม่สามารถลบสำรองบนคลาวด์",
"Welcome to your library. You can import your books here and read them anytime.": "ยินดีต้อนรับสู่คลังหนังสือ คุณสามารถนำเข้าหนังสือและอ่านได้ตลอดเวลา",
"Copied to notebook": "คัดลอกไปยังสมุดบันทึก",
"No annotations to export": "ไม่มีบันทึกให้ส่งออก",
"Exported from Readest": "ส่งออกจาก Readest",
"Highlights & Annotations": "ไฮไลต์และบันทึก",
"Untitled": "ไม่มีชื่อ",
"Note": "บันทึก",
"Copied to clipboard": "คัดลอกแล้ว",
"Copy": "คัดลอก",
"Delete Highlight": "ลบไฮไลต์",
"Highlight": "ไฮไลต์",
"Annotate": "บันทึก",
"Search": "ค้นหา",
"Dictionary": "พจนานุกรม",
"Wikipedia": "วิกิพีเดีย",
"Translate": "แปล",
"Speak": "อ่านออกเสียง",
"Login Required": "ต้องเข้าสู่ระบบ",
"Quota Exceeded": "เกินโควตา",
"Unable to fetch the translation. Please log in first and try again.": "ไม่สามารถแปลได้ กรุณาเข้าสู่ระบบก่อน",
"Unable to fetch the translation. Try again later.": "ไม่สามารถแปลได้ ลองใหม่ภายหลัง",
"Original Text": "ข้อความต้นฉบับ",
"Auto Detect": "ตรวจสอบอัตโนมัติ",
"(detected)": "(ตรวจพบ)",
"Translated Text": "ข้อความแปล",
"Loading...": "กำลังโหลด...",
"No translation available.": "ไม่มีการแปล",
"Translated by {{provider}}.": "แปลโดย {{provider}}",
"Bookmark": "บุ๊กมาร์ก",
"Next Section": "ส่วนถัดไป",
"Previous Section": "ส่วนก่อนหน้า",
"Next Page": "หน้าถัดไป",
"Previous Page": "หน้าก่อนหน้า",
"Go Forward": "ไปข้างหน้า",
"Small": "เล็ก",
"Large": "ใหญ่",
"Notebook": "สมุดบันทึก",
"No notes match your search": "ไม่พบบันทึกที่ตรงกับการค้นหา",
"Excerpts": "ข้อความที่เลือก",
"Notes": "บันทึก",
"Add your notes here...": "เพิ่มบันทึกที่นี่...",
"Search notes and excerpts...": "ค้นหาบันทึกและข้อความ...",
"{{time}} min left in chapter": "เหลือ {{time}} นาทีในบท",
"{{count}} pages left in chapter_other": "เหลือ {{count}} หน้าในบท",
"Theme Mode": "โหมดธีม",
"Invert Image In Dark Mode": "กลับสีรูปในโหมดมืด",
"Override Book Color": "เขียนทับสีหนังสือ",
"Theme Color": "สีธีม",
"Custom": "กำหนดเอง",
"Code Highlighting": "ไฮไลต์โค้ด",
"Enable Highlighting": "เปิดไฮไลต์",
"Code Language": "ภาษาโค้ด",
"Scroll": "เลื่อน",
"Scrolled Mode": "โหมดเลื่อน",
"Continuous Scroll": "เลื่อนต่อเนื่อง",
"Overlap Pixels": "พิกเซลซ้อน",
"Click": "คลิก",
"Clicks for Page Flip": "คลิกเพื่อพลิกหน้า",
"Swap Clicks Area": "สลับพื้นที่คลิก",
"Volume Keys for Page Flip": "ปุ่มเสียงเพื่อพลิกหน้า",
"Animation": "ภาพเคลื่อนไหว",
"Paging Animation": "ภาพเคลื่อนไหวพลิกหน้า",
"Security": "ความปลอดภัย",
"Allow JavaScript": "อนุญาต JavaScript",
"Enable only if you trust the file.": "เปิดเฉพาะไฟล์ที่เชื่อถือได้",
"Global Settings": "การตั้งค่าทั่วไป",
"Apply to All Books": "ใช้กับหนังสือทั้งหมด",
"Apply to This Book": "ใช้กับหนังสือนี้",
"Reset Settings": "รีเซ็ตการตั้งค่า",
"System Fonts": "ฟอนต์ระบบ",
"Serif Font": "ฟอนต์ Serif",
"Sans-Serif Font": "ฟอนต์ Sans-Serif",
"Override Book Font": "เขียนทับฟอนต์หนังสือ",
"Font Size": "ขนาดฟอนต์",
"Default Font Size": "ขนาดฟอนต์เริ่มต้น",
"Minimum Font Size": "ขนาดฟอนต์ต่ำสุด",
"Font Weight": "น้ำหนักฟอนต์",
"Font Family": "ตระกูลฟอนต์",
"Default Font": "ฟอนต์เริ่มต้น",
"CJK Font": "ฟอนต์ CJK",
"Font Face": "หน้าฟอนต์",
"Monospace Font": "ฟอนต์ Monospace",
"Auto": "อัตโนมัติ",
"System Language": "ภาษาระบบ",
"Language": "ภาษา",
"Interface Language": "ภาษาส่วนติดต่อ",
"Translation": "การแปล",
"Enable Translation": "เปิดการแปล",
"Show Source Text": "แสดงข้อความต้นฉบับ",
"Translation Service": "บริการแปล",
"Translate To": "แปลเป็น",
"Override Book Layout": "เขียนทับเลย์เอาต์หนังสือ",
"Writing Mode": "โหมดการเขียน",
"Default": "เริ่มต้น",
"Horizontal Direction": "ทิศทางแนวนอน",
"Vertical Direction": "ทิศทางแนวตั้ง",
"RTL Direction": "ทิศทาง RTL",
"Border Frame": "กรอบขอบ",
"Double Border": "ขอบคู่",
"Border Color": "สีขอบ",
"Paragraph": "ย่อหน้า",
"Paragraph Margin": "ระยะขอบย่อหน้า",
"Line Spacing": "ระยะห่างบรรทัด",
"Word Spacing": "ระยะห่างคำ",
"Letter Spacing": "ระยะห่างตัวอักษร",
"Text Indent": "การเยื้องข้อความ",
"Full Justification": "จัดแนวเต็มบรรทัด",
"Hyphenation": "การใส่เครื่องหมายยัติภังค์",
"Page": "หน้า",
"Top Margin (px)": "ขอบบน (px)",
"Bottom Margin (px)": "ขอบล่าง (px)",
"Left Margin (px)": "ขอบซ้าย (px)",
"Right Margin (px)": "ขอบขวา (px)",
"Column Gap (%)": "ช่องว่างคอลัมน์ (%)",
"Maximum Number of Columns": "จำนวนคอลัมน์สูงสุด",
"Maximum Column Height": "ความสูงคอลัมน์สูงสุด",
"Maximum Column Width": "ความกว้างคอลัมน์สูงสุด",
"Header & Footer": "หัวกระดาษและท้ายกระดาษ",
"Show Header": "แสดงหัวกระดาษ",
"Show Footer": "แสดงท้ายกระดาษ",
"Show Remaining Time": "แสดงเวลาที่เหลือ",
"Show Remaining Pages": "แสดงหน้าที่เหลือ",
"Show Page Number": "แสดงเลขหน้า",
"Apply also in Scrolled Mode": "ใช้ในโหมดเลื่อนด้วย",
"Screen": "หน้าจอ",
"Orientation": "การวางแนว",
"Portrait": "แนวตั้ง",
"Landscape": "แนวนอน",
"Apply": "ใช้",
"Custom Content CSS": "CSS เนื้อหากำหนดเอง",
"Enter CSS for book content styling...": "ป้อน CSS สำหรับจัดรูปแบบเนื้อหา...",
"Custom Reader UI CSS": "CSS ส่วนติดต่อผู้ใช้",
"Enter CSS for reader interface styling...": "ป้อน CSS สำหรับจัดรูปแบบส่วนติดต่อ...",
"Font": "ฟอนต์",
"Layout": "เลย์เอาต์",
"Color": "สี",
"Behavior": "พฤติกรรม",
"Reset {{settings}}": "รีเซ็ต {{settings}}",
"(from 'As You Like It', Act II)": "(จาก 'As You Like It', องก์ที่ 2)",
"Custom Theme": "ธีมกำหนดเอง",
"Theme Name": "ชื่อธีม",
"Text Color": "สีข้อความ",
"Background Color": "สีพื้นหลัง",
"Link Color": "สีลิงก์",
"Preview": "ดูตัวอย่าง",
"Font & Layout": "ฟอนต์และเลย์เอาต์",
"Book Cover": "ปกหนังสือ",
"More Info": "ข้อมูลเพิ่มเติม",
"Parallel Read": "อ่านคู่ขนาน",
"Disable": "ปิด",
"Enable": "เปิด",
"Export Annotations": "ส่งออกบันทึก",
"Sort TOC by Page": "เรียงสารบัญตามหน้า",
"Edit": "แก้ไข",
"Search...": "ค้นหา...",
"Book": "หนังสือ",
"Chapter": "บท",
"Match Case": "ตรงตามตัวอักษร",
"Match Whole Words": "ตรงทั้งคำ",
"Match Diacritics": "ตรงตามเครื่องหมายวรรณยุกต์",
"TOC": "สารบัญ",
"Sidebar": "แถบข้าง",
"Disable Translation": "ปิดการแปล",
"Translation Not Available": "ไม่มีการแปล",
"TTS not supported for PDF": "ไม่รองรับ TTS สำหรับ PDF",
"TTS not supported in this device": "อุปกรณ์นี้ไม่รองรับ TTS",
"No Timeout": "ไม่จำกัดเวลา",
"{{value}} minute": "{{value}} นาที",
"{{value}} minutes": "{{value}} นาที",
"{{value}} hour": "{{value}} ชั่วโมง",
"{{value}} hours": "{{value}} ชั่วโมง",
"Slow": "ช้า",
"Fast": "เร็ว",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} เสียง",
"Sign in to Sync": "เข้าสู่ระบบเพื่อซิงค์",
"Synced at {{time}}": "ซิงค์เมื่อ {{time}}",
"Never synced": "ไม่เคยซิงค์",
"Reading Progress Synced": "ซิงค์ความคืบหน้าแล้ว",
"Delete Your Account?": "ลบบัญชีของคุณ?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "การกระทำนี้ไม่สามารถยกเลิกได้ ข้อมูลทั้งหมดบนคลาวด์จะถูกลบถาวร",
"Delete Permanently": "ลบถาวร",
"Manage Subscription": "จัดการการสมัครสมาชิก",
"Sign Out": "ออกจากระบบ",
"Delete Account": "ลบบัญชี",
"Upgrade to {{plan}}": "อัปเกรดเป็น {{plan}}",
"Complete Your Subscription": "ดำเนินการสมัครสมาชิกให้เสร็จ",
"Coming Soon": "เร็วๆ นี้",
"Upgrade to Plus or Pro": "อัปเกรดเป็น Plus หรือ Pro",
"Current Plan": "แผนปัจจุบัน",
"Plan Limits": "ข้อจำกัดแผน",
"Failed to delete user. Please try again later.": "ไม่สามารถลบผู้ใช้ ลองใหม่ภายหลัง",
"Loading profile...": "กำลังโหลดโปรไฟล์...",
"Processing your payment...": "กำลังประมวลผลการชำระเงิน...",
"Please wait while we confirm your subscription.": "กรุณารอสักครู่เพื่อยืนยันการสมัครสมาชิก",
"Payment Processing": "กำลังประมวลผลการชำระเงิน",
"Your payment is being processed. This usually takes a few moments.": "กำลังประมวลผลการชำระเงิน โดยปกติใช้เวลาสักครู่",
"Payment Failed": "การชำระเงินล้มเหลว",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "ไม่สามารถประมวลผลการสมัครสมาชิก ลองใหม่หรือติดต่อฝ่ายสนับสนุน",
"Back to Profile": "กลับไปที่โปรไฟล์",
"Subscription Successful!": "สมัครสมาชิกสำเร็จ!",
"Thank you for your subscription! Your payment has been processed successfully.": "ขอบคุณสำหรับการสมัครสมาชิก! ชำระเงินเรียบร้อยแล้ว",
"Email:": "อีเมล:",
"Plan:": "แผน:",
"Amount:": "จำนวนเงิน:",
"Subscription ID:": "รหัสสมาชิก:",
"Go to Library": "ไปที่คลังหนังสือ",
"Need help? Contact our support team at support@readest.com": "ต้องการความช่วยเหลือ? ติดต่อทีมสนับสนุนที่ support@readest.com",
"Free Plan": "แผนฟรี",
"month": "เดือน",
"Cross-Platform Sync": "ซิงค์ข้ามแพลตฟอร์ม",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "ซิงค์คลังหนังสือ ความคืบหน้า ไฮไลต์ และบันทึกข้ามอุปกรณ์—ไม่สูญหายอีกต่อไป",
"Customizable Reading": "การอ่านแบบปรับแต่งได้",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "ปรับแต่งทุกรายละเอียดด้วยฟอนต์ เลย์เอาต์ ธีม และการตั้งค่าขั้นสูงเพื่อประสบการณ์การอ่านที่สมบูรณ์แบบ",
"AI Read Aloud": "AI อ่านออกเสียง",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "เพลิดเพลินกับการอ่านแบบเสรีมือด้วยเสียง AI ที่เป็นธรรมชาติ",
"AI Translations": "การแปลด้วย AI",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "แปลข้อความได้ทันทีด้วยพลัง Google, Azure หรือ DeepL—เข้าใจเนื้อหาทุกภาษา",
"Community Support": "การสนับสนุนชุมชน",
"Connect with fellow readers and get help fast in our friendly community channels.": "เชื่อมต่อกับนักอ่านอื่นๆ และรับความช่วยเหลือในชุมชนที่เป็นมิตร",
"Cloud Sync Storage": "พื้นที่จัดเก็บซิงค์คลาวด์",
"AI Translations (per day)": "การแปล AI (ต่อวัน)",
"Plus Plan": "แผน Plus",
"Includes All Free Plan Benefits": "รวมสิทธิประโยชน์แผนฟรีทั้งหมด",
"Unlimited AI Read Aloud Hours": "AI อ่านออกเสียงไม่จำกัด",
"Listen without limits—convert as much text as you like into immersive audio.": "ฟังไม่จำกัด—แปลงข้อความเป็นเสียงได้ไม่อั้น",
"More AI Translations": "การแปล AI เพิ่มขึ้น",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "ปลดล็อกความสามารถการแปลที่เพิ่มขึ้นพร้อมโควตารายวันและตัวเลือกขั้นสูง",
"DeepL Pro Access": "การเข้าถึง DeepL Pro",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "แปลได้สูงสุด 100,000 อักขระต่อวันด้วยเครื่องมือแปลที่แม่นยำที่สุด",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "จัดเก็บและเข้าถึงคอลเลคชันการอ่านอย่างปลอดภัยด้วยพื้นที่คลาวด์ 5 GB",
"Priority Support": "การสนับสนุนแบบพิเศษ",
"Enjoy faster responses and dedicated assistance whenever you need help.": "รับการตอบสนองที่รวดเร็วและความช่วยเหลือเฉพาะเมื่อต้องการ",
"Pro Plan": "แผน Pro",
"Includes All Plus Plan Benefits": "รวมสิทธิประโยชน์แผน Plus ทั้งหมด",
"Early Feature Access": "เข้าถึงฟีเจอร์ใหม่ก่อนใคร",
"Be the first to explore new features, updates, and innovations before anyone else.": "เป็นคนแรกที่ได้สำรวจฟีเจอร์ใหม่ อัปเดต และนวัตกรรมก่อนใคร",
"Advanced AI Tools": "เครื่องมือ AI ขั้นสูง",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "ใช้เครื่องมือ AI อันทรงพลังเพื่อการอ่าน การแปล และการค้นหาเนื้อหาที่ฉลาดขึ้น",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "แปลได้สูงสุด 500,000 อักขระต่อวันด้วยเครื่องมือแปลที่แม่นยำที่สุด",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "จัดเก็บและเข้าถึงคอลเลคชันการอ่านอย่างปลอดภัยด้วยพื้นที่คลาวด์ 20 GB",
"Version {{version}}": "เวอร์ชัน {{version}}",
"Check Update": "ตรวจสอบอัปเดต",
"Already the latest version": "เป็นเวอร์ชันล่าสุดแล้ว",
"Checking for updates...": "กำลังตรวจสอบอัปเดต...",
"Error checking for updates": "เกิดข้อผิดพลาดในการตรวจสอบอัปเดต",
"Book Details": "รายละเอียดหนังสือ",
"Unknown": "ไม่ทราบ",
"Publisher": "สำนักพิมพ์",
"Published": "วันที่พิมพ์",
"Updated": "อัปเดต",
"Added": "เพิ่มเมื่อ",
"Subjects": "หัวข้อ",
"File Size": "ขนาดไฟล์",
"Description": "คำอธิบาย",
"No description available": "ไม่มีคำอธิบาย",
"Are you sure to delete the selected book?": "ยืนยันลบหนังสือที่เลือก?",
"Are you sure to delete the cloud backup of the selected book?": "ยืนยันลบสำรองคลาวด์ของหนังสือที่เลือก?",
"Drop to Import Books": "วางเพื่อนำเข้าหนังสือ",
"A new version of Readest is available!": "Readest เวอร์ชันใหม่พร้อมใช้งาน!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} พร้อมใช้งาน (เวอร์ชันที่ติดตั้ง {{currentVersion}})",
"Download and install now?": "ดาวน์โหลดและติดตั้งเลย?",
"Downloading {{downloaded}} of {{contentLength}}": "กำลังดาวน์โหลด {{downloaded}} จาก {{contentLength}}",
"Download finished": "ดาวน์โหลดเสร็จสิ้น",
"DOWNLOAD & INSTALL": "ดาวน์โหลดและติดตั้ง",
"Changelog": "รายการเปลี่ยนแปลง",
"Software Update": "อัปเดตซอฟต์แวร์",
"{{percentage}}% of Cloud Sync Space Used.": "ใช้พื้นที่ซิงค์คลาวด์ {{percentage}}%",
"Translation Characters": "อักขระการแปล",
"{{percentage}}% of Daily Translation Characters Used.": "ใช้อักขระการแปลรายวัน {{percentage}}%",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "ใช้โควตาการแปลรายวันครบแล้ว อัปเกรดแผนเพื่อใช้การแปล AI ต่อ",
"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",
"KingHwa_OldSong": "KingHwa_OldSong",
"Azure Translator": "Azure Translator",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Gray": "สีเทา",
"Sepia": "สีซีเปีย",
"Grass": "สีเขียว",
"Cherry": "สีชมพู",
"Sky": "สีฟ้า",
"Solarized": "Solarized",
"Gruvbox": "Gruvbox",
"Nord": "Nord",
"Contrast": "ความคมชัด",
"Sunset": "สีพระอาทิตย์ตก",
"Reveal in Finder": "แสดงใน Finder",
"Reveal in File Explorer": "แสดงใน File Explorer",
"Reveal in Folder": "แสดงในโฟลเดอร์",
"Don't have an account? Sign up": "ยังไม่มีบัญชี? สมัครสมาชิก",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "โลกทั้งใบเป็นเวที\nและชายหญิงทั้งปวงเป็นนักแสดง\nมีทางเข้าและทางออก\nแต่ละคนในชีวิตเล่นหลายบท\nการกระทำของเขาแบ่งเป็นเจ็ดวัย\n\n— วิลเลียม เชกสเปียร์",
"What's New in Readest": "มีอะไรใหม่ใน Readest",
"Enter book title": "ป้อนชื่อหนังสือ",
"Subtitle": "หัวข้อรอง",
"Enter book subtitle": "ป้อนหัวข้อรองของหนังสือ",
"Enter author name": "ป้อนชื่อผู้แต่ง",
"Series": "ชุด",
"Enter series name": "ป้อนชื่อชุด",
"Series Index": "เลขที่ในชุด",
"Enter series index": "ป้อนเลขที่ในชุด",
"Total in Series": "จำนวนรวมในชุด",
"Enter total books in series": "ป้อนจำนวนหนังสือทั้งหมดในชุด",
"Enter publisher": "ป้อนสำนักพิมพ์",
"Publication Date": "วันที่พิมพ์",
"Identifier": "ตัวระบุ",
"Enter book description": "ป้อนคำอธิบายหนังสือ",
"Change cover image": "เปลี่ยนรูปปก",
"Replace": "แทนที่",
"Unlock cover": "ปลดล็อคปก",
"Lock cover": "ล็อคปก",
"Auto-Retrieve Metadata": "ดึงข้อมูลเมตาอัตโนมัติ",
"Auto-Retrieve": "ดึงข้อมูลอัตโนมัติ",
"Unlock all fields": "ปลดล็อคทุกช่อง",
"Unlock All": "ปลดล็อคทั้งหมด",
"Lock all fields": "ล็อคทุกช่อง",
"Lock All": "ล็อคทั้งหมด",
"Reset": "รีเซ็ต",
"Edit Metadata": "แก้ไขข้อมูลเมตา",
"Locked": "ล็อคแล้ว",
"Select Metadata Source": "เลือกแหล่งข้อมูลเมตา",
"Keep manual input": "เก็บข้อมูลที่ป้อนด้วยตนเอง",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "นิยาย, วิทยาศาสตร์, ประวัติศาสตร์",
"Select Cover Image": "เลือกภาพปก",
"Open Book in New Window": "เปิดหนังสือในหน้าต่างใหม่",
"Voices for {{lang}}": "เสียงสำหรับ {{lang}}",
"Yandex Translate": "Yandex Translate",
"YYYY or YYYY-MM-DD": "YYYY หรือ YYYY-MM-DD",
"Restore Purchase": "กู้คืนการซื้อ",
"No purchases found to restore.": "ไม่พบการซื้อเพื่อกู้คืน",
"Failed to restore purchases.": "ไม่สามารถกู้คืนการซื้อได้",
"Failed to manage subscription.": "ไม่สามารถจัดการการสมัครสมาชิกได้",
"Failed to load subscription plans.": "ไม่สามารถโหลดแผนการสมัครสมาชิกได้",
"year": "ปี",
"Failed to create checkout session": "ไม่สามารถสร้างเซสชันการชำระเงินได้",
"Storage": "พื้นที่เก็บข้อมูล",
"Terms of Service": "ข้อกำหนดในการให้บริการ",
"Privacy Policy": "นโยบายความเป็นส่วนตัว",
"Disable Double Click": "ปิดการคลิกสองครั้ง"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Silmeyi Onayla",
"Copied to notebook": "Not defterine kopyalandı",
"Copy": "Kopyala",
"Custom CSS": "Özel CSS",
"Dark Mode": "Karanlık Mod",
"Default": "Varsayılan",
"Default Font": "Varsayılan Yazı Tipi",
@@ -28,7 +27,6 @@
"Dictionary": "Sözlük",
"Download Readest": "Readest'i İndir",
"Edit": "Düzenle",
"Enter your custom CSS here...": "Özel CSS'nizi buraya girin...",
"Excerpts": "Alıntılar",
"Failed to import book(s): {{filenames}}": "Kitap(lar) içe aktarılamadı: {{filenames}}",
"Fast": "Hızlı",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Yatay Yön",
"Hyphenation": "Heceleme",
"Import Books": "Kitapları İçe Aktar",
"Language:": "Dil:",
"Layout": "Düzen",
"Light Mode": "Aydınlık Mod",
"Loading...": "Yükleniyor...",
@@ -70,8 +67,8 @@
"Paging Animation": "Sayfa Çevirme Animasyonu",
"Paragraph": "Paragraf",
"Parallel Read": "Paralel Okuma",
"Published:": "Yayınlanma:",
"Publisher:": "Yayıncı:",
"Published": "Yayınlanma",
"Publisher": "Yayıncı",
"Reading Progress Synced": "Okuma ilerlemesi senkronize edildi",
"Reload Page": "Sayfayı Yenile",
"Reveal in File Explorer": "Dosya Gezgininde Göster",
@@ -96,7 +93,7 @@
"Slow": "Yavaş",
"Solarized": "Solarized",
"Speak": "Konuş",
"Subjects:": "Konular:",
"Subjects": "Konular",
"System Fonts": "Sistem Yazı Tipleri",
"TTS not supported in this device": "Bu cihazda TTS desteklenmiyor",
"Theme Color": "Tema Rengi",
@@ -105,7 +102,7 @@
"Translated Text": "Çevrilen Metin",
"Unknown": "Bilinmiyor",
"Untitled": "Başlıksız",
"Updated:": "Güncellendi:",
"Updated": "Güncellendi",
"Version {{version}}": "Sürüm {{version}}",
"Vertical Direction": "Dikey Yön",
"Welcome to your library. You can import your books here and read them anytime.": "Kütüphanenize hoş geldiniz. Buradan kitaplarınızı içe aktarabilir ve istediğiniz zaman okuyabilirsiniz.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Kitap indirilemedi: {{title}}",
"Upload Book": "Kitap Yükle",
"Auto Upload Books to Cloud": "Kitapları Otomatik Olarak Buluta Yükle",
"{{percentage}}% of Cloud Storage Used.": "Bulut Depolamanın %{{percentage}}'si kullanıldı.",
"Storage": "Depolama",
"Book deleted: {{title}}": "Kitap silindi: {{title}}",
"Failed to delete book: {{title}}": "Kitap silinemedi: {{title}}",
"Check Updates on Start": "Başlangıçta Güncellemeleri Kontrol Et",
@@ -161,7 +156,7 @@
"Create a Password": "Şifre oluştur",
"Sign up": "Kaydol",
"Signing up...": "Kaydolunuyor...",
"Dont have an account? Sign up": "Hesabınız yok mu? Kaydolun",
"Don't have an account? Sign up": "Hesabınız yok mu? Kaydolun",
"Check your email for the confirmation link": "Onay bağlantısı için e-posta adresinizi kontrol edin",
"Signing in ...": "Giriş yapılıyor...",
"Send a magic link email": "Sihirli bağlantı e-postası gönder",
@@ -185,35 +180,13 @@
"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.",
"Unlimited Offline/Online Reading": "Sınırsız Çevrimdışı/Çevrimiçi Okuma",
"Unlimited Cloud Sync Devices": "Sınırsız Bulut Senkronizasyon Cihazları",
"Essential Text-to-Speech Voices": "Temel Metinden Sese Dönüştürme Sesleri",
"DeepL Free Access": "DeepL Ücretsiz Erişim",
"Community Support": "Topluluk Desteği",
"500 MB Cloud Sync Space": "500 MB Bulut Senkronizasyon Alanı",
"Includes All Free Tier Benefits": "Tüm Ücretsiz Paket Avantajlarını İçerir",
"AI Summaries": "Yapay Zeka Özetleri",
"AI Translations": "Yapay Zeka Çevirileri",
"Priority Support": "Öncelikli Destek",
"DeepL Pro Access": "DeepL Pro Erişimi",
"Expanded Text-to-Speech Voices": "Genişletilmiş Metinden Sese Dönüştürme Sesleri",
"2000 MB Cloud Sync Space": "2000 MB Bulut Senkronizasyon Alanı",
"Includes All Plus Tier Benefits": "Tüm Plus Paket Avantajlarını İçerir",
"Unlimited AI Summaries": "Sınırsız Yapay Zeka Özetleri",
"Unlimited AI Translations": "Sınırsız Yapay Zeka Çevirileri",
"Unlimited DeepL Pro Access": "Sınırsız DeepL Pro Erişimi",
"Advanced AI Tools": "Gelişmiş Yapay Zeka Araçları",
"Early Feature Access": "Erken Özellik Erişimi",
"10 GB Cloud Sync Space": "10 GB Bulut Senkronizasyon Alanı",
"Loading profile...": "Profil yükleniyor...",
"Features": "Özellikler",
"Delete Account": "Hesabı Sil",
"Delete Your Account?": "Hesabınızı Silmek İstiyor musunuz?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Bu işlem geri alınamaz. Buluttaki tüm verileriniz kalıcı olarak silinecektir.",
"Delete Permanently": "Kalıcı Olarak Sil",
"Free Tier": "Ücretsiz Paket",
"Plus Tier": "Plus Paket",
"Pro Tier": "Pro Paket",
"RTL Direction": "RTL Yönü",
"Maximum Column Height": "Maksimum Sütun Yüksekliği",
"Maximum Column Width": "Maksimum Sütun Genişliği",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "İndiriliyor {{downloaded}}/{{contentLength}}",
"Download finished": "İndirme tamamlandı",
"DOWNLOAD & INSTALL": "İNDİR & YÜKLE",
"Changelog:": "Değişiklik Günlüğü:",
"Changelog": "Değişiklik Günlüğü",
"Software Update": "Yazılım Güncellemesi",
"Title": "Başlık",
"Date Read": "Okunma Tarihi",
@@ -279,10 +252,9 @@
"Ascending": "Artan",
"Descending": "Azalan",
"Sort by...": "Şuna göre sırala...",
"Added:": "Eklenme:",
"Format:": "Biçim:",
"Added": "Eklenme",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Tanım:",
"Description": "Tanım",
"No description available": "Tanım mevcut değil",
"List": "Liste",
"Grid": "Izgara",
@@ -299,7 +271,7 @@
"Checking for updates...": "Yenilikler kontrol ediliyor...",
"Error checking for updates": "Güncellemeleri kontrol ederken hata oluştu",
"Details": "Detaylar",
"File Size:": "Dosya Boyutu:",
"File Size": "Dosya Boyutu",
"Auto Detect": "Algıla",
"Next Section": "Sonraki Bölüm",
"Previous Section": "Önceki Bölüm",
@@ -347,7 +319,6 @@
"Quota Exceeded": "Kota aşıldı",
"{{percentage}}% of Daily Translation Characters Used.": "Çeviri için günlük karakterlerin %{{percentage}}'si kullanıldı.",
"Translation Characters": "Çeviri Karakterleri",
"Daily translation quota reached. Select another translate service to proceed.": "Günlük çeviri kotası aşıldı. Devam etmek için başka bir çeviri hizmeti seçin.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} ses",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} sesler",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Bir şeyler yanlış gitti. Endişelenmeyin, ekibimiz bilgilendirildi ve bir çözüm üzerinde çalışıyoruz.",
@@ -363,5 +334,132 @@
"Right Margin (px)": "Sağ Kenar Boşluğu (px)",
"Left Margin (px)": "Sol Kenar Boşluğu (px)",
"Column Gap (%)": "Sütun Aralığı (%)",
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster"
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster",
"Translation Not Available": "Çeviri Mevcut Değil",
"Custom Content CSS": "Özel İçerik CSS'si",
"Enter CSS for book content styling...": "Kitap içeriği stili için CSS girin...",
"Custom Reader UI CSS": "Özel Okuyucu Arayüzü CSS'si",
"Enter CSS for reader interface styling...": "Okuyucu arayüzü stili için CSS girin...",
"Crop": "Kes",
"Book Covers": "Kitap Kapağı",
"Fit": "Uygun",
"Reset {{settings}}": "{{settings}}'i Sıfırla",
"Reset Settings": "Ayarları Sıfırla",
"{{count}} pages left in chapter_one": "Bu bölümde {{count}} sayfa kaldı",
"{{count}} pages left in chapter_other": "Bu bölümde {{count}} sayfa kaldı",
"Show Remaining Pages": "Kalan sayfaları göster",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Aboneliği Yönet",
"Coming Soon": "Yakında",
"Upgrade to {{plan}}": "{{plan}} planına yükselt",
"Upgrade to Plus or Pro": "Plus veya Pro'ya yükselt",
"Current Plan": "Mevcut Plan",
"Plan Limits": "Plan Limitleri",
"Processing your payment...": "Ödemeniz işleniyor...",
"Please wait while we confirm your subscription.": "Aboneliğinizi onaylıyoruz, lütfen bekleyin.",
"Payment Processing": "Ödeme İşleniyor",
"Your payment is being processed. This usually takes a few moments.": "Ödemeniz işleniyor. Bu genelde birkaç saniye sürer.",
"Payment Failed": "Ödeme Başarısız",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Aboneliğinizi işleyemedik. Lütfen tekrar deneyin veya sorun devam ederse destek ile iletişime geçin.",
"Back to Profile": "Profille Dön",
"Subscription Successful!": "Abonelik Başarılı!",
"Thank you for your subscription! Your payment has been processed successfully.": "Aboneliğiniz için teşekkürler! Ödemeniz başarıyla işlendi.",
"Email:": "E-posta:",
"Plan:": "Plan:",
"Amount:": "Tutar:",
"Subscription ID:": "Abonelik ID'si:",
"Go to Library": "Kütüphaneye Git",
"Need help? Contact our support team at support@readest.com": "Yardım mı gerekiyor? Destek ekibimizle iletişime geçin: support@readest.com",
"Free Plan": "Ücretsiz Plan",
"month": "ay",
"AI Translations (per day)": "AI Çevirileri (günlük)",
"Plus Plan": "Plus Plan",
"Includes All Free Plan Benefits": "Tüm Ücretsiz Plan Özelliklerini İçerir",
"Pro Plan": "Pro Plan",
"More AI Translations": "Daha Fazla AI Çevirisi",
"Complete Your Subscription": "Aboneliğinizi Tamamlayın",
"{{percentage}}% of Cloud Sync Space Used.": "Bulut Senkronizasyon Alanının %{{percentage}}'si Kullanıldı.",
"Cloud Sync Storage": "Bulut Senkronizasyon Depolama",
"Disable": "Devre Dışı Bırak",
"Enable": "Etkinleştir",
"Upgrade to Readest Premium": "Readest Premiuma Yükselt",
"Show Source Text": "Kaynak Metni Göster",
"Cross-Platform Sync": "Platformlar Arası Senkronizasyon",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Kütüphanenizi, ilerlemenizi, vurgularınızı ve notlarınızı tüm cihazlarınızda sorunsuzca senkronize edin—bir daha asla yerinizi kaybetmeyin.",
"Customizable Reading": "Özelleştirilebilir Okuma",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Mükemmel okuma deneyimi için ayarlanabilir yazı tipleri, düzenler, temalar ve gelişmiş görüntü ayarlarıyla her detayı kişiselleştirin.",
"AI Read Aloud": "AI Sesli Okuma",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Kitaplarınızı hayata geçiren doğal sesli AI sesleriyle eller serbest okuma keyfini çıkarın.",
"AI Translations": "AI Çeviriler",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Google, Azure veya DeepL'in gücüyle herhangi bir metni anında çevirin—her dildeki içeriği anlayın.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Diğer okuyucularla bağlantı kurun ve dostane topluluk kanallarımızda hızla yardım alın.",
"Unlimited AI Read Aloud Hours": "Sınırsız AI Sesli Okuma Saatleri",
"Listen without limits—convert as much text as you like into immersive audio.": "Sınırsız dinleyin—istediğiniz kadar metni sürükleyici sese dönüştürün.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Daha fazla günlük kullanım ve gelişmiş seçeneklerle gelişmiş çeviri yeteneklerinin kilidini açın.",
"DeepL Pro Access": "DeepL Pro Erişimi",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Yardıma ihtiyacınız olduğunda daha hızlı yanıtlar ve özel destek keyfini çıkarın.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Günlük çeviri kotasına ulaşıldı. AI çevirileri kullanmaya devam etmek için planınızı yükseltin.",
"Includes All Plus Plan Benefits": "Tüm Plus Plan Avantajları Dahil",
"Early Feature Access": "Erken Özellik Erişimi",
"Be the first to explore new features, updates, and innovations before anyone else.": "Yeni özellikleri, güncellemeleri ve yenilikleri herkesten önce keşfedin.",
"Advanced AI Tools": "Gelişmiş AI Araçları",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Akıllı okuma, çeviri ve içerik keşfi için güçlü AI araçlarını kullanın.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Mevcut en doğru çeviri motoruyla günlük 100.000 karaktere kadar çeviri yapın.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Mevcut en doğru çeviri motoruyla günlük 500.000 karaktere kadar çeviri yapın.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Tüm okuma koleksiyonunuzu 5 GB'a kadar güvenli bulut depolamayla saklayın ve erişin.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Tüm okuma koleksiyonunuzu 20 GB'a kadar güvenli bulut depolamayla saklayın ve erişin.",
"Deleted cloud backup of the book: {{title}}": "Kitabın bulut yedeği silindi: {{title}}",
"Failed to delete cloud backup of the book": "Kitabın bulut yedeğini silme işlemi başarısız oldu.",
"Are you sure to delete the cloud backup of the selected book?": "Seçilen kitabın bulut yedeğini silmek istediğinize emin misiniz?",
"What's New in Readest": "Readest'te Neler Yeni",
"Enter book title": "Kitap başlığını girin",
"Subtitle": "Alt başlık",
"Enter book subtitle": "Kitap alt başlığını girin",
"Enter author name": "Yazar adını girin",
"Series": "Seri",
"Enter series name": "Seri adını girin",
"Series Index": "Seri numarası",
"Enter series index": "Seri numarasını girin",
"Total in Series": "Serideki toplam",
"Enter total books in series": "Serideki toplam kitap sayısını girin",
"Enter publisher": "Yayınevini girin",
"Publication Date": "Yayın tarihi",
"Identifier": "Tanımlayıcı",
"Enter book description": "Kitap açıklamasını girin",
"Change cover image": "Kapak resmini değiştir",
"Replace": "Değiştir",
"Unlock cover": "Kapağın kilidini aç",
"Lock cover": "Kapağı kilitle",
"Auto-Retrieve Metadata": "Meta verileri otomatik al",
"Auto-Retrieve": "Otomatik alma",
"Unlock all fields": "Tüm alanların kilidini aç",
"Unlock All": "Tümünün kilidini aç",
"Lock all fields": "Tüm alanları kilitle",
"Lock All": "Tümünü kilitle",
"Reset": "Sıfırla",
"Edit Metadata": "Meta verileri düzenle",
"Locked": "Kilitli",
"Select Metadata Source": "Meta veri kaynağını seç",
"Keep manual input": "Manuel girişi koru",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Kurgu, Bilim, Tarih",
"Select Cover Image": "Kapak Resmi Seç",
"Open Book in New Window": "Kitabı Yeni Pencerede Aç",
"Voices for {{lang}}": "{{lang}} için Sesler",
"Yandex Translate": "Yandex Çeviri",
"YYYY or YYYY-MM-DD": "YYYY veya YYYY-MM-DD",
"Restore Purchase": "Satın Almayı Geri Yükle",
"No purchases found to restore.": "Geri yüklemek için satın alma bulunamadı.",
"Failed to restore purchases.": "Satın alımları geri yükleme başarısız oldu.",
"Failed to manage subscription.": "Aboneliği yönetme başarısız oldu.",
"Failed to load subscription plans.": "Abonelik planlarını yükleme başarısız oldu.",
"year": "yıl",
"Failed to create checkout session": "Ödeme oturumu oluşturma başarısız oldu",
"Storage": "Depolama",
"Terms of Service": "Hizmet Şartları",
"Privacy Policy": "Gizlilik Politikası",
"Disable Double Click": "Çift Tıklamayı Devre Dışı Bırak"
}
@@ -1,7 +1,7 @@
{
"(detected)": "(виявлено)",
"About Readest": "Про Readest",
"Add your notes here...": "Додайте свої нотатки тут...",
"Add your notes here...": "Додайте свої нотатки сюди...",
"Animation": "Анімація",
"Annotate": "Анотація",
"Apply": "Застосувати",
@@ -18,7 +18,6 @@
"Confirm Deletion": "Підтвердити видалення",
"Copied to notebook": "Скопійовано до нотатника",
"Copy": "Копіювати",
"Custom CSS": "Користувацький CSS",
"Dark Mode": "Темний режим",
"Default": "За замовчуванням",
"Default Font": "Шрифт за замовчуванням",
@@ -28,7 +27,6 @@
"Dictionary": "Словник",
"Download Readest": "Завантажити Readest",
"Edit": "Редагувати",
"Enter your custom CSS here...": "Введіть ваш користувацький CSS тут...",
"Excerpts": "Уривки",
"Failed to import book(s): {{filenames}}": "Не вдалося імпортувати книгу(и): {{filenames}}",
"Fast": "Швидко",
@@ -48,33 +46,32 @@
"Horizontal Direction": "Горизонтальний напрямок",
"Hyphenation": "Перенесення слів",
"Import Books": "Імпортувати книги",
"Language:": "Мова:",
"Layout": "Макет",
"Light Mode": "Світлий режим",
"Loading...": "Завантаження...",
"Logged in": "Увійшли",
"Logged in as {{userDisplayName}}": "Увійшли як {{userDisplayName}}",
"Match Case": "Враховувати регістр",
"Logged in": "Увійдено",
"Logged in as {{userDisplayName}}": "Увійдено як {{userDisplayName}}",
"Match Case": "Враховувати реґістр",
"Match Diacritics": "Враховувати діакритичні знаки",
"Match Whole Words": "Шукати цілі слова",
"Maximum Number of Columns": "Максимальна кількість колонок",
"Minimum Font Size": "Мінімальний розмір шрифту",
"Monospace Font": "Моноширинний шрифт",
"More Info": "Більше інформації",
"Nord": "Північний",
"Nord": "Норд",
"Notebook": "Нотатник",
"Notes": "Нотатки",
"Open": "Відкрити",
"Original Text": "Оригінальний текст",
"Original Text": "Ориґінальний текст",
"Page": "Сторінка",
"Paging Animation": "Анімація гортання",
"Paragraph": "Абзац",
"Parallel Read": "Паралельне читання",
"Published:": "Опубліковано:",
"Publisher:": "Видавець:",
"Published": "Опубліковано",
"Publisher": "Видавець",
"Reading Progress Synced": "Прогрес читання синхронізовано",
"Reload Page": "Перезавантажити сторінку",
"Reveal in File Explorer": "Показати у провіднику",
"Reveal in File Explorer": "Показати у Файловому менеджері",
"Reveal in Finder": "Показати у Finder",
"Reveal in Folder": "Показати в папці",
"Sans-Serif Font": "Шрифт без засічок",
@@ -96,7 +93,7 @@
"Slow": "Повільно",
"Solarized": "Сонячний",
"Speak": "Озвучити",
"Subjects:": "Теми:",
"Subjects": "Теми",
"System Fonts": "Системні шрифти",
"TTS not supported in this device": "TTS не підтримується на цьому пристрої",
"Theme Color": "Колір теми",
@@ -105,7 +102,7 @@
"Translated Text": "Перекладений текст",
"Unknown": "Невідомо",
"Untitled": "Без назви",
"Updated:": "Оновлено:",
"Updated": "Оновлено",
"Version {{version}}": "Версія {{version}}",
"Vertical Direction": "Вертикальний напрямок",
"Welcome to your library. You can import your books here and read them anytime.": "Ласкаво просимо до вашої бібліотеки. Ви можете імпортувати сюди свої книги та читати їх будь-коли.",
@@ -115,10 +112,10 @@
"TTS not supported for PDF": "TTS не підтримується для PDF",
"Override Book Font": "Перевизначити шрифт книги",
"Apply to All Books": "Застосувати до всіх книг",
"Apply to This Book": "Застосувати до цієї книги",
"Apply to This Book": "Застосувати лише до цієї книги",
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
"Check Update": "Перевірити оновлення",
"Already the latest version": "Вже остання версія",
"Already the latest version": "У вас вже встановлена остання версія",
"Book Details": "Деталі книги",
"From Local File": "З локального файлу",
"TOC": "Зміст",
@@ -128,12 +125,10 @@
"Failed to download book: {{title}}": "Не вдалося завантажити книгу: {{title}}",
"Upload Book": "Завантажити книгу",
"Auto Upload Books to Cloud": "Автоматично завантажувати книги в хмару",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% використано хмарного сховища.",
"Storage": "Сховище",
"Book deleted: {{title}}": "Книгу видалено: {{title}}",
"Failed to delete book: {{title}}": "Не вдалося видалити книгу: {{title}}",
"Check Updates on Start": "Перевіряти оновлення при запуску",
"Insufficient storage quota": "Недостатньо квоти сховища",
"Insufficient storage quota": "Недостатньо місця у сховищі",
"Font Weight": "Насиченість шрифту",
"Line Spacing": "Міжрядковий інтервал",
"Word Spacing": "Міжслівний інтервал",
@@ -161,11 +156,11 @@
"Create a Password": "Створити пароль",
"Sign up": "Зареєструватися",
"Signing up...": "Реєстрація...",
"Dont have an account? Sign up": "Немає акаунту? Зареєструйтеся",
"Check your email for the confirmation link": "Перевірте свою електронну пошту для підтвердження посилання",
"Don't have an account? Sign up": "Немає акаунту? Зареєструйтеся",
"Check your email for the confirmation link": "Перевірте свою електронну пошту на посилання із підтвердженням",
"Signing in ...": "Увійти...",
"Send a magic link email": "Відправити магічне посилання на електронну пошту",
"Check your email for the magic link": "Перевірте свою електронну пошту для магічного посилання",
"Check your email for the magic link": "Ми надіслали вам магічне посилання. Перевірте свою електронну пошту",
"Send reset password instructions": "Надіслати інструкції для скидання пароля",
"Sending reset instructions ...": "Надсилаються інструкції для скидання пароля...",
"Forgot your password?": "Забули пароль?",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "Увійти через GitHub",
"Account": "Обліковий запис",
"Failed to delete user. Please try again later.": "Не вдалося видалити користувача. Будь ласка, спробуйте пізніше.",
"Unlimited Offline/Online Reading": "Необмежене читання офлайн/онлайн",
"Unlimited Cloud Sync Devices": "Необмежена кількість пристроїв для хмарної синхронізації",
"Essential Text-to-Speech Voices": "Основні голоси для перетворення тексту в мовлення",
"DeepL Free Access": "Безкоштовний доступ до DeepL",
"Community Support": "Підтримка спільноти",
"500 MB Cloud Sync Space": "500 МБ простору для хмарної синхронізації",
"Includes All Free Tier Benefits": "Включає всі переваги безкоштовного тарифу",
"AI Summaries": "AI-резюме",
"AI Translations": "AI-переклади",
"Priority Support": "Пріоритетна підтримка",
"DeepL Pro Access": "Доступ до DeepL Pro",
"Expanded Text-to-Speech Voices": "Розширений набір голосів для перетворення тексту в мовлення",
"2000 MB Cloud Sync Space": "2000 МБ простору для хмарної синхронізації",
"Includes All Plus Tier Benefits": "Включає всі переваги тарифу Plus",
"Unlimited AI Summaries": "Необмежені AI-резюме",
"Unlimited AI Translations": "Необмежені AI-переклади",
"Unlimited DeepL Pro Access": "Необмежений доступ до DeepL Pro",
"Advanced AI Tools": "Розширені AI-інструменти",
"Early Feature Access": "Ранній доступ до нових функцій",
"10 GB Cloud Sync Space": "10 ГБ простору для хмарної синхронізації",
"Loading profile...": "Завантаження профілю...",
"Features": "Функції",
"Delete Account": "Видалити обліковий запис",
"Delete Your Account?": "Видалити ваш обліковий запис?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Цю дію неможливо скасувати. Всі ваші дані в хмарі будуть повністю видалені.",
"Delete Permanently": "Видалити назавжди",
"Free Tier": "Безкоштовний тариф",
"Plus Tier": "Тариф Plus",
"Pro Tier": "Тариф Pro",
"RTL Direction": "Напрямок RTL",
"Maximum Column Height": "Максимальна висота колонки",
"Maximum Column Width": "Максимальна ширина колонки",
@@ -222,7 +195,7 @@
"No supported files found. Supported formats: {{formats}}": "Не знайдено підтримуваних файлів. Підтримувані формати: {{formats}}",
"Drop to Import Books": "Перетягніть для імпорту книг",
"Custom": "Користувацький",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "Весь світ — театр,\nІ всі чоловіки та жінки — просто актори;\nУ них є свої виходи та входи,\nІ один чоловік у свій час грає багато ролей,\nЙого вчинки — це сім віков.\n\n— Вільям Шекспір",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "Так, світ — театр,\nДе всі чоловіки й жінки — актори.\nТут кожному приписаний свій вихід,\nне одну з них кожне грає роль.\nСім дій в тій п’єсі...\nВільям Шекспір\n(переклад Олександра Мокровольського)",
"Custom Theme": "Користувацька тема",
"Theme Name": "Назва теми",
"Text Color": "Колір тексту",
@@ -231,8 +204,8 @@
"Contrast": "Контраст",
"Sunset": "Захід сонця",
"Double Border": "Подвійна рамка",
"Border Color": "Колір рамки",
"Border Frame": "Рамка рамки",
"Border Color": "Колір країв",
"Border Frame": "Рамка по краям",
"Show Header": "Показати заголовок",
"Show Footer": "Показати нижню частину",
"Small": "Малий",
@@ -244,7 +217,7 @@
"Exported from Readest": "Експортовано з Readest",
"Highlights & Annotations": "Виділення та анотації",
"Note": "Примітка",
"Copied to clipboard": "Скопійовано в буфер обміну",
"Copied to clipboard": "Скопійовано до буфера обміну",
"Export Annotations": "Експортувати анотації",
"Auto Import on File Open": "Автоматичний імпорт при відкритті файлу",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
@@ -265,13 +238,13 @@
"Header & Footer": "Заголовок і нижній колонтитул",
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
"A new version of Readest is available!": "Доступна нова версія Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлена версія {{currentVersion}}).",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлено {{currentVersion}}).",
"Download and install now?": "Завантажити та встановити зараз?",
"Downloading {{downloaded}} of {{contentLength}}": "Завантаження {{downloaded}} з {{contentLength}}",
"Downloading {{downloaded}} of {{contentLength}}": "Завантаженно {{downloaded}} з {{contentLength}}",
"Download finished": "Завантаження завершено",
"DOWNLOAD & INSTALL": "ЗАВАНТАЖИТИ І ВСТАНОВИТИ",
"Changelog:": "Журнал змін:",
"Software Update": "Оновлення програмного забезпечення",
"DOWNLOAD & INSTALL": "ЗАВАНТАЖИТИ ТА ВСТАНОВИТИ",
"Changelog": "Список змін",
"Software Update": "Оновлення ПЗ",
"Title": "Назва",
"Date Read": "Дата прочитання",
"Date Added": "Дата додавання",
@@ -279,27 +252,26 @@
"Ascending": "За зростанням",
"Descending": "За спаданням",
"Sort by...": "Сортувати за...",
"Added:": "Додано:",
"Format:": "Формат:",
"Added": "Додано",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Опис:",
"Description": "Опис",
"No description available": "Опис недоступний",
"List": "Список",
"Grid": "Сітка",
"(from 'As You Like It', Act II)": "(з 'Як вам це подобається', Акт II)",
"(from 'As You Like It', Act II)": "(із 'Як вам це подобається', Дія II)",
"Link Color": "Колір посилання",
"Volume Keys for Page Flip": "Клавіші гучності для перевороту сторінки",
"Clicks for Page Flip": "Клацання для перевороту сторінки",
"Swap Clicks Area": "Змінити область клацання",
"Volume Keys for Page Flip": "Перегортати сторінки клавішами гучності",
"Clicks for Page Flip": "Перегортати сторінки натисканням",
"Swap Clicks Area": "Поміняти області клацання місцями",
"Screen": "Екран",
"Orientation": "Орієнтація",
"Portrait": "Портрет",
"Landscape": "Ландшафт",
"Open Last Book on Start": "Відкрити останню книгу при запуску",
"Open Last Book on Start": "Відкривати останню книгу при запуску",
"Checking for updates...": "Перевірка наявності оновлень...",
"Error checking for updates": "Помилка перевірки наявності оновлень",
"Details": "Деталі",
"File Size:": "Розмір файлу:",
"File Size": "Розмір файлу",
"Auto Detect": "Авто виявлення",
"Next Section": "Наступний розділ",
"Previous Section": "Попередній розділ",
@@ -313,22 +285,22 @@
"Deselect": "Зняти",
"Select All": "Усі",
"No translation available.": "Переклад недоступний.",
"Translated by {{provider}}.": "Перекладено {{provider}}.",
"Translated by {{provider}}.": "Перекладено за допомогою {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Azure Translator": "Azure Translator",
"Invert Image In Dark Mode": "Інвертувати зображення в темряві",
"Google Translate": "Google Перекладач",
"Azure Translator": "Azure Перекладач",
"Invert Image In Dark Mode": "Інвертувати зображення в темному режимі",
"Help improve Readest": "Допоможіть покращити Readest",
"Sharing anonymized statistics": "Поділ анонімною статистикою",
"Sharing anonymized statistics": "Ділитись анонімною статистикою",
"Interface Language": "Мова інтерфейсу",
"Translation": "Переклад",
"Enable Translation": "Увімкнути переклад",
"Translation Service": "Служба перекладу",
"Translate To": "Перекласти на",
"Translate To": "Мова перекладу",
"Disable Translation": "Вимкнути переклад",
"Scroll": "Прокрутка",
"Overlap Pixels": "Пікселі перекриття",
"Click": "Клацнути",
"Overlap Pixels": ерекривати пікселі",
"Click": "Натиснути",
"System Language": "Системна мова",
"Security": "Безпека",
"Allow JavaScript": "Дозволити JavaScript",
@@ -351,17 +323,16 @@
"Quota Exceeded": "Перевищено ліміт",
"{{percentage}}% of Daily Translation Characters Used.": "Використано {{percentage}}% символів перекладу за день.",
"Translation Characters": "Символи перекладу",
"Daily translation quota reached. Select another translate service to proceed.": "Досягнуто добового ліміту перекладу. Виберіть іншу службу перекладу для продовження.",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} голос",
"{{engine}}: {{count}} voices_few": "{{engine}}: {{count}} голоси",
"{{engine}}: {{count}} voices_many": "{{engine}}: {{count}} голосів",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} голоси",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Щось пішло не так. Не хвилюйтеся, наша команда була сповіщена, і ми працюємо над виправленням.",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "От халепа! Щось пішло не так. Не хвилюйтеся, наша команда знає про це, ми вже працюємо над виправленням.",
"Error Details:": "Деталі помилки:",
"Try Again": "Спробувати ще раз",
"Need help?": "Потрібна допомога?",
"Contact Support": "Зв'язатися з підтримкою",
"Code Highlighting": "Підсвічування коду",
"Code Highlighting": "Підсвічувати код",
"Enable Highlighting": "Увімкнути підсвічування",
"Code Language": "Мова коду",
"Top Margin (px)": "Верхній відступ (px)",
@@ -369,5 +340,134 @@
"Right Margin (px)": "Правий відступ (px)",
"Left Margin (px)": "Лівий відступ (px)",
"Column Gap (%)": "Проміжок між стовпцями (%)",
"Always Show Status Bar": "Завжди показувати панель стану"
"Always Show Status Bar": "Завжди показувати панель стану",
"Translation Not Available": "Переклад недоступний",
"Custom Content CSS": "Користуацький CSS вмісту",
"Enter CSS for book content styling...": "Введіть CSS для стилізації вмісту книги...",
"Custom Reader UI CSS": "CSS інтерфейсу",
"Enter CSS for reader interface styling...": "Введіть CSS для стилізації інтерфейсу читанки...",
"Crop": "Обрізати",
"Book Covers": "Обкладинки книг",
"Fit": "Припасувати",
"Reset {{settings}}": "Скинути {{settings}}",
"Reset Settings": "Скинути налаштування",
"{{count}} pages left in chapter_one": "Залишилася {{count}} сторінка у розділі",
"{{count}} pages left in chapter_few": "Залишилось {{count}} сторінки у розділі",
"{{count}} pages left in chapter_many": "Залишилось {{count}} сторінок у розділі",
"{{count}} pages left in chapter_other": "Залишилось {{count}} сторінок у розділі",
"Show Remaining Pages": "Показати залишок сторінок",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Керувати підпискою",
"Coming Soon": "Незабаром",
"Upgrade to {{plan}}": "Перейти на {{plan}}",
"Upgrade to Plus or Pro": "Перейти на Plus або Pro",
"Current Plan": "Поточний план",
"Plan Limits": "Обмеження плану",
"Processing your payment...": "Обробка вашого платежу...",
"Please wait while we confirm your subscription.": "Зачекайте, ми підтверджуємо вашу підписку.",
"Payment Processing": "Обробка платежу",
"Your payment is being processed. This usually takes a few moments.": "Ваш платіж обробляється. Зазвичай це займає кілька секунд.",
"Payment Failed": "Платіж не вдався",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Не вдалося обробити вашу підписку. Спробуйте ще раз або зверніться до служби підтримки, якщо проблема не зникне.",
"Back to Profile": "Повернутися до профілю",
"Subscription Successful!": "Підписка активована!",
"Thank you for your subscription! Your payment has been processed successfully.": "Дякуємо за підписку! Ваш платіж успішно оброблено.",
"Email:": "Електронна пошта:",
"Plan:": "План:",
"Amount:": "Сума:",
"Subscription ID:": "ID підписки:",
"Go to Library": "Перейти до бібліотеки",
"Need help? Contact our support team at support@readest.com": "Потрібна допомога? Зверніться до служби підтримки: support@readest.com",
"Free Plan": "Безкоштовний план",
"month": "місяць",
"AI Translations (per day)": "ШІ-переклади (на день)",
"Plus Plan": "План Plus",
"Includes All Free Plan Benefits": "Включає всі переваги Безкоштовного плану",
"Pro Plan": "План Pro",
"More AI Translations": "Більше ШІ-перекладів",
"Complete Your Subscription": "Завершіть підписку",
"{{percentage}}% of Cloud Sync Space Used.": "Використано {{percentage}}% хмарного сховища.",
"Cloud Sync Storage": "Хмарне сховище",
"Disable": "Вимкнути",
"Enable": "Увімкнути",
"Upgrade to Readest Premium": "Оновити до Readest Premium",
"Show Source Text": "Показати вихідний текст",
"Cross-Platform Sync": "Міжплатформова синхронізація",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Безперешкодно синхронізуйте свою бібліотеку, прогрес, виділення та нотатки на всіх своїх пристроях. Більше ніколи не втратите свої книги.",
"Customizable Reading": "Користувацьке читання",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Персоналізуйте кожну деталь за допомогою налаштовуваних шрифтів, макетів, тем та розширених налаштувань відображення для ідеального читання.",
"AI Read Aloud": "Озвучування ШІ",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Насолоджуйтесь читанням без використання рук з природними звучаннями голосів, які оживляють ваші книги.",
"AI Translations": "Переклади ШІ",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Перекладайте будь-який текст миттєво за допомогою Google, Azure або DeepL. Розумійте зміст будь-якою мовою.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Спілкуйтеся з іншими читачами та отримуйте швидку допомогу в наших дружніх каналах спільноти.",
"Unlimited AI Read Aloud Hours": "Необмежені години озвучування ШІ",
"Listen without limits—convert as much text as you like into immersive audio.": "Слухайте без обмежень—перетворюйте стільки тексту, скільки хочете, в захоплююче аудіо.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Розблокуйте покращені можливості перекладу з більшим щоденним використанням та розширеними функціями.",
"DeepL Pro Access": "Доступ до DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Насолоджуйтеся швидшими відповідями та персональною допомогою, коли вам потрібна підтримка.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Досягнуто денну квоту перекладів. Оновіть свій план, щоб продовжити використовувати переклади ШІ.",
"Includes All Plus Plan Benefits": "Включає усі переваги Plus плану",
"Early Feature Access": "Ранній Доступ до функцій",
"Be the first to explore new features, updates, and innovations before anyone else.": "Досліджуйте нові функції, оновлення та інновації першими.",
"Advanced AI Tools": "Розширені ШІ інструменти",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Використовуйте потужні ШІ інструменти для розумного читання, перекладу та пошуку контенту.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Перекладайте до 100 000 символів щодня з найточнішим доступним рушієм перекладу.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Перекладайте до 500 000 символів щодня з найточнішим доступним рушієм перекладу.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Безпечно зберігайте та отримуйте доступ до всієї колекції читання з до 5 ГБ хмарного сховища.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Безпечно зберігайте та отримуйте доступ до всієї колекції читання з до 20 ГБ хмарного сховища.",
"Deleted cloud backup of the book: {{title}}": "Видалено хмарну резервну копію книги: {{title}}",
"Failed to delete cloud backup of the book": "Не вдалося видалити хмарну резервну копію.",
"Are you sure to delete the cloud backup of the selected book?": "Ви впевнені, що хочете видалити хмарну резервну копію вибраної книги?",
"What's New in Readest": "Що нового в Readest",
"Enter book title": "Введіть назву книги",
"Subtitle": "Підзаголовок",
"Enter book subtitle": "Введіть підзаголовок книги",
"Enter author name": "Введіть ім'я автора",
"Series": "Цикл",
"Enter series name": "Введіть назву циклу",
"Series Index": "Номер циклу",
"Enter series index": "Введіть номер циклу",
"Total in Series": "Всього у циклі",
"Enter total books in series": "Введіть загальну кількість книг у циклі",
"Enter publisher": "Введіть видавництвця",
"Publication Date": "Дата видання",
"Identifier": "Ідентифікатор",
"Enter book description": "Введіть опис книги",
"Change cover image": "Змінити зображення обкладинки",
"Replace": "Замінити",
"Unlock cover": "Розблокувати обкладинку",
"Lock cover": "Заблокувати обкладинку",
"Auto-Retrieve Metadata": "Автоматично підтягнути метадані",
"Auto-Retrieve": "Автоматичне підтягування",
"Unlock all fields": "Розблокувати всі поля",
"Unlock All": "Розблокувати все",
"Lock all fields": "Заблокувати всі поля",
"Lock All": "Заблокувати все",
"Reset": "Скинути",
"Edit Metadata": "Редагувати метадані",
"Locked": "Заблоковано",
"Select Metadata Source": "Оберіть джерело метаданих",
"Keep manual input": "Зберегти ручне введення",
"Google Books": "Google Книги",
"Open Library": "Open Library",
"Fiction, Science, History": "Художня література, Наукова фантастика, Історія",
"Select Cover Image": "Оберіть зображення обкладинки",
"Open Book in New Window": "Відкрити книгу в новому вікні",
"Voices for {{lang}}": "Голоси для {{lang}}",
"Yandex Translate": "Yandex Перекладач",
"YYYY or YYYY-MM-DD": "YYYY або YYYY-MM-DD",
"Restore Purchase": "Відновити покупку",
"No purchases found to restore.": "Не знайдено покупок для відновлення.",
"Failed to restore purchases.": "Не вдалося відновити покупки.",
"Failed to manage subscription.": "Не вдалося керувати підпискою.",
"Failed to load subscription plans.": "Не вдалося завантажити плани підписки.",
"year": "рік",
"Failed to create checkout session": "Не вдалося створити сесію оформлення замовлення",
"Storage": "Сховище",
"Terms of Service": "Умови обслуговування",
"Privacy Policy": "Політика конфіденційності",
"Disable Double Click": "Вимкнути подвійне клацання"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "Xác nhận xóa",
"Copied to notebook": "Đã sao chép vào sổ ghi chú",
"Copy": "Sao chép",
"Custom CSS": "CSS tùy chỉnh",
"Dark Mode": "Chế độ tối",
"Default": "Mặc định",
"Default Font": "Phông chữ mặc định",
@@ -28,7 +27,6 @@
"Dictionary": "Từ điển",
"Download Readest": "Tải Readest",
"Edit": "Chỉnh sửa",
"Enter your custom CSS here...": "Nhập CSS tùy chỉnh của bạn vào đây...",
"Excerpts": "Trích dẫn",
"Failed to import book(s): {{filenames}}": "Không thể nhập sách: {{filenames}}",
"Fast": "Nhanh",
@@ -48,7 +46,6 @@
"Horizontal Direction": "Hướng ngang",
"Hyphenation": "Gạch nối từ",
"Import Books": "Nhập sách",
"Language:": "Ngôn ngữ:",
"Layout": "Bố cục",
"Light Mode": "Chế độ sáng",
"Loading...": "Đang tải...",
@@ -70,8 +67,8 @@
"Paging Animation": "Hiệu ứng lật trang",
"Paragraph": "Đoạn văn",
"Parallel Read": "Đọc song song",
"Published:": "Xuất bản:",
"Publisher:": "Nhà xuất bản:",
"Published": "Xuất bản",
"Publisher": "Nhà xuất bản",
"Reading Progress Synced": "Tiến độ đọc đã được đồng bộ",
"Reload Page": "Tải lại trang",
"Reveal in File Explorer": "Hiển thị trong File Explorer",
@@ -96,7 +93,7 @@
"Slow": "Chậm",
"Solarized": "Solarized",
"Speak": "Đọc",
"Subjects:": "Chủ đề:",
"Subjects": "Chủ đề",
"System Fonts": "Phông chữ hệ thống",
"TTS not supported in this device": "TTS không được hỗ trợ trên thiết bị này",
"Theme Color": "Màu chủ đề",
@@ -105,7 +102,7 @@
"Translated Text": "Văn bản dịch",
"Unknown": "Không xác định",
"Untitled": "Không có tiêu đề",
"Updated:": "Cập nhật:",
"Updated": "Cập nhật",
"Version {{version}}": "Phiên bản {{version}}",
"Vertical Direction": "Hướng dọc",
"Welcome to your library. You can import your books here and read them anytime.": "Chào mừng đến với thư viện của bạn. Bạn có thể nhập sách vào đây và đọc bất cứ lúc nào.",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "Không thể tải về sách: {{title}}",
"Upload Book": "Tải lên sách",
"Auto Upload Books to Cloud": "Tự động tải sách lên đám mây",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dung lượng lưu trữ đám mây đã sử dụng.",
"Storage": "Lưu trữ",
"Book deleted: {{title}}": "Sách đã xóa: {{title}}",
"Failed to delete book: {{title}}": "Không thể xóa sách: {{title}}",
"Check Updates on Start": "Kiểm tra cập nhật khi khởi động",
@@ -161,7 +156,7 @@
"Create a Password": "Tạo mật khẩu",
"Sign up": "Đăng ký",
"Signing up...": "Đang đăng ký...",
"Dont have an account? Sign up": "Chưa có tài khoản? Đăng ký",
"Don't have an account? Sign up": "Chưa có tài khoản? Đăng ký",
"Check your email for the confirmation link": "Kiểm tra email của bạn để nhận liên kết xác nhận",
"Signing in ...": "Đang đăng nhập ...",
"Send a magic link email": "Gửi email liên kết ma thuật",
@@ -185,35 +180,13 @@
"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.",
"Unlimited Offline/Online Reading": "Đọc ngoại tuyến/trực tuyến không giới hạn",
"Unlimited Cloud Sync Devices": "Đồng bộ hóa thiết bị đám mây không giới hạn",
"Essential Text-to-Speech Voices": "Giọng nói chuyển văn bản thành giọng nói cơ bản",
"DeepL Free Access": "Truy cập DeepL miễn phí",
"Community Support": "Hỗ trợ cộng đồng",
"500 MB Cloud Sync Space": "500 MB không gian đồng bộ hóa đám mây",
"Includes All Free Tier Benefits": "Bao gồm tất cả lợi ích của gói miễn phí",
"AI Summaries": "Tóm tắt AI",
"AI Translations": "Dịch thuật AI",
"Priority Support": "Hỗ trợ ưu tiên",
"DeepL Pro Access": "Truy cập DeepL Pro",
"Expanded Text-to-Speech Voices": "Giọng nói chuyển văn bản thành giọng nói mở rộng",
"2000 MB Cloud Sync Space": "2000 MB không gian đồng bộ hóa đám mây",
"Includes All Plus Tier Benefits": "Bao gồm tất cả lợi ích của gói Plus",
"Unlimited AI Summaries": "Tóm tắt AI không giới hạn",
"Unlimited AI Translations": "Dịch thuật AI không giới hạn",
"Unlimited DeepL Pro Access": "Truy cập DeepL Pro không giới hạn",
"Advanced AI Tools": "Công cụ AI nâng cao",
"Early Feature Access": "Truy cập tính năng sớm",
"10 GB Cloud Sync Space": "10 GB không gian đồng bộ hóa đám mây",
"Loading profile...": "Đang tải hồ sơ...",
"Features": "Tính năng",
"Delete Account": "Xóa tài khoản",
"Delete Your Account?": "Xóa tài khoản của bạn?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Hành động này không thể hoàn tác. Tất cả dữ liệu của bạn trên đám mây sẽ bị xóa vĩnh viễn.",
"Delete Permanently": "Xóa vĩnh viễn",
"Free Tier": "Gói miễn phí",
"Plus Tier": "Gói Plus",
"Pro Tier": "Gói Pro",
"RTL Direction": "Hướng RTL",
"Maximum Column Height": "Chiều cao cột tối đa",
"Maximum Column Width": "Chiều rộng cột tối đa",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "Đang tải {{downloaded}} trong {{contentLength}}",
"Download finished": "Tải xuống hoàn tất",
"DOWNLOAD & INSTALL": "TẢI XUỐNG & CÀI ĐẶT",
"Changelog:": "Thay đổi:",
"Changelog": "Thay đổi",
"Software Update": "Cập nhật phần mềm",
"Title": "Tiêu đề",
"Date Read": "Ngày đã đọc",
@@ -279,10 +252,9 @@
"Ascending": "Tăng dần",
"Descending": "Giảm dần",
"Sort by...": "Sắp xếp theo...",
"Added:": "Đã thêm:",
"Format:": "Định dạng:",
"Added": "Đã thêm",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description:": "Miêu tả:",
"Description": "Miêu tả",
"No description available": "Không có mô tả nào",
"List": "Danh sách",
"Grid": "Lưới",
@@ -299,7 +271,7 @@
"Checking for updates...": "Đang kiểm tra cập nhật...",
"Error checking for updates": "Lỗi khi kiểm tra cập nhật",
"Details": "Chi tiết",
"File Size:": "Kích thước tệp:",
"File Size": "Kích thước tệp",
"Auto Detect": "Phát hiện tự động",
"Next Section": "Phần tiếp theo",
"Previous Section": "Phần trước",
@@ -345,7 +317,6 @@
"Quota Exceeded": "Vượt hạn mức",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% số ký tự dịch hàng ngày đã sử dụng.",
"Translation Characters": "Những ký tự dịch",
"Daily translation quota reached. Select another translate service to proceed.": "Đã đạt hạn mức dịch hàng ngày. Chọn dịch vụ dịch khác để tiếp tục.",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} giọng",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Đã xảy ra sự cố. Đừng lo, nhóm của chúng tôi đã được thông báo và chúng tôi đang làm việc để sửa chữa.",
"Error Details:": "Chi tiết lỗi:",
@@ -360,5 +331,131 @@
"Right Margin (px)": "Lề phải (px)",
"Left Margin (px)": "Lề trái (px)",
"Column Gap (%)": "Khoảng cách cột (%)",
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái"
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái",
"Translation Not Available": "Không có bản dịch",
"Custom Content CSS": "CSS nội dung",
"Enter CSS for book content styling...": "Nhập CSS cho phần nội dung sách...",
"Custom Reader UI CSS": "CSS giao diện đọc",
"Enter CSS for reader interface styling...": "Nhập CSS cho giao diện trình đọc...",
"Crop": "Cắt",
"Book Covers": "Bìa sách",
"Fit": "Vừa vặn",
"Reset {{settings}}": "Đặt lại {{settings}}",
"Reset Settings": "Đặt lại cài đặt",
"{{count}} pages left in chapter_other": "Còn {{count}} trang trong chương",
"Show Remaining Pages": "Hiển thị trang còn lại",
"Source Han Serif CN VF": "Source Han Serif",
"Huiwen-mincho": "Huiwen Mincho",
"KingHwa_OldSong": "KingHwa Song",
"Manage Subscription": "Quản lý đăng ký",
"Coming Soon": "Sắp ra mắt",
"Upgrade to {{plan}}": "Nâng cấp lên {{plan}}",
"Upgrade to Plus or Pro": "Nâng cấp lên Plus hoặc Pro",
"Current Plan": "Gói hiện tại",
"Plan Limits": "Giới hạn gói",
"Processing your payment...": "Đang xử lý thanh toán...",
"Please wait while we confirm your subscription.": "Vui lòng chờ trong khi chúng tôi xác nhận đăng ký của bạn.",
"Payment Processing": "Đang xử lý thanh toán",
"Your payment is being processed. This usually takes a few moments.": "Thanh toán của bạn đang được xử lý. Thường mất vài giây.",
"Payment Failed": "Thanh toán thất bại",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Chúng tôi không thể xử lý đăng ký của bạn. Vui lòng thử lại hoặc liên hệ hỗ trợ nếu sự cố tiếp tục.",
"Back to Profile": "Quay lại hồ sơ",
"Subscription Successful!": "Đăng ký thành công!",
"Thank you for your subscription! Your payment has been processed successfully.": "Cảm ơn bạn đã đăng ký! Thanh toán của bạn đã được xử lý thành công.",
"Email:": "Email:",
"Plan:": "Gói:",
"Amount:": "Số tiền:",
"Subscription ID:": "ID đăng ký:",
"Go to Library": "Đi đến Thư viện",
"Need help? Contact our support team at support@readest.com": "Cần hỗ trợ? Liên hệ nhóm hỗ trợ tại support@readest.com",
"Free Plan": "Gói miễn phí",
"month": "tháng",
"AI Translations (per day)": "Dịch AI (mỗi ngày)",
"Plus Plan": "Gói Plus",
"Includes All Free Plan Benefits": "Bao gồm tất cả lợi ích của gói miễn phí",
"Pro Plan": "Gói Pro",
"More AI Translations": "Thêm dịch AI",
"Complete Your Subscription": "Hoàn tất đăng ký của bạn",
"{{percentage}}% of Cloud Sync Space Used.": "Đã sử dụng {{percentage}}% không gian đồng bộ đám mây.",
"Cloud Sync Storage": "Lưu trữ đồng bộ đám mây",
"Disable": "Tắt",
"Enable": "Bật",
"Upgrade to Readest Premium": "Đăng ký Readest Premium",
"Show Source Text": "Hiển thị văn bản gốc",
"Cross-Platform Sync": "Đồng Bộ Đa Nền Tảng",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Đồng bộ liền mạch thư viện, tiến độ, đánh dấu và ghi chú trên tất cả thiết bị—không bao giờ mất vị trí đọc nữa.",
"Customizable Reading": "Đọc Tùy Chỉnh",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Cá nhân hóa mọi chi tiết với phông chữ, bố cục, chủ đề có thể điều chỉnh và cài đặt hiển thị nâng cao cho trải nghiệm đọc hoàn hảo.",
"AI Read Aloud": "AI Đọc To",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Thưởng thức việc đọc rảnh tay với giọng AI tự nhiên làm sống động sách của bạn.",
"AI Translations": "Dịch Thuật AI",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Dịch bất kỳ văn bản nào ngay lập tức với sức mạnh của Google, Azure hoặc DeepL—hiểu nội dung bằng mọi ngôn ngữ.",
"Connect with fellow readers and get help fast in our friendly community channels.": "Kết nối với độc giả khác và nhận trợ giúp nhanh chóng trong các kênh cộng đồng thân thiện.",
"Unlimited AI Read Aloud Hours": "Giờ Đọc To AI Không Giới Hạn",
"Listen without limits—convert as much text as you like into immersive audio.": "Nghe không giới hạn—chuyển đổi bao nhiêu văn bản bạn muốn thành âm thanh sống động.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Mở khóa khả năng dịch thuật nâng cao với nhiều lượt sử dụng hàng ngày và tùy chọn nâng cao.",
"DeepL Pro Access": "Truy Cập DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Tận hưởng phản hồi nhanh hơn và hỗ trợ chuyên biệt khi bạn cần giúp đỡ.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Đã đạt hạn ngạch dịch thuật hàng ngày. Nâng cấp gói để tiếp tục sử dụng dịch thuật AI.",
"Includes All Plus Plan Benefits": "Bao Gồm Tất Cả Lợi Ích Gói Plus",
"Early Feature Access": "Truy Cập Tính Năng Sớm",
"Be the first to explore new features, updates, and innovations before anyone else.": "Khám phá tính năng mới, cập nhật và đổi mới trước người khác.",
"Advanced AI Tools": "Công Cụ AI Nâng Cao",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Khai thác công cụ AI mạnh mẽ cho đọc thông minh, dịch thuật và khám phá nội dung.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Dịch tới 100.000 ký tự hàng ngày với công cụ dịch chính xác nhất hiện có.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Dịch tới 500.000 ký tự hàng ngày với công cụ dịch chính xác nhất hiện có.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Lưu trữ và truy cập an toàn toàn bộ bộ sưu tập đọc với tới 5 GB lưu trữ đám mây.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Lưu trữ và truy cập an toàn toàn bộ bộ sưu tập đọc với tới 20 GB lưu trữ đám mây.",
"Deleted cloud backup of the book: {{title}}": "Đã xóa bản sao lưu đám mây của sách: {{title}}",
"Failed to delete cloud backup of the book": "Không thể xóa bản sao lưu đám mây của sách.",
"Are you sure to delete the cloud backup of the selected book?": "Bạn có chắc chắn muốn xóa bản sao lưu đám mây của sách đã chọn không?",
"What's New in Readest": "Những gì mới trong Readest",
"Enter book title": "Nhập tiêu đề sách",
"Subtitle": "Phụ đề",
"Enter book subtitle": "Nhập phụ đề sách",
"Enter author name": "Nhập tên tác giả",
"Series": "Bộ sách",
"Enter series name": "Nhập tên bộ sách",
"Series Index": "Số thứ tự trong bộ",
"Enter series index": "Nhập số thứ tự trong bộ",
"Total in Series": "Tổng số trong bộ",
"Enter total books in series": "Nhập tổng số sách trong bộ",
"Enter publisher": "Nhập nhà xuất bản",
"Publication Date": "Ngày xuất bản",
"Identifier": "Định danh",
"Enter book description": "Nhập mô tả sách",
"Change cover image": "Thay đổi ảnh bìa",
"Replace": "Thay thế",
"Unlock cover": "Mở khóa bìa",
"Lock cover": "Khóa bìa",
"Auto-Retrieve Metadata": "Tự động lấy siêu dữ liệu",
"Auto-Retrieve": "Tự động lấy",
"Unlock all fields": "Mở khóa tất cả trường",
"Unlock All": "Mở khóa tất cả",
"Lock all fields": "Khóa tất cả trường",
"Lock All": "Khóa tất cả",
"Reset": "Đặt lại",
"Edit Metadata": "Chỉnh sửa siêu dữ liệu",
"Locked": "Đã khóa",
"Select Metadata Source": "Chọn nguồn siêu dữ liệu",
"Keep manual input": "Giữ nhập liệu thủ công",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Tiểu thuyết, Khoa học, Lịch sử",
"Select Cover Image": "Chọn ảnh bìa",
"Open Book in New Window": "Mở Sách trong Cửa sổ Mới",
"Voices for {{lang}}": "Giọng nói cho {{lang}}",
"Yandex Translate": "Yandex Dịch",
"YYYY or YYYY-MM-DD": "YYYY hoặc YYYY-MM-DD",
"Restore Purchase": "Khôi phục mua hàng",
"No purchases found to restore.": "Không tìm thấy giao dịch mua nào để khôi phục.",
"Failed to restore purchases.": "Khôi phục giao dịch mua không thành công.",
"Failed to manage subscription.": "Quản lý đăng ký không thành công.",
"Failed to load subscription plans.": "Tải kế hoạch đăng ký không thành công.",
"year": "năm",
"Failed to create checkout session": "Không thể tạo phiên làm việc thanh toán",
"Storage": "Lưu trữ",
"Terms of Service": "Điều khoản dịch vụ",
"Privacy Policy": "Chính sách bảo mật",
"Disable Double Click": "Tắt Nhấp Đôi"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "确认删除",
"Copied to notebook": "已复制到笔记本",
"Copy": "复制",
"Custom CSS": "自定义 CSS",
"Dark Mode": "深色主题",
"Default": "默认",
"Default Font": "默认字体",
@@ -28,7 +27,6 @@
"Dictionary": "词典",
"Download Readest": "下载 Readest",
"Edit": "编辑",
"Enter your custom CSS here...": "在此处输入您的自定义 CSS...",
"Excerpts": "摘录",
"Failed to import book(s): {{filenames}}": "导入书籍失败:{{filenames}}",
"Fast": "快",
@@ -48,7 +46,6 @@
"Horizontal Direction": "横排",
"Hyphenation": "断字",
"Import Books": "导入书籍",
"Language:": "语言",
"Layout": "布局",
"Light Mode": "浅色主题",
"Loading...": "加载中...",
@@ -70,8 +67,8 @@
"Paging Animation": "翻页动画",
"Paragraph": "段落",
"Parallel Read": "并排阅读",
"Published:": "出版日期",
"Publisher:": "出版商",
"Published": "出版日期",
"Publisher": "出版商",
"Reading Progress Synced": "阅读进度已同步",
"Reload Page": "重新加载页面",
"Reveal in File Explorer": "在文件管理器中显示",
@@ -96,7 +93,7 @@
"Slow": "慢",
"Solarized": "日晖",
"Speak": "朗读",
"Subjects:": "主题",
"Subjects": "主题",
"System Fonts": "系统字体",
"TTS not supported in this device": "此设备不支持 TTS",
"Theme Color": "主题颜色",
@@ -105,7 +102,7 @@
"Translated Text": "译文",
"Unknown": "未知",
"Untitled": "无标题",
"Updated:": "更新日期",
"Updated": "更新日期",
"Version {{version}}": "版本 {{version}}",
"Vertical Direction": "竖排",
"Welcome to your library. You can import your books here and read them anytime.": "书库空空如也,您可以导入您的书籍并随时阅读。",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "下载书籍失败:{{title}}",
"Upload Book": "上传书籍",
"Auto Upload Books to Cloud": "自动上传书籍到云端",
"{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 云存储空间",
"Storage": "存储空间",
"Book deleted: {{title}}": "书籍已删除:{{title}}",
"Failed to delete book: {{title}}": "删除书籍失败:{{title}}",
"Check Updates on Start": "启动时检查更新",
@@ -161,7 +156,7 @@
"Create a Password": "创建密码",
"Sign up": "注册",
"Signing up...": "正在注册...",
"Dont have an account? Sign up": "没有账号?注册",
"Don't have an account? Sign up": "没有账号?注册",
"Check your email for the confirmation link": "检查您的电子邮件以获取确认链接",
"Signing in ...": "正在登录...",
"Send a magic link email": "发送魔法链接邮件",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "使用 GitHub 登录",
"Account": "账户",
"Failed to delete user. Please try again later.": "删除用户失败。请稍后重试。",
"Unlimited Offline/Online Reading": "无限离线和在线阅读",
"Unlimited Cloud Sync Devices": "无限云同步设备个数",
"Essential Text-to-Speech Voices": "基础 TTS 语音包",
"DeepL Free Access": "免费版 DeepL 翻译",
"Community Support": "社区支持",
"500 MB Cloud Sync Space": "500 MB 云同步空间",
"Includes All Free Tier Benefits": "包含所有免费版权益",
"AI Summaries": "AI 摘要",
"AI Translations": "AI 翻译",
"Priority Support": "优先支持",
"DeepL Pro Access": "专业版 DeepL 翻译",
"Expanded Text-to-Speech Voices": "更多文本转语音声音",
"2000 MB Cloud Sync Space": "2000 MB 云同步空间",
"Includes All Plus Tier Benefits": "包含所有增强版权益",
"Unlimited AI Summaries": "无限使用 AI 摘要",
"Unlimited AI Translations": "无限使用 AI 翻译",
"Unlimited DeepL Pro Access": "无限使用专业版 DeepL 翻译",
"Advanced AI Tools": "高级 AI 工具",
"Early Feature Access": "抢先体验新功能",
"10 GB Cloud Sync Space": "10 GB 云同步空间",
"Loading profile...": "加载个人资料中...",
"Features": "功能特性",
"Delete Account": "删除账户",
"Delete Your Account?": "删除您的账户?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "此操作无法撤销。您在云端的所有数据将被永久删除。",
"Delete Permanently": "永久删除",
"Free Tier": "免费版",
"Plus Tier": "增强版",
"Pro Tier": "专业版",
"RTL Direction": "从右向左",
"Maximum Column Height": "最大列高",
"Maximum Column Width": "最大列宽",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "正在下载 {{downloaded}} / {{contentLength}}",
"Download finished": "下载完成",
"DOWNLOAD & INSTALL": "下载并安装",
"Changelog:": "更新日志",
"Changelog": "更新日志",
"Software Update": "软件更新",
"Title": "书名",
"Date Read": "阅读日期",
@@ -279,10 +252,9 @@
"Ascending": "升序",
"Descending": "降序",
"Sort by...": "排序方式...",
"Added:": "添加日期",
"Format:": "格式",
"Added": "添加日期",
"GuanKiapTsingKhai-T": "原俠正楷-T",
"Description:": "简介",
"Description": "简介",
"No description available": "暂无简介",
"List": "列表",
"Grid": "网格",
@@ -299,7 +271,7 @@
"Checking for updates...": "检查更新中...",
"Error checking for updates": "检查更新时出错",
"Details": "详情",
"File Size:": "文件大小",
"File Size": "文件大小",
"Auto Detect": "自动检测",
"Next Section": "下一章",
"Previous Section": "上一章",
@@ -345,8 +317,7 @@
"Quota Exceeded": "额度不足",
"{{percentage}}% of Daily Translation Characters Used.": "已使用 {{percentage}}% 的每日翻译字符数",
"Translation Characters": "翻译字数",
"Daily translation quota reached. Select another translate service to proceed.": "已达到每日翻译配额,请选择其他翻译服务继续。",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} 种声音",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} 种语音",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "出了点问题。请不要担心,我们的团队已经收到通知并正在修复中。",
"Error Details:": "错误详情:",
"Try Again": "重试",
@@ -360,5 +331,131 @@
"Right Margin (px)": "右边距",
"Left Margin (px)": "左边距",
"Column Gap (%)": "列间距",
"Always Show Status Bar": "始终显示状态栏"
"Always Show Status Bar": "始终显示状态栏",
"Translation Not Available": "翻译不可用",
"Custom Content CSS": "自定义内容 CSS",
"Enter CSS for book content styling...": "输入书籍内容样式的CSS…",
"Custom Reader UI CSS": "自定义界面 CSS",
"Enter CSS for reader interface styling...": "输入阅读器界面样式的CSS…",
"Crop": "裁剪",
"Book Covers": "书籍封面",
"Fit": "适应",
"Reset {{settings}}": "重置{{settings}}",
"Reset Settings": "重置设置",
"{{count}} pages left in chapter_other": "本章剩余 {{count}} 页",
"Show Remaining Pages": "显示剩余页数",
"Source Han Serif CN VF": "思源宋体",
"Huiwen-mincho": "汇文明朝体",
"KingHwa_OldSong": "京华老宋体",
"Manage Subscription": "管理订阅",
"Coming Soon": "即将上线",
"Upgrade to {{plan}}": "升级到 {{plan}}",
"Upgrade to Plus or Pro": "升级到 Plus 或 Pro",
"Current Plan": "当前套餐",
"Plan Limits": "套餐限制",
"Processing your payment...": "正在处理付款...",
"Please wait while we confirm your subscription.": "请稍候,我们正在确认您的订阅。",
"Payment Processing": "付款处理中",
"Your payment is being processed. This usually takes a few moments.": "您的付款正在处理中,通常需要几秒钟。",
"Payment Failed": "付款失败",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "我们无法处理您的订阅。请重试,或如问题持续请联系支持。",
"Back to Profile": "返回个人资料",
"Subscription Successful!": "订阅成功!",
"Thank you for your subscription! Your payment has been processed successfully.": "感谢您的订阅!您的付款已成功处理。",
"Email:": "邮箱:",
"Plan:": "套餐:",
"Amount:": "金额:",
"Subscription ID:": "订阅 ID",
"Go to Library": "前往书库",
"Need help? Contact our support team at support@readest.com": "需要帮助?请联系支持团队:support@readest.com",
"Free Plan": "免费套餐",
"month": "月",
"AI Translations (per day)": "AI 翻译(每日)",
"Plus Plan": "Plus 套餐",
"Includes All Free Plan Benefits": "包含所有免费套餐功能",
"Pro Plan": "Pro 套餐",
"More AI Translations": "更多 AI 翻译",
"Complete Your Subscription": "完成您的订阅",
"{{percentage}}% of Cloud Sync Space Used.": "已使用 {{percentage}}% 的云同步空间。",
"Cloud Sync Storage": "云同步空间",
"Disable": "禁用",
"Enable": "启用",
"Upgrade to Readest Premium": "升级到 Readest Premium",
"Show Source Text": "显示原文",
"Cross-Platform Sync": "跨平台同步",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "在所有设备间无缝同步您的书库、进度、标注和笔记——再也不会丢失阅读位置。",
"Customizable Reading": "个性化阅读",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "通过可调节的字体、布局、主题和高级显示设置个性化每个细节,获得完美阅读体验。",
"AI Read Aloud": "AI 朗读",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "享受解放双手的阅读体验,使用自然流畅的 AI 语音让书籍栩栩如生。",
"AI Translations": "AI 翻译",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "借助 Google、Azure 或 DeepL 的强大功能即时翻译任何语言的内容。",
"Connect with fellow readers and get help fast in our friendly community channels.": "与其他读者交流,在我们友好的社区频道中快速获得帮助。",
"Unlimited AI Read Aloud Hours": "无限 AI 朗读时长",
"Listen without limits—convert as much text as you like into immersive audio.": "将任意数量的文本转换为沉浸式音频。",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "增强翻译功能、更多翻译用量和高级选项。",
"DeepL Pro Access": "DeepL Pro 访问权限",
"Enjoy faster responses and dedicated assistance whenever you need help.": "享受更快响应和专属帮助,随时获得支持。",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "已达每日翻译配额。升级套餐以继续使用 AI 翻译。",
"Includes All Plus Plan Benefits": "包含所有 Plus 套餐功能",
"Early Feature Access": "抢先体验新功能",
"Be the first to explore new features, updates, and innovations before anyone else.": "抢先体验新功能、更新和创新。",
"Advanced AI Tools": "高级 AI 工具",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "利用强大的 AI 工具实现智能阅读、翻译和内容发现。",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "使用最精准的翻译引擎每天可翻译10万字。",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "使用最精准的翻译引擎每天可翻译50万字。",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "使用多达 5GB 的安全云存储保存和访问您的整个阅读收藏。",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "使用多达 20GB 的安全云存储保存和访问您的整个阅读收藏。",
"Deleted cloud backup of the book: {{title}}": "已删除书籍云备份:{{title}}",
"Failed to delete cloud backup of the book": "无法删除书籍云备份。",
"Are you sure to delete the cloud backup of the selected book?": "您确定要删除所选书籍的云备份吗?",
"What's New in Readest": "Readest 更新功能",
"Enter book title": "输入书名",
"Subtitle": "副标题",
"Enter book subtitle": "输入书籍副标题",
"Enter author name": "输入作者姓名",
"Series": "系列",
"Enter series name": "输入系列名称",
"Series Index": "系列编号",
"Enter series index": "输入系列编号",
"Total in Series": "系列总数",
"Enter total books in series": "输入系列中书籍总数",
"Enter publisher": "输入出版社",
"Publication Date": "出版日期",
"Identifier": "标识符",
"Enter book description": "输入书籍描述",
"Change cover image": "替换封面图片",
"Replace": "替换",
"Unlock cover": "解锁封面",
"Lock cover": "锁定封面",
"Auto-Retrieve Metadata": "自动获取元数据",
"Auto-Retrieve": "自动获取",
"Unlock all fields": "解锁所有字段",
"Unlock All": "全部解锁",
"Lock all fields": "锁定所有字段",
"Lock All": "全部锁定",
"Reset": "重置",
"Edit Metadata": "编辑元数据",
"Locked": "已锁定",
"Select Metadata Source": "选择元数据来源",
"Keep manual input": "保留手动输入",
"Google Books": "谷歌图书",
"Open Library": "开放图书馆",
"Fiction, Science, History": "小说,科学,历史",
"Select Cover Image": "选择封面图片",
"Open Book in New Window": "在新窗口中打开书籍",
"Voices for {{lang}}": "{{lang}} 的语音",
"Yandex Translate": "Yandex 翻译",
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD",
"Restore Purchase": "恢复购买",
"No purchases found to restore.": "未找到可恢复的购买。",
"Failed to restore purchases.": "恢复购买失败。",
"Failed to manage subscription.": "管理订阅失败。",
"Failed to load subscription plans.": "加载订阅计划失败。",
"year": "年",
"Failed to create checkout session": "创建结账会话失败",
"Storage": "云同步空间",
"Terms of Service": "服务条款",
"Privacy Policy": "隐私政策",
"Disable Double Click": "禁用双击"
}
@@ -18,7 +18,6 @@
"Confirm Deletion": "確認刪除",
"Copied to notebook": "已複製到筆記本",
"Copy": "複製",
"Custom CSS": "自定義 CSS",
"Dark Mode": "深色主題",
"Default": "預設",
"Default Font": "預設字體",
@@ -28,7 +27,6 @@
"Dictionary": "詞典",
"Download Readest": "下載 Readest",
"Edit": "編輯",
"Enter your custom CSS here...": "在此處輸入您的自定義 CSS...",
"Excerpts": "摘錄",
"Failed to import book(s): {{filenames}}": "導入書籍失敗:{{filenames}}",
"Fast": "快",
@@ -48,7 +46,6 @@
"Horizontal Direction": "橫排",
"Hyphenation": "斷字",
"Import Books": "導入書籍",
"Language:": "語言",
"Layout": "版面",
"Light Mode": "淺色主題",
"Loading...": "載入中...",
@@ -70,8 +67,8 @@
"Paging Animation": "翻頁動畫",
"Paragraph": "段落",
"Parallel Read": "並排閱讀",
"Published:": "出版日期",
"Publisher:": "出版商",
"Published": "出版日期",
"Publisher": "出版商",
"Reading Progress Synced": "閱讀進度已同步",
"Reload Page": "重新載入頁面",
"Reveal in File Explorer": "在檔案管理器中顯示",
@@ -96,7 +93,7 @@
"Slow": "慢",
"Solarized": "日暉",
"Speak": "朗讀",
"Subjects:": "主題",
"Subjects": "主題",
"System Fonts": "系統字體",
"TTS not supported in this device": "此設備不支持 TTS",
"Theme Color": "主題顏色",
@@ -105,7 +102,7 @@
"Translated Text": "譯文",
"Unknown": "未知",
"Untitled": "無標題",
"Updated:": "更新日期",
"Updated": "更新日期",
"Version {{version}}": "版本 {{version}}",
"Vertical Direction": "豎排",
"Welcome to your library. You can import your books here and read them anytime.": "書庫空空如也,您可以導入您的書籍並隨時閱讀。",
@@ -128,8 +125,6 @@
"Failed to download book: {{title}}": "書籍下載失敗:{{title}}",
"Upload Book": "上傳書籍",
"Auto Upload Books to Cloud": "自動上傳書籍到雲端",
"{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 雲存儲空間",
"Storage": "存儲空間",
"Book deleted: {{title}}": "書籍已刪除:{{title}}",
"Failed to delete book: {{title}}": "書籍刪除失敗:{{title}}",
"Check Updates on Start": "啟動時檢查更新",
@@ -161,7 +156,7 @@
"Create a Password": "創建密碼",
"Sign up": "註冊",
"Signing up...": "正在註冊...",
"Dont have an account? Sign up": "沒有帳號?註冊",
"Don't have an account? Sign up": "沒有帳號?註冊",
"Check your email for the confirmation link": "檢查您的電子郵件以獲取確認連結",
"Signing in ...": "正在登入...",
"Send a magic link email": "發送魔法連結郵件",
@@ -185,35 +180,13 @@
"Sign in with GitHub": "使用 GitHub 登入",
"Account": "帳戶",
"Failed to delete user. Please try again later.": "刪除使用者失敗。請稍後再試。",
"Unlimited Offline/Online Reading": "無限離線/線上閱讀",
"Unlimited Cloud Sync Devices": "無限雲端同步裝置",
"Essential Text-to-Speech Voices": "基本文字轉語音聲音",
"DeepL Free Access": "DeepL免費存取",
"Community Support": "社群支援",
"500 MB Cloud Sync Space": "500 MB雲端同步空間",
"Includes All Free Tier Benefits": "包含所有免費方案權益",
"AI Summaries": "AI摘要",
"AI Translations": "AI翻譯",
"Priority Support": "優先支援",
"DeepL Pro Access": "DeepL專業版存取",
"Expanded Text-to-Speech Voices": "擴展文字轉語音聲音",
"2000 MB Cloud Sync Space": "2000 MB雲端同步空間",
"Includes All Plus Tier Benefits": "包含所有Plus方案權益",
"Unlimited AI Summaries": "無限AI摘要",
"Unlimited AI Translations": "無限AI翻譯",
"Unlimited DeepL Pro Access": "無限DeepL專業版存取",
"Advanced AI Tools": "進階AI工具",
"Early Feature Access": "搶先體驗新功能",
"10 GB Cloud Sync Space": "10 GB雲端同步空間",
"Loading profile...": "載入個人資料中...",
"Features": "功能",
"Delete Account": "刪除帳戶",
"Delete Your Account?": "刪除您的帳戶?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "此操作無法撤銷。您在雲端的所有資料將被永久刪除。",
"Delete Permanently": "永久刪除",
"Free Tier": "免費方案",
"Plus Tier": "Plus方案",
"Pro Tier": "專業方案",
"RTL Direction": "從右向左",
"Maximum Column Height": "最大列高",
"Maximum Column Width": "最大列寬",
@@ -270,7 +243,7 @@
"Downloading {{downloaded}} of {{contentLength}}": "正在下載 {{downloaded}} / {{contentLength}}",
"Download finished": "下載完成",
"DOWNLOAD & INSTALL": "下載並安裝",
"Changelog:": "更新日誌:",
"Changelog": "更新日誌:",
"Software Update": "軟體更新",
"Title": "書名",
"Date Read": "閱讀日期",
@@ -279,10 +252,9 @@
"Ascending": "升序",
"Descending": "降序",
"Sort by...": "排序方式...",
"Added:": "添加日期",
"Format:": "格式",
"Added": "添加日期",
"GuanKiapTsingKhai-T": "原俠正楷-T",
"Description:": "簡介",
"Description": "簡介",
"No description available": "暫無簡介",
"List": "列表",
"Grid": "網格",
@@ -299,7 +271,7 @@
"Checking for updates...": "檢查更新中...",
"Error checking for updates": "檢查更新時出錯",
"Details": "詳情",
"File Size:": "檔案大小",
"File Size": "檔案大小",
"Auto Detect": "自動檢測",
"Next Section": "下一章",
"Previous Section": "上一章",
@@ -345,7 +317,6 @@
"Quota Exceeded": "額度不足",
"{{percentage}}% of Daily Translation Characters Used.": "已使用 {{percentage}}% 的每日翻譯字數",
"Translation Characters": "翻譯字數",
"Daily translation quota reached. Select another translate service to proceed.": "已達到每日翻譯字數限制,請選擇其他翻譯服務繼續。",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} 種語音",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "出了點問題。別擔心,我們的團隊已經收到通知,正在努力修復。",
"Error Details:": "錯誤詳情:",
@@ -360,5 +331,131 @@
"Right Margin (px)": "右邊距",
"Left Margin (px)": "左邊距",
"Column Gap (%)": "欄間距",
"Always Show Status Bar": "始終顯示狀態欄"
"Always Show Status Bar": "始終顯示狀態欄",
"Translation Not Available": "翻譯不可用",
"Custom Content CSS": "自訂內容 CSS",
"Enter CSS for book content styling...": "輸入書籍內容樣式的CSS…",
"Custom Reader UI CSS": "自訂介面 CSS",
"Enter CSS for reader interface styling...": "輸入閱讀器介面樣式的CSS…",
"Crop": "裁剪",
"Book Covers": "書籍封面",
"Fit": "適應",
"Reset {{settings}}": "重置{{settings}}",
"Reset Settings": "重置設置",
"{{count}} pages left in chapter_other": "本章剩餘 {{count}} 頁",
"Show Remaining Pages": "顯示剩餘頁數",
"Source Han Serif CN VF": "思源宋體",
"Huiwen-mincho": "匯文明朝體",
"KingHwa_OldSong": "京華老宋體",
"Manage Subscription": "管理訂閱",
"Coming Soon": "即將推出",
"Upgrade to {{plan}}": "升級到 {{plan}}",
"Upgrade to Plus or Pro": "升級到 Plus 或 Pro",
"Current Plan": "當前方案",
"Plan Limits": "方案限制",
"Processing your payment...": "正在處理付款...",
"Please wait while we confirm your subscription.": "請稍候,我們正在確認您的訂閱。",
"Payment Processing": "付款處理中",
"Your payment is being processed. This usually takes a few moments.": "您的付款正在處理中,通常需要幾秒鐘。",
"Payment Failed": "付款失敗",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "我們無法處理您的訂閱。請重試,或若問題持續,請聯絡客服。",
"Back to Profile": "返回個人資料",
"Subscription Successful!": "訂閱成功!",
"Thank you for your subscription! Your payment has been processed successfully.": "感謝您的訂閱!您的付款已成功完成。",
"Email:": "電子郵件:",
"Plan:": "方案:",
"Amount:": "金額:",
"Subscription ID:": "訂閱 ID",
"Go to Library": "前往書庫",
"Need help? Contact our support team at support@readest.com": "需要協助?請聯絡支援團隊:support@readest.com",
"Free Plan": "免費方案",
"month": "月",
"AI Translations (per day)": "AI 翻譯(每日)",
"Plus Plan": "Plus 方案",
"Includes All Free Plan Benefits": "包含所有免費方案功能",
"Pro Plan": "Pro 方案",
"More AI Translations": "更多 AI 翻譯",
"Complete Your Subscription": "完成您的訂閱",
"{{percentage}}% of Cloud Sync Space Used.": "已使用 {{percentage}}% 的雲同步空間。",
"Cloud Sync Storage": "雲同步空間",
"Disable": "禁用",
"Enable": "啟用",
"Upgrade to Readest Premium": "升級到 Readest Premium",
"Show Source Text": "顯示原文",
"Cross-Platform Sync": "跨平台同步",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "在所有裝置間無縫同步您的書庫、閱讀進度、標註和筆記——再也不會遺失閱讀位置。",
"Customizable Reading": "個人化閱讀",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "透過可調節的字體、版面配置、主題和進階顯示設定個人化每個細節,獲得完美閱讀體驗。",
"AI Read Aloud": "AI 朗讀",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "享受免手持的閱讀體驗,透過自然流暢的 AI 聲音讓書籍栩栩如生。",
"AI Translations": "AI 翻譯",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "借助 Google、Azure 或 DeepL 的強大功能即時翻譯任何文字——理解任何語言的內容。",
"Connect with fellow readers and get help fast in our friendly community channels.": "與其他讀者交流,在我們友善的社群頻道中快速獲得幫助。",
"Unlimited AI Read Aloud Hours": "無限 AI 朗讀時長",
"Listen without limits—convert as much text as you like into immersive audio.": "無限制聆聽——將任意數量的文字轉換為沉浸式音訊。",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "解鎖增強翻譯功能,享受更多翻譯用量和進階選項。",
"DeepL Pro Access": "DeepL Pro 存取權限",
"Enjoy faster responses and dedicated assistance whenever you need help.": "享受更快回應和專屬幫助,隨時獲得所需支援。",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "已達每日翻譯配額。升級方案以繼續使用 AI 翻譯。",
"Includes All Plus Plan Benefits": "包含所有 Plus 方案優惠",
"Early Feature Access": "搶先體驗新功能",
"Be the first to explore new features, updates, and innovations before anyone else.": "搶先體驗新功能、更新和創新。",
"Advanced AI Tools": "進階 AI 工具",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "運用強大的 AI 工具實現智慧閱讀、翻譯和內容探索。",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "使用最精準的翻譯引擎每天翻譯多達10萬字元。",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "使用最精準的翻譯引擎每天翻譯多達50萬字元。",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "使用多達 5GB 的安全雲端儲存保存和存取您的整個閱讀收藏。",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "使用多達 20GB 的安全雲端儲存保存和存取您的整個閱讀收藏。",
"Deleted cloud backup of the book: {{title}}": "已刪除書籍的雲端備份:{{title}}",
"Failed to delete cloud backup of the book": "無法刪除書籍的雲端備份。",
"Are you sure to delete the cloud backup of the selected book?": "您確定要刪除所選書籍的雲端備份嗎?",
"What's New in Readest": "Readest 更新功能",
"Enter book title": "輸入書名",
"Subtitle": "副標題",
"Enter book subtitle": "輸入書籍副標題",
"Enter author name": "輸入作者姓名",
"Series": "系列",
"Enter series name": "輸入系列名稱",
"Series Index": "系列編號",
"Enter series index": "輸入系列編號",
"Total in Series": "系列總數",
"Enter total books in series": "輸入系列中書籍總數",
"Enter publisher": "輸入出版社",
"Publication Date": "出版日期",
"Identifier": "識別碼",
"Enter book description": "輸入書籍描述",
"Change cover image": "替換封面圖片",
"Replace": "替換",
"Unlock cover": "解鎖封面",
"Lock cover": "鎖定封面",
"Auto-Retrieve Metadata": "自動擷取中繼資料",
"Auto-Retrieve": "自動擷取",
"Unlock all fields": "解鎖所有欄位",
"Unlock All": "全部解鎖",
"Lock all fields": "鎖定所有欄位",
"Lock All": "全部鎖定",
"Reset": "重設",
"Edit Metadata": "編輯中繼資料",
"Locked": "已鎖定",
"Select Metadata Source": "選擇中繼資料來源",
"Keep manual input": "保留手動輸入",
"Google Books": "Google 圖書",
"Open Library": "開放圖書館",
"Fiction, Science, History": "小說,科學,歷史",
"Select Cover Image": "選擇封面圖片",
"Open Book in New Window": "在新視窗中打開書籍",
"Voices for {{lang}}": "{{lang}} 的語音",
"Yandex Translate": "Yandex 翻譯",
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD",
"Restore Purchase": "恢復購買",
"No purchases found to restore.": "未找到可恢復的購買。",
"Failed to restore purchases.": "恢復購買失敗。",
"Failed to manage subscription.": "管理訂閱失敗。",
"Failed to load subscription plans.": "加載訂閱計劃失敗。",
"year": "年",
"Failed to create checkout session": "創建結帳會話失敗",
"Storage": "雲同步空間",
"Terms of Service": "服務條款",
"Privacy Policy": "隱私政策",
"Disable Double Click": "禁用雙擊"
}
+124 -1
View File
@@ -1,5 +1,128 @@
{
"releases": {
"0.9.69": {
"date": "2025-08-02",
"notes": [
"Updater: Fixed permission issue when updating in standalone reader window",
"Config: Added option to disable doubleclick actions",
"EPUB: Fixed missing images in some EPUB files",
"EPUB: Corrected formatting for book subjects",
"TXT: Improved language detection when parsing TXT files",
"CSS: Resolved issue where custom font colors could not be applied",
"Layout: Corrected cover image width display in 'Fit' mode for list view",
"Layout: Fixed unintended scrolling in reader view when highlighting text"
]
},
"0.9.68": {
"date": "2025-07-30",
"notes": [
"Reader: Files now open in a new window by default on Desktop",
"Translation: Skips translating content inside <pre>, <code>, and <math> tags",
"Performance: Improved multi-part downloading for large books",
"Sync: Resolved issue preventing some PDF files from syncing",
"CSS: Resolved issue with fixed font color in some EPUB files",
"TXT: Enhanced TXT parsing for better compatibility"
]
},
"0.9.67": {
"date": "2025-07-22",
"notes": [
"Reader: Fixed file opening issue when filenames contain commas",
"Translation: Added Yandex Translator as a new translation provider",
"Layout: Hovering over header now reveals macOS traffic light window controls",
"Theme: Reader background color is now customizable for EPUB and PDF",
"Library: Search now includes book format, group names, and descriptions",
"Library: Various improvements to metadata editor and bookshelf management",
"Linux: Fixed social login failure on Linux systems"
]
},
"0.9.66": {
"date": "2025-07-21",
"notes": [
"Layout: Fixed nested table of contents items not expanding in long TOC lists",
"Reader: Opening files from the OS now takes highest priority",
"TTS: Improved voice filtering for some language codes",
"App: Fixed loading issues on older browsers",
"App: Fixed wrong MIME types generated on Linux"
]
},
"0.9.65": {
"date": "2025-07-19",
"notes": [
"Reader: Added support for opening multiple reader windows on desktop",
"Layout: Added visible window borders on Linux",
"TTS: Improved handling of invalid book language",
"TTS: Skipped reading footnote anchor links",
"Translation: Fixed an issue where table of contents entries were not translated",
"Sync: Edited book metadata is now included in sync across devices",
"iOS: Disabled context menu when long-pressing on book covers"
]
},
"0.9.64": {
"date": "2025-07-16",
"notes": [
"Library: Added support to edit book metadata",
"Layout: Fixed bookmark icon placement when the sidebar is pinned",
"Reader: Fixed an issue where links to other chapters in EPUB files did not work",
"Reader: Lightened the highlight color in dark mode for better contrast",
"Reader: Pull-to-refresh gesture is now smoother and more responsive",
"Reader: Removed unintended indentation on images",
"Search: Improved responsiveness of full-text search for faster results"
]
},
"0.9.63": {
"date": "2025-07-07",
"notes": [
"Reader: Fixed links not working in some MOBI books",
"TTS: Fixed an issue where some text was skipped during reading",
"Library: You can now delete only the cloud backup of a book without removing the local copy",
"Performance: Fonts in the font list now load only when displayed for faster startup",
"Performance: Opening large MOBI books is now significantly faster",
"Layout: Fixed overlay scrollbar display in longer TOC items list",
"Layout: Improved grid cover image height in crop mode",
"Layout: Removed unsupported screen orientation lock option on iPad",
"Layout: System context menu no longer appears when selecting text on iPad",
"Translation: Added Thai (th-TH) language support"
]
},
"0.9.62": {
"date": "2025-07-02",
"notes": [
"Subscription: You can now upgrade to Readest Premium and manage your subscription",
"Reader: Added an option to toggle Parallel Reading when viewing multiple books",
"Translation: Added an option to show or hide the original text in translations",
"Layout: TOC on Android now uses an overlay scrollbar for easier navigation",
"Layout: Custom CSS can now also be applied to the library page",
"Fonts: Added three new CJK fonts to choose from",
"Shortcuts: Press ESC to quickly close the search bar"
]
},
"0.9.61": {
"date": "2025-06-26",
"notes": [
"Layout: Fixed view menu width on smaller screens",
"Layout: Added option to crop or fit book covers in library view",
"Reader: You can now apply custom CSS to the reading interface (UI)",
"Reader: Added an option to show remaining pages in the current chapter",
"Settings: Added options to reset settings from the config dialog",
"Import: Fixed encoded filenames when importing TXT files on iOS",
"Linux: Fixed missing app icon on some Linux distributions",
"GitHub: Added donation link to support Readest development"
]
},
"0.9.60": {
"date": "2025-06-22",
"notes": [
"Layout has been improved for iPad devices",
"Screen orientation changes are now smoother on iOS",
"Importing books on iOS during the first launch is now more reliable",
"Font size no longer changes unexpectedly in scrolled mode on iOS",
"Top and bottom safe insets are now ignored when no header or footer is present",
"Interactive books now load correctly on the Desktop platform",
"Translation is now disabled when the primary language is undefined",
"Android: the task list background color has been updated with current theme color"
]
},
"0.9.59": {
"date": "2025-06-19",
"notes": [
@@ -229,7 +352,7 @@
"0.9.30": {
"date": "2025-04-05",
"notes": [
"Added fixed storage quata when running the app in self-hosted mode",
"Added fixed storage quota when running the app in self-hosted mode",
"Link colors will only change when you override fonts",
"Added cache for native files for better performance"
]
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
set -e
VERSION=$(jq -r '.version' package.json)
if [[ -z "$VERSION" ]]; then
echo "❌ Failed to extract version from package.json"
exit 1
fi
echo "📦 Found version: $VERSION"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" ]]; then
echo "❌ Invalid version format: $VERSION"
exit 1
fi
# Convert x.y.z => x * 10000 + y * 1000 + z
VERSION_CODE=$((10#$MAJOR * 10000 + 10#$MINOR * 1000 + 10#$PATCH))
echo "🔢 Computed versionCode: $VERSION_CODE"
PROPERTIES_FILE="./src-tauri/gen/android/app/tauri.properties"
if [[ ! -f "$PROPERTIES_FILE" ]]; then
echo "❌ File not found: $PROPERTIES_FILE"
exit 1
fi
tmpfile=$(mktemp)
sed "s/^tauri\.android\.versionName=.*/tauri.android.versionName=$VERSION/" "$PROPERTIES_FILE" | \
sed "s/^tauri\.android\.versionCode=.*/tauri.android.versionCode=$VERSION_CODE/" > "$tmpfile"
mv "$tmpfile" "$PROPERTIES_FILE"
echo "✅ Updated $PROPERTIES_FILE"
echo "🚀 Running: pnpm tauri android build"
pnpm tauri android build
source .env.google-play.local
if [[ -z "$GOOGLE_PLAY_JSON_KEY_FILE" ]]; then
echo "❌ GOOGLE_PLAY_JSON_KEY_FILE is not set"
exit 1
fi
cd ../../
fastlane android upload_production
+21 -17
View File
@@ -14,8 +14,12 @@ rust-version = "1.77.2"
name = "readestlib"
crate-type = ["staticlib", "cdylib", "lib"]
[features]
# Internal feature to suppress warnings from old objc crate
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.1.1", features = [] }
tauri-build = { version = "2", features = [] }
[dependencies]
serde_json = "1.0"
@@ -25,26 +29,26 @@ thiserror = "2"
tokio = { version = "1", features = ["fs"] }
tokio-util = { version = "0.7", features = ["codec"] }
futures-util = "0.3"
futures = "0.3.31"
read-progress-stream = "1.0.0"
reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
] }
# FIXME: remove the devtools feature in production
tauri = { version = "2.5.1", features = [ "protocol-asset", "devtools"] }
tauri-build = "2.1.1"
tauri-plugin-log = "2.4.0"
tauri-plugin-fs = "2.2.1"
tauri-plugin-dialog = "2.2.1"
tauri-plugin-os = "2.2.1"
tauri-plugin-http = "2.4.3"
tauri-plugin-shell = "2.2.1"
tauri-plugin-process = "2.2.1"
tauri = { version = "2.5.1", features = [ "protocol-asset" ] }
tauri-build = "2"
tauri-plugin-log = "2"
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
tauri-plugin-os = "2"
tauri-plugin-http = "2"
tauri-plugin-shell = "2"
tauri-plugin-process = "2"
tauri-plugin-oauth = "2"
tauri-plugin-opener = "2.2.6"
tauri-plugin-deep-link = "2.2.1"
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2"
tauri-plugin-sign-in-with-apple = "1.0.2"
tauri-plugin-haptics = "2.2.4"
tauri-plugin-haptics = "2"
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
@@ -61,6 +65,6 @@ objc2-foundation = { version = "0.3", features = ["NSError", "NSArray"] }
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
tauri-plugin-cli = "2"
tauri-plugin-single-instance = "2.2.3"
tauri-plugin-updater = "2.7.0"
tauri-plugin-window-state = "2.2.2"
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"
tauri-plugin-window-state = "2"
@@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": ["main", "updater"],
"windows": ["main", "updater", "reader-*"],
"permissions": [
"core:default",
"fs:default",
@@ -43,7 +43,18 @@
"identifier": "fs:allow-cache-read",
"allow": [
{
"path": "$CACHEDIR/**/*"
"path": "$APPCACHE/**/*"
},
{
"path": "/private/var/mobile/Containers/Data/Application/**/*"
}
]
},
{
"identifier": "fs:allow-cache-write",
"allow": [
{
"path": "$APPCACHE/**/*"
},
{
"path": "/private/var/mobile/Containers/Data/Application/**/*"
@@ -54,17 +65,23 @@
"identifier": "http:default",
"allow": [
{
"url": "https://*.deepl.com"
"url": "https://*.readest.com"
},
{
"url": "https://github.com/readest/*"
},
{
"url": "https://*.deepl.com"
},
{
"url": "https://*.cloudflarestorage.com"
},
{
"url": "https://edge.microsoft.com"
},
{
"url": "https://translate.toil.cc"
},
{
"url": "https://*.microsofttranslator.com"
},
@@ -88,6 +105,7 @@
"core:window:allow-toggle-maximize",
"core:window:allow-set-fullscreen",
"core:window:allow-set-always-on-top",
"core:window:allow-destroy",
"core:webview:allow-create-webview-window",
"core:path:allow-resolve-directory",
"log:default",
@@ -1,7 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"windows": ["main", "updater"],
"windows": ["main", "updater", "reader-*"],
"platforms": ["linux", "macOS", "windows"],
"permissions": ["updater:default", "cli:default"]
}
@@ -15,28 +15,29 @@ val tauriProperties = Properties().apply {
}
android {
compileSdk = 34
compileSdk = 36
namespace = "com.bilingify.readest"
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.bilingify.readest"
minSdk = 26
targetSdk = 34
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
create("release") {
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
if (keystorePropertiesFile.exists()) {
create("signing") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["password"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["password"] as String
}
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["password"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["password"] as String
}
}
buildTypes {
@@ -45,14 +46,20 @@ android {
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("signing")
}
packaging {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("signing")
}
}
}
kotlinOptions {
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
@@ -10,10 +9,11 @@
<application
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:largeHeap="true"
android:label="@string/app_name"
android:theme="@style/Theme.readest"
android:hardwareAccelerated="true"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
@@ -42,15 +42,15 @@
<data android:mimeType="application/zip" />
<data android:mimeType="text/plain" />
<data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.pdf" />
<data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.fb2.zip" />
<data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.azw" />
<data android:pathPattern=".*\\.cbz" />
<data android:pathPattern=".*\\.zip" />
<data android:pathPattern=".*\\.txt" />
<data android:pathPattern="/.*\\.epub" />
<data android:pathPattern="/.*\\.pdf" />
<data android:pathPattern="/.*\\.fb2" />
<data android:pathPattern="/.*\\.fb2.zip" />
<data android:pathPattern="/.*\\.mobi" />
<data android:pathPattern="/.*\\.azw" />
<data android:pathPattern="/.*\\.cbz" />
<data android:pathPattern="/.*\\.zip" />
<data android:pathPattern="/.*\\.txt" />
</intent-filter>
<intent-filter android:label="oauth">
@@ -70,10 +70,13 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="web.readest.com" />
</intent-filter>
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</activity>
@@ -1,10 +1,14 @@
package com.bilingify.readest
import android.os.Build
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.view.KeyEvent
import android.webkit.WebView
import android.util.Log
import android.graphics.Color
import android.app.ActivityManager
import android.content.res.Configuration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@@ -77,5 +81,15 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setTaskDescription(
ActivityManager.TaskDescription(
getString(R.string.app_name),
null,
Color.TRANSPARENT
)
)
}
}
}
@@ -2,6 +2,9 @@
<!-- Base application theme. -->
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowSplashScreenBackground">@android:color/transparent</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="31">@null</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">false</item>
@@ -9,13 +9,13 @@ exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
links = "tauri-plugin-native-bridge"
[dependencies]
tauri = { version = "2.3.1" }
tauri = { version = "2" }
serde = "1.0"
thiserror = "2"
schemars = "0.8"
[build-dependencies]
tauri-plugin = { version = "2.0.4", features = ["build"] }
tauri-plugin = { version = "2", features = ["build"] }
schemars = "0.8"
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
@@ -5,7 +5,7 @@ plugins {
android {
namespace = "com.readest.native_bridge"
compileSdk = 34
compileSdk = 36
defaultConfig {
minSdk = 21
@@ -9,6 +9,10 @@ const COMMANDS: &[&str] = &[
"get_sys_fonts_list",
"intercept_keys",
"lock_screen_orientation",
"iap_initialize",
"iap_fetch_products",
"iap_purchase_product",
"iap_restore_purchases",
];
fn main() {
@@ -3,6 +3,7 @@ import AuthenticationServices
import CoreText
import MediaPlayer
import ObjectiveC
import StoreKit
import SwiftRs
import Tauri
import UIKit
@@ -48,6 +49,37 @@ class LockScreenOrientationRequestArgs: Decodable {
let orientation: String?
}
struct InitializeRequest: Decodable {
let publicKey: String?
}
struct FetchProductsRequest: Decodable {
let productIds: [String]
}
struct PurchaseProductRequest: Decodable {
let productId: String
}
struct ProductData: Codable {
let id: String
let title: String
let description: String
let price: String
let priceCurrencyCode: String?
let priceAmountMicros: Int64
let productType: String
}
struct PurchaseData: Codable {
let productId: String
let transactionId: String
let originalTransactionId: String
let purchaseDate: String
let purchaseState: String
let platform: String
}
class VolumeKeyHandler: NSObject {
private var audioSession: AVAudioSession?
private var originalVolume: Float = 0.0
@@ -154,9 +186,8 @@ class VolumeKeyHandler: NSObject {
class NativeBridgePlugin: Plugin {
private var webView: WKWebView?
private var authSession: ASWebAuthenticationSession?
private var isOrientationLocked = false
private var currentOrientationMask: UIInterfaceOrientationMask = .all
private var orientationObserver: NSObjectProtocol?
private var originalDelegate: UIApplicationDelegate?
@objc public override func load(webview: WKWebView) {
self.webView = webview
@@ -175,16 +206,19 @@ class NativeBridgePlugin: Plugin {
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
if let app = UIApplication.value(forKey: "sharedApplication") as? UIApplication {
self.originalDelegate = app.delegate
app.delegate = self
} else {
Logger.error("NativeBridgePlugin: Failed to get shared application")
}
}
@objc func appDidBecomeActive() {
if volumeKeyHandler != nil {
activateVolumeKeyInterception()
}
if orientationObserver != nil {
self.setupOrientationObserver()
}
}
@objc func appDidEnterBackground() {
@@ -360,18 +394,11 @@ class NativeBridgePlugin: Plugin {
let orientation = args.orientation ?? "auto"
switch orientation.lowercased() {
case "portrait":
self.isOrientationLocked = true
self.currentOrientationMask = .portrait
self.forceOrientation(.portrait)
self.setupOrientationObserver()
self.changeOrientation(.portrait)
case "landscape":
self.isOrientationLocked = true
self.currentOrientationMask = .landscape
self.forceOrientation(.landscapeRight)
self.setupOrientationObserver()
self.changeOrientation(.landscape)
case "auto":
self.isOrientationLocked = false
self.currentOrientationMask = .all
self.changeOrientation(.all)
default:
invoke.reject("Invalid orientation mode")
return
@@ -381,41 +408,129 @@ class NativeBridgePlugin: Plugin {
}
}
private func forceOrientation(_ orientation: UIInterfaceOrientation) {
private func changeOrientation(_ orientationMask: UIInterfaceOrientationMask) {
self.currentOrientationMask = orientationMask
if #available(iOS 16.0, *) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
let orientationMask: UIInterfaceOrientationMask =
orientation.isPortrait ? .portrait : .landscape
for window in windowScene.windows {
if let rootVC = window.rootViewController {
rootVC.setNeedsUpdateOfSupportedInterfaceOrientations()
}
}
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
print("Orientation update error: \(error.localizedDescription)")
if orientationMask == .all {
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .all)) { error in
logger.error("Orientation update error: \(error.localizedDescription)")
DispatchQueue.main.async {
UIViewController.attemptRotationToDeviceOrientation()
}
}
} else {
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
logger.error("Orientation update error: \(error.localizedDescription)")
}
}
}
} else {
UIDevice.current.setValue(orientation.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
if orientationMask == .all {
UIViewController.attemptRotationToDeviceOrientation()
} else {
let specificOrientation: UIInterfaceOrientation
if orientationMask.contains(.portrait) {
specificOrientation = .portrait
} else if orientationMask.contains(.landscape) {
let currentOrientation = UIDevice.current.orientation
if currentOrientation == .landscapeLeft {
specificOrientation = .landscapeRight
} else if currentOrientation == .landscapeRight {
specificOrientation = .landscapeLeft
} else {
specificOrientation = .landscapeRight
}
} else {
specificOrientation = .portrait
}
UIDevice.current.setValue(specificOrientation.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
}
}
}
private func setupOrientationObserver() {
orientationObserver = NotificationCenter.default.addObserver(
forName: UIDevice.orientationDidChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
guard let self = self, self.isOrientationLocked else { return }
@objc public func iap_initialize(_ invoke: Invoke) {
StoreKitManager.shared.initialize()
invoke.resolve(["success": true])
}
if self.currentOrientationMask == .portrait {
self.forceOrientation(.portrait)
} else if self.currentOrientationMask == .landscape {
self.forceOrientation(.landscapeRight)
@objc public func iap_fetch_products(_ invoke: Invoke) {
do {
let args = try invoke.parseArgs(FetchProductsRequest.self)
StoreKitManager.shared.fetchProducts(productIds: args.productIds) { products in
let productsData: [ProductData] = products.map { product in
return ProductData(
id: product.productIdentifier,
title: product.localizedTitle,
description: product.localizedDescription,
price: product.price.stringValue,
priceCurrencyCode: product.priceLocale.currencyCode,
priceAmountMicros: Int64(product.price.doubleValue * 1_000_000),
productType: product.productIdentifier.contains("monthly")
|| product.productIdentifier.contains("yearly") ? "subscription" : "consumable"
)
}
invoke.resolve(["products": productsData])
}
} catch {
invoke.reject("Failed to parse fetch products arguments: \(error.localizedDescription)")
}
}
@objc public func iap_purchase_product(_ invoke: Invoke) {
do {
let args = try invoke.parseArgs(PurchaseProductRequest.self)
StoreKitManager.shared.fetchProducts(productIds: [args.productId]) { products in
guard let product = products.first else {
invoke.reject("Product not found")
return
}
StoreKitManager.shared.purchase(product: product) { result in
switch result {
case .success(let txn):
let purchase = PurchaseData(
productId: txn.payment.productIdentifier,
transactionId: txn.transactionIdentifier ?? "",
originalTransactionId: txn.original?.transactionIdentifier ?? txn
.transactionIdentifier ?? "",
purchaseDate: ISO8601DateFormatter().string(from: txn.transactionDate ?? Date()),
purchaseState: "purchased",
platform: "ios"
)
invoke.resolve(["purchase": purchase])
case .failure(let error):
invoke.reject("Purchase failed: \(error.localizedDescription)")
}
}
}
} catch {
invoke.reject("Failed to parse purchase arguments: \(error.localizedDescription)")
}
}
@objc public func iap_restore_purchases(_ invoke: Invoke) {
StoreKitManager.shared.restorePurchases { transactions in
let restored = transactions.map { txn -> PurchaseData in
return PurchaseData(
productId: txn.payment.productIdentifier,
transactionId: txn.transactionIdentifier ?? "",
originalTransactionId: txn.original?.transactionIdentifier ?? txn.transactionIdentifier
?? "",
purchaseDate: ISO8601DateFormatter().string(from: txn.transactionDate ?? Date()),
purchaseState: "restored",
platform: "ios"
)
}
invoke.resolve(["purchases": restored])
}
}
}
@@ -431,3 +546,66 @@ extension NativeBridgePlugin: ASWebAuthenticationPresentationContextProviding {
return UIApplication.shared.windows.first ?? UIWindow()
}
}
extension NativeBridgePlugin: UIApplicationDelegate {
public func application(
_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?
) -> UIInterfaceOrientationMask {
return self.currentOrientationMask
}
/*
Proxy all application delegate methods to the original delegate:
sel!(application:didFinishLaunchingWithOptions:),
sel!(application:openURL:options:),
sel!(application:continue:restorationHandler:),
sel!(applicationDidBecomeActive:),
sel!(applicationWillResignActive:),
sel!(applicationWillEnterForeground:),
sel!(applicationDidEnterBackground:),
sel!(applicationWillTerminate:),
*/
public func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
self.originalDelegate?.application?(application, didFinishLaunchingWithOptions: launchOptions)
?? false
}
public func application(
_ application: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
self.originalDelegate?.application?(application, open: url, options: options) ?? false
}
public func application(
_ application: UIApplication, continue continueUserActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
self.originalDelegate?.application?(
application, continue: continueUserActivity, restorationHandler: restorationHandler) ?? false
}
public func applicationDidBecomeActive(_ application: UIApplication) {
self.originalDelegate?.applicationDidBecomeActive?(application)
}
public func applicationWillResignActive(_ application: UIApplication) {
self.originalDelegate?.applicationWillResignActive?(application)
}
public func applicationWillEnterForeground(_ application: UIApplication) {
self.originalDelegate?.applicationWillEnterForeground?(application)
}
public func applicationDidEnterBackground(_ application: UIApplication) {
self.originalDelegate?.applicationDidEnterBackground?(application)
}
public func applicationWillTerminate(_ application: UIApplication) {
self.originalDelegate?.applicationWillTerminate?(application)
}
}
@@ -0,0 +1,121 @@
import StoreKit
import os
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StoreKitManager")
class StoreKitManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
static let shared = StoreKitManager()
private var productsRequest: SKProductsRequest?
private var productResponseHandler: (([SKProduct]) -> Void)?
private var purchaseHandler: ((Result<SKPaymentTransaction, Error>) -> Void)?
private var restoreHandler: (([SKPaymentTransaction]) -> Void)?
private override init() {
super.init()
}
func initialize() {
SKPaymentQueue.default().add(self)
}
deinit {
SKPaymentQueue.default().remove(self)
}
func fetchProducts(productIds: [String], completion: @escaping ([SKProduct]) -> Void) {
let ids = Set(productIds)
productsRequest = SKProductsRequest(productIdentifiers: ids)
productsRequest?.delegate = self
productResponseHandler = completion
productsRequest?.start()
}
func purchase(
product: SKProduct, completion: @escaping (Result<SKPaymentTransaction, Error>) -> Void
) {
guard SKPaymentQueue.canMakePayments() else {
completion(
.failure(
NSError(
domain: "iap", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchases disabled."])))
return
}
purchaseHandler = completion
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
func restorePurchases(completion: @escaping ([SKPaymentTransaction]) -> Void) {
restoreHandler = completion
SKPaymentQueue.default().restoreCompletedTransactions()
}
// MARK: - SKProductsRequestDelegate
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
DispatchQueue.main.async { [weak self] in
self?.productResponseHandler?(response.products)
self?.productResponseHandler = nil
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
DispatchQueue.main.async { [weak self] in
logger.error("Products request failed: \(error.localizedDescription)")
self?.productResponseHandler?([])
self?.productResponseHandler = nil
}
}
// MARK: - SKPaymentTransactionObserver
func paymentQueue(
_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]
) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
DispatchQueue.main.async { [weak self] in
self?.purchaseHandler?(.success(transaction))
self?.purchaseHandler = nil
}
case .failed:
SKPaymentQueue.default().finishTransaction(transaction)
let error =
transaction.error
?? NSError(
domain: "iap", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase error"]
)
DispatchQueue.main.async { [weak self] in
self?.purchaseHandler?(.failure(error))
self?.purchaseHandler = nil
}
case .restored:
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing:
// Handle these states if needed
break
@unknown default:
break
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
let restored = queue.transactions.filter { $0.transactionState == .restored }
DispatchQueue.main.async { [weak self] in
self?.restoreHandler?(restored)
self?.restoreHandler = nil
}
}
func paymentQueue(
_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error
) {
DispatchQueue.main.async { [weak self] in
logger.error("Restore purchases failed: \(error.localizedDescription)")
self?.restoreHandler?([])
self?.restoreHandler = nil
}
}
}
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-iap-fetch-products"
description = "Enables the iap_fetch_products command without any pre-configured scope."
commands.allow = ["iap_fetch_products"]
[[permission]]
identifier = "deny-iap-fetch-products"
description = "Denies the iap_fetch_products command without any pre-configured scope."
commands.deny = ["iap_fetch_products"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-iap-initialize"
description = "Enables the iap_initialize command without any pre-configured scope."
commands.allow = ["iap_initialize"]
[[permission]]
identifier = "deny-iap-initialize"
description = "Denies the iap_initialize command without any pre-configured scope."
commands.deny = ["iap_initialize"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-iap-purchase-product"
description = "Enables the iap_purchase_product command without any pre-configured scope."
commands.allow = ["iap_purchase_product"]
[[permission]]
identifier = "deny-iap-purchase-product"
description = "Denies the iap_purchase_product command without any pre-configured scope."
commands.deny = ["iap_purchase_product"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-iap-restore-purchases"
description = "Enables the iap_restore_purchases command without any pre-configured scope."
commands.allow = ["iap_restore_purchases"]
[[permission]]
identifier = "deny-iap-restore-purchases"
description = "Denies the iap_restore_purchases command without any pre-configured scope."
commands.deny = ["iap_restore_purchases"]
@@ -14,6 +14,10 @@ Default permissions for the plugin
- `allow-get-sys-fonts-list`
- `allow-intercept-keys`
- `allow-lock-screen-orientation`
- `allow-iap-initialize`
- `allow-iap-fetch-products`
- `allow-iap-purchase-product`
- `allow-iap-restore-purchases`
## Permission Table
@@ -157,6 +161,110 @@ Denies the get_sys_fonts_list command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-iap-fetch-products`
</td>
<td>
Enables the iap_fetch_products command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-iap-fetch-products`
</td>
<td>
Denies the iap_fetch_products command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-iap-initialize`
</td>
<td>
Enables the iap_initialize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-iap-initialize`
</td>
<td>
Denies the iap_initialize command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-iap-purchase-product`
</td>
<td>
Enables the iap_purchase_product command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-iap-purchase-product`
</td>
<td>
Denies the iap_purchase_product command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-iap-restore-purchases`
</td>
<td>
Enables the iap_restore_purchases command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-iap-restore-purchases`
</td>
<td>
Denies the iap_restore_purchases command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-install-package`
</td>
@@ -11,4 +11,8 @@ permissions = [
"allow-get-sys-fonts-list",
"allow-intercept-keys",
"allow-lock-screen-orientation",
"allow-iap-initialize",
"allow-iap-fetch-products",
"allow-iap-purchase-product",
"allow-iap-restore-purchases",
]
@@ -354,6 +354,54 @@
"const": "deny-get-sys-fonts-list",
"markdownDescription": "Denies the get_sys_fonts_list command without any pre-configured scope."
},
{
"description": "Enables the iap_fetch_products command without any pre-configured scope.",
"type": "string",
"const": "allow-iap-fetch-products",
"markdownDescription": "Enables the iap_fetch_products command without any pre-configured scope."
},
{
"description": "Denies the iap_fetch_products command without any pre-configured scope.",
"type": "string",
"const": "deny-iap-fetch-products",
"markdownDescription": "Denies the iap_fetch_products command without any pre-configured scope."
},
{
"description": "Enables the iap_initialize command without any pre-configured scope.",
"type": "string",
"const": "allow-iap-initialize",
"markdownDescription": "Enables the iap_initialize command without any pre-configured scope."
},
{
"description": "Denies the iap_initialize command without any pre-configured scope.",
"type": "string",
"const": "deny-iap-initialize",
"markdownDescription": "Denies the iap_initialize command without any pre-configured scope."
},
{
"description": "Enables the iap_purchase_product command without any pre-configured scope.",
"type": "string",
"const": "allow-iap-purchase-product",
"markdownDescription": "Enables the iap_purchase_product command without any pre-configured scope."
},
{
"description": "Denies the iap_purchase_product command without any pre-configured scope.",
"type": "string",
"const": "deny-iap-purchase-product",
"markdownDescription": "Denies the iap_purchase_product command without any pre-configured scope."
},
{
"description": "Enables the iap_restore_purchases command without any pre-configured scope.",
"type": "string",
"const": "allow-iap-restore-purchases",
"markdownDescription": "Enables the iap_restore_purchases command without any pre-configured scope."
},
{
"description": "Denies the iap_restore_purchases command without any pre-configured scope.",
"type": "string",
"const": "deny-iap-restore-purchases",
"markdownDescription": "Denies the iap_restore_purchases command without any pre-configured scope."
},
{
"description": "Enables the install_package command without any pre-configured scope.",
"type": "string",
@@ -415,10 +463,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`",
"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-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`",
"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`"
"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-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`"
}
]
}
@@ -81,3 +81,34 @@ pub(crate) async fn lock_screen_orientation<R: Runtime>(
) -> Result<()> {
app.native_bridge().lock_screen_orientation(payload)
}
#[command]
pub(crate) async fn iap_initialize<R: Runtime>(
app: AppHandle<R>,
payload: IAPInitializeRequest,
) -> Result<IAPInitializeResponse> {
app.native_bridge().iap_initialize(payload)
}
#[command]
pub(crate) async fn iap_fetch_products<R: Runtime>(
app: AppHandle<R>,
payload: IAPFetchProductsRequest,
) -> Result<IAPFetchProductsResponse> {
app.native_bridge().iap_fetch_products(payload)
}
#[command]
pub(crate) async fn iap_purchase_product<R: Runtime>(
app: AppHandle<R>,
payload: IAPPurchaseProductRequest,
) -> Result<IAPPurchaseProductResponse> {
app.native_bridge().iap_purchase_product(payload)
}
#[command]
pub(crate) async fn iap_restore_purchases<R: Runtime>(
app: AppHandle<R>,
) -> Result<IAPRestorePurchasesResponse> {
app.native_bridge().iap_restore_purchases()
}
@@ -73,4 +73,29 @@ impl<R: Runtime> NativeBridge<R> {
) -> crate::Result<()> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn iap_initialize(
&self,
_payload: IAPInitializeRequest,
) -> crate::Result<IAPInitializeResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn iap_fetch_products(
&self,
_payload: IAPFetchProductsRequest,
) -> crate::Result<IAPFetchProductsResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn iap_purchase_product(
&self,
_payload: IAPPurchaseProductRequest,
) -> crate::Result<IAPPurchaseProductResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn iap_restore_purchases(&self) -> crate::Result<IAPRestorePurchasesResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
}
@@ -47,6 +47,10 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::get_sys_fonts_list,
commands::intercept_keys,
commands::lock_screen_orientation,
commands::iap_initialize,
commands::iap_fetch_products,
commands::iap_purchase_product,
commands::iap_restore_purchases,
])
.setup(|app, api| {
#[cfg(mobile)]
@@ -112,3 +112,46 @@ impl<R: Runtime> NativeBridge<R> {
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn iap_initialize(
&self,
payload: IAPInitializeRequest,
) -> crate::Result<IAPInitializeResponse> {
self.0
.run_mobile_plugin("iap_initialize", payload)
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn iap_fetch_products(
&self,
payload: IAPFetchProductsRequest,
) -> crate::Result<IAPFetchProductsResponse> {
self.0
.run_mobile_plugin("iap_fetch_products", payload)
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn iap_purchase_product(
&self,
payload: IAPPurchaseProductRequest,
) -> crate::Result<IAPPurchaseProductResponse> {
self.0
.run_mobile_plugin("iap_purchase_product", payload)
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn iap_restore_purchases(
&self,
) -> crate::Result<IAPRestorePurchasesResponse> {
self.0
.run_mobile_plugin("iap_restore_purchases", ())
.map_err(Into::into)
}
}
@@ -86,3 +86,69 @@ pub struct InterceptKeysRequest {
pub struct LockScreenOrientationRequest {
pub orientation: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Product {
pub id: String,
pub title: String,
pub description: String,
pub price: String,
pub price_currency_code: Option<String>,
pub price_amount_micros: i64,
pub product_type: String, // "consumable", "non_consumable", or "subscription"
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Purchase {
pub product_id: String,
pub transaction_id: String,
pub purchase_date: String,
pub original_transaction_id: String,
pub purchase_state: String, // "purchased", "pending", "cancelled"
pub platform: String, // "ios" or "android"
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPInitializeRequest {
pub public_key: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPInitializeResponse {
pub success: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPFetchProductsRequest {
pub product_ids: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPFetchProductsResponse {
pub products: Vec<Product>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPPurchaseProductRequest {
pub product_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPPurchaseProductResponse {
pub purchase: Option<Purchase>,
pub cancelled_purchase: Option<Purchase>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IAPRestorePurchasesResponse {
pub purchases: Vec<Purchase>,
}
@@ -9,11 +9,11 @@ exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
links = "tauri-plugin-native-tts"
[dependencies]
tauri = { version = "2.5.0" }
tauri = { version = "2" }
serde = "1.0"
thiserror = "2"
schemars = "0.8"
[build-dependencies]
tauri-plugin = { version = "2.2.0", features = ["build"] }
tauri-plugin = { version = "2", features = ["build"] }
schemars = "0.8"
@@ -5,7 +5,7 @@ plugins {
android {
namespace = "com.readest.native_tts"
compileSdk = 34
compileSdk = 36
defaultConfig {
minSdk = 21
+27 -42
View File
@@ -29,14 +29,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);
eprintln!("Failed to allow file in fs_scope: {e}");
} else {
println!("Allowed file in fs_scope: {:?}", file);
println!("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);
eprintln!("Failed to allow file in asset_protocol_scope: {e}");
} else {
println!("Allowed file in asset_protocol_scope: {:?}", file);
println!("Allowed file in asset_protocol_scope: {file:?}");
}
}
}
@@ -80,18 +80,18 @@ fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
.collect::<Vec<_>>()
.join(",");
let window = app.get_webview_window("main").unwrap();
let script = format!("window.OPEN_WITH_FILES = [{}];", files);
let script = format!("window.OPEN_WITH_FILES = [{files}];");
if let Err(e) = window.eval(&script) {
eprintln!("Failed to set open files variable: {}", e);
eprintln!("Failed to set open files variable: {e}");
}
}
#[cfg(desktop)]
fn set_rounded_window(app: &AppHandle, rounded: bool) {
let window = app.get_webview_window("main").unwrap();
let script = format!("window.IS_ROUNDED = {};", rounded);
let script = format!("window.IS_ROUNDED = {rounded};");
if let Err(e) = window.eval(&script) {
eprintln!("Failed to set IS_ROUNDED variable: {}", e);
eprintln!("Failed to set IS_ROUNDED variable: {e}");
}
}
@@ -105,6 +105,11 @@ async fn start_server(window: Window) -> Result<u16, String> {
.map_err(|err| err.to_string())
}
#[tauri::command]
fn get_environment_variable(name: &str) -> String {
std::env::var(String::from(name)).unwrap_or(String::from(""))
}
#[derive(Clone, serde::Serialize)]
#[allow(dead_code)]
struct Payload {
@@ -121,6 +126,7 @@ pub fn run() {
start_server,
download_file,
upload_file,
get_environment_variable,
#[cfg(target_os = "macos")]
macos::safari_auth::auth_with_safari,
#[cfg(target_os = "macos")]
@@ -220,28 +226,10 @@ pub fn run() {
});
}
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
}
#[cfg(target_os = "linux")]
{
fn has_xdg_mime() -> bool {
std::process::Command::new("which")
.arg("xdg-mime")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
if has_xdg_mime() {
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
} else {
println!("xdg-mime not found; skipping deep link setup on Linux.");
}
let _ = app.deep_link().register_all();
}
if let Err(e) = app.handle().plugin(
@@ -249,7 +237,7 @@ pub fn run() {
.level(log::LevelFilter::Info)
.build(),
) {
eprintln!("Failed to initialize tauri_plugin_log: {}", e);
eprintln!("Failed to initialize tauri_plugin_log: {e}");
};
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
@@ -265,25 +253,22 @@ pub fn run() {
#[cfg(all(not(target_os = "macos"), desktop))]
let win_builder = {
let mut win_builder = win_builder
let mut builder = win_builder
.decorations(false)
.transparent(true)
.visible(false)
.shadow(true)
.title("Readest");
if cfg!(target_os = "windows") {
if tauri_plugin_os::version()
<= tauri_plugin_os::Version::from_string("10.0.19045")
{
win_builder = win_builder.shadow(false);
} else {
win_builder = win_builder.shadow(true);
}
} else {
win_builder = win_builder.shadow(true);
#[cfg(target_os = "windows")]
{
builder = builder.transparent(false);
}
#[cfg(target_os = "linux")]
{
builder = builder.transparent(true);
}
win_builder
builder
};
win_builder.build().unwrap();
@@ -144,7 +144,8 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
.expect("NS window should exist on state to handle resize")
as id;
if state.window.label() == "main" {
if state.window.label() == "main" || state.window.label().starts_with("reader")
{
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
TRAFFIC_LIGHTS_VISIBLE,
@@ -278,7 +279,8 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
.expect("Failed to emit event");
let id = state.window.ns_window().expect("Failed to emit event") as id;
if state.window.label() == "main" {
if state.window.label() == "main" || state.window.label().starts_with("reader")
{
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
TRAFFIC_LIGHTS_VISIBLE,
@@ -363,26 +365,26 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
app_box: *mut c_void = app_box,
toolbar: id = cocoa::base::nil,
super_delegate: id = current_delegate,
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
(windowShouldClose:) => on_window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern "C" fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern "C" fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern "C" fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern "C" fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern "C" fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern "C" fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern "C" fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern "C" fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern "C" fn(&Object, Sel, id)
}))
}
}
+119 -31
View File
@@ -17,8 +17,8 @@ use tokio_util::codec::{BytesCodec, FramedRead};
use read_progress_stream::ReadProgressStream;
use std::collections::HashMap;
use std::time::Instant;
use std::{collections::HashMap, sync::Arc};
type Result<T> = std::result::Result<T, Error>;
@@ -109,41 +109,129 @@ pub async fn download_file(
body: Option<String>,
on_progress: Channel<ProgressPayload>,
) -> Result<()> {
use futures::stream::{self, StreamExt};
use std::cmp::min;
use tokio::io::AsyncSeekExt;
const PART_SIZE: u64 = 1024 * 1024;
let client = reqwest::Client::new();
let mut request = if let Some(body) = body {
client.post(url).body(body)
} else {
client.get(url)
};
// Loop trought the headers keys and values
// and add them to the request object.
for (key, value) in headers {
request = request.header(&key, value);
// Check if server supports range requests
let range_resp = client.get(url).header("Range", "bytes=0-0").send().await?;
let accept_ranges = range_resp
.headers()
.get("accept-ranges")
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("")
.eq_ignore_ascii_case("bytes");
let total = range_resp
.headers()
.get("content-range")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split('/').nth(1))
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
if !accept_ranges || total == 0 {
// Fallback to original single-threaded logic
let mut request = if let Some(body) = body {
client.post(url).body(body)
} else {
client.get(url)
};
for (key, value) in headers.iter() {
request = request.header(key, value);
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(Error::HttpErrorCode(
response.status().as_u16(),
response.text().await.unwrap_or_default(),
));
}
let total = response.content_length().unwrap_or(0);
let mut file = BufWriter::new(File::create(file_path).await?);
let mut stream = response.bytes_stream();
let mut stats = TransferStats::default();
while let Some(chunk) = stream.try_next().await? {
file.write_all(&chunk).await?;
stats.record_chunk_transfer(chunk.len());
let _ = on_progress.send(ProgressPayload {
progress: stats.total_transferred,
total,
transfer_speed: stats.transfer_speed,
});
}
file.flush().await?;
return Ok(());
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(Error::HttpErrorCode(
response.status().as_u16(),
response.text().await.unwrap_or_default(),
));
}
let total = response.content_length().unwrap_or(0);
// Multi-part download with range access
let part_count = total.div_ceil(PART_SIZE);
let file = File::create(file_path).await?;
file.set_len(total).await?;
let mut file = BufWriter::new(File::create(file_path).await?);
let mut stream = response.bytes_stream();
let file = Arc::new(tokio::sync::Mutex::new(file));
let progress = Arc::new(tokio::sync::Mutex::new(TransferStats::default()));
let mut stats = TransferStats::default();
while let Some(chunk) = stream.try_next().await? {
file.write_all(&chunk).await?;
stats.record_chunk_transfer(chunk.len());
let _ = on_progress.send(ProgressPayload {
progress: stats.total_transferred,
total,
transfer_speed: stats.transfer_speed,
});
}
file.flush().await?;
stream::iter(0..part_count)
.for_each_concurrent(8, |i| {
let client = client.clone();
let file = Arc::clone(&file);
let progress = Arc::clone(&progress);
let headers = headers.clone();
let url = url.to_string();
let on_progress = on_progress.clone();
async move {
let start = i * PART_SIZE;
let end = min(start + PART_SIZE - 1, total - 1);
let range_header = format!("bytes={start}-{end}");
let mut req = client.get(&url).header("Range", range_header);
for (key, value) in headers {
req = req.header(key, value);
}
let resp = match req.send().await {
Ok(r) => r,
Err(_) => return,
};
if !resp.status().is_success()
&& resp.status() != reqwest::StatusCode::PARTIAL_CONTENT
{
return;
}
let bytes = match resp.bytes().await {
Ok(b) => b,
Err(_) => return,
};
{
let mut f = file.lock().await;
f.seek(std::io::SeekFrom::Start(start)).await.unwrap();
f.write_all(&bytes).await.unwrap();
}
{
let mut stat = progress.lock().await;
stat.record_chunk_transfer(bytes.len());
let _ = on_progress.send(ProgressPayload {
progress: stat.total_transferred,
total,
transfer_speed: stat.transfer_speed,
});
}
}
})
.await;
Ok(())
}
+10 -6
View File
@@ -15,12 +15,12 @@
"security": {
"csp": {
"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 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://*.microsofttranslator.com https://edge.microsoft.com",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost 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://*",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://fontsapi.zeoseven.com",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://fontsapi.zeoseven.com",
"frame-src": "'self' blob: asset: http://asset.localhost",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com"
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app",
"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.netlify.app",
"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"
},
"assetProtocol": {
"enable": true,
@@ -28,6 +28,7 @@
"allow": [
"$RESOURCE/**",
"$APPDATA/**/*",
"$APPCACHE/**/*",
"$TEMP/**/*",
"/private/var/mobile/Containers/Data/Application/**/*"
],
@@ -151,7 +152,10 @@
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJFMEQ1QjE2OEU1NEIzNTEKUldSUnMxU09GbHNOdmpEaWFMT1crRFpEV2VORzQ2MklxaFc0M1R0ci9xY2c1bENXS0xhM1R1L2sK",
"endpoints": ["https://github.com/readest/readest/releases/latest/download/latest.json"]
"endpoints": [
"https://download.readest.com/releases/latest.json",
"https://github.com/readest/readest/releases/latest/download/latest.json"
]
}
}
}
@@ -0,0 +1,40 @@
import { describe, it, expect, vi } from 'vitest';
import { POST } from '@/app/api/apple/iap-verify/route';
import { NextRequest } from 'next/server';
import { setupSupabaseMocks } from '../helpers/supabase-mock';
const SKIP_IAP_API_TESTS = !process.env['ENABLE_IAP_API_TESTS'];
vi.mock('@/utils/supabase', () => ({
supabase: {
auth: {
getUser: vi.fn(),
refreshSession: vi.fn(),
},
from: vi.fn(),
},
createSupabaseAdminClient: vi.fn(),
}));
describe.skipIf(SKIP_IAP_API_TESTS)('/api/apple/iap-verify', () => {
it('should verify a valid transaction', async () => {
setupSupabaseMocks();
const request = new NextRequest('http://localhost:3000/api/apple/iap-verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({
transactionId: '2000000969168810',
originalTransactionId: '2000000968585424',
}),
});
const response = await POST(request);
const data = await response.json();
console.log('Response:', data);
expect(response.status).toBe(200);
expect(data.purchase).toBeDefined();
});
});
@@ -0,0 +1,109 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { vi } from 'vitest';
export const setupSupabaseMocks = async (
customResponses = {
getUser: null,
select: null,
upsert: null,
update: null,
insert: null,
adminUpsert: null,
adminSelect: {
data: [],
error: null,
},
adminSelectMany: null,
adminUpdate: null,
adminInsert: null,
adminDelete: null,
adminSelectSingle: null,
},
) => {
const { supabase, createSupabaseAdminClient } = await import('@/utils/supabase');
vi.mocked(supabase.auth.getUser).mockResolvedValue(
customResponses.getUser || {
data: {
user: {
id: 'test-user-123',
email: 'test@example.com',
app_metadata: {},
user_metadata: {},
aud: 'test-aud',
created_at: new Date().toISOString(),
},
},
error: null,
},
);
vi.mocked(supabase.from).mockReturnValue({
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn().mockResolvedValue(customResponses.select || { data: null, error: null }),
})),
})),
upsert: vi.fn().mockResolvedValue(customResponses.upsert || { data: {}, error: null }),
update: vi.fn(() => ({
eq: vi.fn().mockResolvedValue(customResponses.update || { data: {}, error: null }),
})),
insert: vi.fn().mockResolvedValue(customResponses.insert || { data: {}, error: null }),
} as any);
vi.mocked(createSupabaseAdminClient).mockReturnValue({
from: vi.fn(() => ({
select: vi.fn(() => ({
eq: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn().mockResolvedValue({ data: null, error: null }),
order: vi.fn().mockResolvedValue({ data: [], error: null }),
limit: vi.fn().mockResolvedValue({ data: [], error: null }),
})),
single: vi
.fn()
.mockResolvedValue(customResponses.adminSelect || { data: null, error: null }),
limit: vi.fn(() => ({
single: vi
.fn()
.mockResolvedValue(customResponses.adminSelect || { data: null, error: null }),
})),
order: vi
.fn()
.mockResolvedValue(customResponses.adminSelectMany || { data: [], error: null }),
})),
neq: vi.fn(() => ({
order: vi
.fn()
.mockResolvedValue(customResponses.adminSelectMany || { data: [], error: null }),
})),
gt: vi.fn(() => ({
order: vi
.fn()
.mockResolvedValue(customResponses.adminSelectMany || { data: [], error: null }),
})),
in: vi.fn(() => ({
order: vi
.fn()
.mockResolvedValue(customResponses.adminSelectMany || { data: [], error: null }),
})),
order: vi
.fn()
.mockResolvedValue(customResponses.adminSelectMany || { data: [], error: null }),
limit: vi
.fn()
.mockResolvedValue(customResponses.adminSelectMany || { data: [], error: null }),
})),
upsert: vi.fn().mockResolvedValue(customResponses.adminUpsert || { data: [], error: null }),
update: vi.fn(() => ({
eq: vi.fn().mockResolvedValue(customResponses.adminUpdate || { data: {}, error: null }),
match: vi.fn().mockResolvedValue(customResponses.adminUpdate || { data: {}, error: null }),
})),
insert: vi.fn().mockResolvedValue(customResponses.adminInsert || { data: {}, error: null }),
delete: vi.fn(() => ({
eq: vi.fn().mockResolvedValue(customResponses.adminDelete || { data: {}, error: null }),
match: vi.fn().mockResolvedValue(customResponses.adminDelete || { data: {}, error: null }),
})),
})),
} as any);
};
@@ -0,0 +1,71 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { AppleIAPVerifier, createAppleIAPVerifier } from '@/libs/iap/apple/verifier';
const SKIP_IAP_API_TESTS = !process.env['ENABLE_IAP_API_TESTS'];
const REAL_TEST_DATA = {
validSubscription: {
transactionId: '2000000969168810',
originalTransactionId: '2000000968585424',
productId: 'com.bilingify.readest.monthly.plus',
},
expiredSubscription: {
transactionId: '2000000969189989',
originalTransactionId: '2000000969189989',
productId: 'com.bilingify.readest.monthly.plus',
},
refundedTransaction: {
transactionId: '1000000555666777',
originalTransactionId: '1000000555666777',
productId: 'com.bilingify.readest.monthly.plus',
},
};
describe.skipIf(SKIP_IAP_API_TESTS)('Apple IAP Integration Tests', () => {
let verifier: AppleIAPVerifier;
beforeAll(() => {
verifier = createAppleIAPVerifier({
keyId: process.env['APPLE_IAP_KEY_ID']!,
issuerId: process.env['APPLE_IAP_ISSUER_ID']!,
bundleId: process.env['APPLE_IAP_BUNDLE_ID']!,
privateKey: atob(process.env['APPLE_IAP_PRIVATE_KEY_BASE64']!),
environment: 'sandbox',
});
});
describe('Transaction Verification', () => {
it('should verify a real valid transaction', async () => {
const { transactionId, originalTransactionId } = REAL_TEST_DATA.validSubscription;
const result = await verifier.verifyTransaction(originalTransactionId);
expect(result.success).toBe(true);
expect(result.verified).toBe(true);
expect(result.status).toBe('active');
expect(result.transactionId).toBe(transactionId);
expect(result.originalTransactionId).toBe(originalTransactionId);
expect(result.bundleId).toBe(process.env['APPLE_IAP_BUNDLE_ID']);
expect(result.productId).toBeTruthy();
expect(result.purchaseDate).toBeInstanceOf(Date);
}, 10000);
it('should handle expired subscription correctly', async () => {
const { originalTransactionId } = REAL_TEST_DATA.expiredSubscription;
const result = await verifier.verifyTransaction(originalTransactionId);
expect(result.success).toBe(true);
expect(result.verified).toBe(true);
expect(result.status).toBe('expired');
}, 10000);
it('should reject invalid transaction IDs', async () => {
const result = await verifier.verifyTransaction('invalid_transaction_id');
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
}, 10000);
});
});
@@ -0,0 +1,273 @@
import { describe, it, expect } from 'vitest';
import { detectLanguage } from '@/utils/lang';
describe('detectLanguage - Result Tests', () => {
describe('English text detection', () => {
it('should detect English text', () => {
const englishTexts = [
'This is a sample English text for language detection testing.',
'The quick brown fox jumps over the lazy dog.',
'Hello world! This is an English sentence with punctuation.',
'Chapter 1: Introduction to Language Detection',
'In the beginning was the Word, and the Word was with God.',
'To be or not to be, that is the question.',
];
englishTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('en');
});
});
});
describe('Chinese text detection', () => {
it('should detect Chinese text', () => {
const chineseTexts = [
'这是一个中文文本的示例,用于语言检测测试。',
'天下大势,分久必合,合久必分。',
'学而时习之,不亦说乎?有朋自远方来,不亦乐乎?',
'春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。',
'那时候,天空很蓝,云彩很白,我们都还很年轻。',
'人工智能技术的发展正在改变我们的生活方式和工作方式。',
];
chineseTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('zh');
});
});
it('should detect Traditional Chinese text', () => {
const traditionalChineseTexts = [
'這是一個正體中文文本的示例,用於語言檢測測試。',
'天下大勢,分久必合,合久必分。',
'學而時習之,不亦說乎?有朋自遠方來,不亦樂乎?',
'春眠不覺曉,處處聞啼鳥。夜來風雨聲,花落知多少。',
'那時候,天空很藍,雲彩很白,我們都還很年輕。',
'人工智慧技術的發展正在改變我們的生活方式和工作方式。',
];
traditionalChineseTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('zh');
});
});
});
describe('Japanese text detection', () => {
it('should detect Japanese text', () => {
const japaneseTexts = [
'これは日本語のテキストサンプルです。',
'第一章:言語検出について',
'こんにちは、世界!これは日本語の文章です。',
'桜の花が咲く季節になりました。とても美しいです。',
'ひらがな、カタカナ、漢字を使って日本語を書きます。',
'東京は日本の首都であり、多くの人々が住んでいます。',
];
japaneseTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('ja');
});
});
});
describe('French text detection', () => {
it('should detect French text', () => {
const frenchTexts = [
'Ceci est un exemple de texte français pour tester la détection de langue.',
'Chapitre 1: Introduction à la détection de langue.',
'Bonjour le monde! Ceci est une phrase en français.',
'Les Champs-Élysées sont une avenue célèbre à Paris, souvent appelée la plus belle avenue du monde.',
'Victor Hugo était un écrivain français très célèbre du XIXe siècle,',
"La littérature française possède une richesse extraordinaire qui s'étend sur plusieurs siècles.",
];
frenchTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('fr');
});
});
});
describe('Spanish text detection', () => {
it('should detect Spanish text', () => {
const spanishTexts = [
'Este es un ejemplo de texto en español para probar la detección de idioma.',
'Capítulo 1: Introducción a la detección de idiomas',
'¡Hola mundo! Esta es una oración en español.',
'España es un país ubicado en la península ibérica.',
'El flamenco es una forma de arte español muy famosa.',
'Don Quijote de La Mancha es una obra clásica de la literatura española.',
];
spanishTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('es');
});
});
});
describe('German text detection', () => {
it('should detect German text', () => {
const germanTexts = [
'Dies ist ein Beispieltext auf Deutsch für die Spracherkennung.',
'Kapitel 1: Einführung in die Spracherkennung',
'Hallo Welt! Dies ist ein deutscher Satz.',
'Deutschland ist ein Land in Mitteleuropa.',
'Das Oktoberfest ist ein berühmtes deutsches Festival.',
'Die deutsche Sprache hat viele zusammengesetzte Wörter.',
];
germanTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('de');
});
});
});
describe('Arabic text detection', () => {
it('should detect Arabic text', () => {
const arabicTexts = [
'هذا نص عربي للاختبار. يجب أن يتم اكتشاف هذا النص كعربي.',
'الفصل الأول: مقدمة في اكتشاف اللغة',
'مرحبا بالعالم! هذه جملة باللغة العربية.',
'العربية لغة جميلة يتحدث بها ملايين الناس.',
'القرآن الكريم نزل باللغة العربية.',
'دمشق هي عاصمة سوريا وواحدة من أقدم المدن في العالم.',
];
arabicTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('ar');
});
});
});
describe('Korean text detection', () => {
it('should detect Korean text', () => {
const koreanTexts = [
'이것은 언어 감지 테스트를 위한 한국어 텍스트 샘플입니다.',
'제1장: 언어 감지 소개',
'안녕하세요 세계! 이것은 한국어 문장입니다.',
'서울은 대한민국의 수도이며 많은 사람들이 살고 있습니다.',
'한글은 세종대왕이 만든 우리나라의 고유한 문자입니다.',
'김치는 한국의 전통 음식 중 하나입니다.',
];
koreanTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('ko');
});
});
});
describe('fallback behavior', () => {
it('should return "en" for very short text', () => {
const shortTexts = ['', ' ', 'Hi', 'OK', '123', '!@#'];
shortTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('en');
});
});
it('should return "en" for unrecognizable text', () => {
const unrecognizableTexts = [
'123456789012345678901234567890',
'!@#$%^&*()_+{}|:"<>?[]\\;\',./',
'1111111111222222222233333333334444444444',
];
unrecognizableTexts.forEach((text) => {
const result = detectLanguage(text);
expect(result).toBe('en');
});
});
});
describe('mixed content', () => {
it('should detect predominant language in mixed content', () => {
const testCases = [
{
text: 'This is mostly English text with some 中文 words mixed in but English dominates.',
expected: 'en',
},
{
text: '这主要是中文文本,虽然有一些 English words 混合在其中,但中文占主导地位。',
expected: 'zh',
},
{
text: 'Ceci est principalement du français avec quelques English words mélangés.',
expected: 'fr',
},
];
testCases.forEach(({ text, expected }) => {
const result = detectLanguage(text);
expect(result).toBe(expected);
});
});
});
describe('long text handling', () => {
it('should handle long text (over 1000 characters)', () => {
const longEnglishText = 'This is a very long English text. '.repeat(100); // ~3400 chars
const longChineseText = '这是一个很长的中文文本。'.repeat(100); // ~1200 chars
const longFrenchText = 'Ceci est un très long texte français. '.repeat(100); // ~3800 chars
expect(detectLanguage(longEnglishText)).toBe('en');
expect(detectLanguage(longChineseText)).toBe('zh');
expect(detectLanguage(longFrenchText)).toBe('fr');
});
});
describe('edge cases with whitespace and formatting', () => {
it('should handle text with lots of whitespace', () => {
const textWithWhitespace = ' This is English text with lots of spaces ';
const result = detectLanguage(textWithWhitespace);
expect(result).toBe('en');
});
it('should handle text with newlines and tabs', () => {
const textWithFormatting = 'This is English text\nwith newlines\tand tabs\nfor formatting.';
const result = detectLanguage(textWithFormatting);
expect(result).toBe('en');
});
it('should handle text with punctuation', () => {
const textWithPunctuation = 'Hello, world! How are you? I am fine. Thank you very much!!!';
const result = detectLanguage(textWithPunctuation);
expect(result).toBe('en');
});
});
});
describe('detectLanguage - Real World Samples', () => {
it('should detect language in book excerpts', () => {
const bookExcerpts = [
{
text: 'It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness.',
expected: 'en',
},
{
text: 'Dans une trou dans la terre vivait un hobbit. Pas un trou déplaisant, sale et humide.',
expected: 'fr',
},
{
text: '昔々、あるところにおじいさんとおばあさんが住んでいました。',
expected: 'ja',
},
{
text: '很久很久以前,有一个美丽的公主住在一座城堡里。',
expected: 'zh',
},
];
bookExcerpts.forEach(({ text, expected }) => {
const result = detectLanguage(text);
expect(result).toBe(expected);
});
});
});
@@ -0,0 +1,241 @@
import { z } from 'zod';
import { NextResponse } from 'next/server';
import { getAppleIAPVerifier } from '@/libs/iap/apple/verifier';
import { createSupabaseAdminClient } from '@/utils/supabase';
import { validateUserAndToken } from '@/utils/access';
import { IAPError } from '@/types/error';
const iapVerificationSchema = z.object({
transactionId: z.string().min(1, 'Transaction ID is required'),
originalTransactionId: z.string().min(1, 'Original Transaction ID is required'),
});
const PRODUCT_MAP: Record<string, string> = {
'com.bilingify.readest.monthly.plus': 'Plus',
'com.bilingify.readest.yearly.plus': 'Plus',
'com.bilingify.readest.monthly.pro': 'Pro',
'com.bilingify.readest.yearly.pro': 'Pro',
};
const getProductName = (productId: string) => {
return PRODUCT_MAP[productId] || productId;
};
const getProductPlan = (productId: string) => {
if (productId.includes('plus')) {
return 'plus';
} else if (productId.includes('pro')) {
return 'pro';
}
return 'free';
};
interface Purchase {
status: string;
customerEmail: string;
subscriptionId: string;
planName: string;
productId: string;
platform: string;
transactionId: string;
originalTransactionId: string;
purchaseDate?: string;
expiresDate?: string | null;
quantity: number;
environment: string;
bundleId: string;
webOrderLineItemId?: string;
subscriptionGroupIdentifier?: string;
type?: string;
revocationDate?: string | null;
revocationReason?: number | null;
}
async function updateUserSubscription(userId: string, purchase: Purchase) {
try {
const supabase = createSupabaseAdminClient();
const { data, error } = await supabase.from('apple_iap_subscriptions').upsert(
{
user_id: userId,
platform: purchase.platform,
product_id: purchase.productId,
transaction_id: purchase.transactionId,
original_transaction_id: purchase.originalTransactionId,
status: purchase.status === 'active' ? 'active' : 'expired',
purchase_date: purchase.purchaseDate,
expires_date: purchase.expiresDate,
environment: purchase.environment,
bundle_id: purchase.bundleId,
quantity: purchase.quantity || 1,
auto_renew_status: true,
web_order_line_item_id: purchase.webOrderLineItemId,
subscription_group_identifier: purchase.subscriptionGroupIdentifier,
created_at: new Date(),
updated_at: new Date(),
},
{
onConflict: 'user_id,original_transaction_id',
},
);
if (error) {
console.error('Database update error:', error);
throw new Error(`Database update failed: ${error.message}`);
}
const plan = await getProductPlan(purchase.productId);
await supabase
.from('plans')
.update({
plan: ['active', 'trialing'].includes(purchase.status) ? plan : 'free',
status: purchase.status,
})
.eq('id', userId);
return data;
} catch (error) {
console.error('Failed to update user subscription:', error);
throw error;
}
}
export async function POST(request: Request) {
const body = await request.json();
let validatedInput;
try {
validatedInput = iapVerificationSchema.parse(body);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'Invalid input data',
purchase: null,
},
{ status: 400 },
);
}
}
const { transactionId, originalTransactionId } = validatedInput!;
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: IAPError.NOT_AUTHENTICATED }, { status: 403 });
}
try {
const supabase = createSupabaseAdminClient();
const { data: existingSubscription } = await supabase
.from('apple_iap_subscriptions')
.select('*')
.eq('original_transaction_id', originalTransactionId)
.single();
console.log('Existing subscription:', existingSubscription);
// Should not restore purchase for another account
if (existingSubscription && existingSubscription.user_id !== user.id) {
return NextResponse.json(
{ error: IAPError.TRANSACTION_BELONGS_TO_ANOTHER_USER },
{ status: 403 },
);
}
if (
existingSubscription &&
existingSubscription.transactionId === transactionId &&
existingSubscription.status === 'active'
) {
console.log('Transaction already verified and active');
const purchase = {
status: existingSubscription.status,
customerEmail: user.email,
subscriptionId:
existingSubscription.web_order_line_item_id ||
existingSubscription.original_transaction_id,
planName: getProductName(existingSubscription.product_id),
productId: existingSubscription.product_id,
platform: existingSubscription.platform,
transactionId: existingSubscription.transaction_id,
originalTransactionId: existingSubscription.original_transaction_id,
purchaseDate: existingSubscription.purchase_date,
expiresDate: existingSubscription.expires_date,
quantity: existingSubscription.quantity,
environment: existingSubscription.environment,
bundleId: existingSubscription.bundle_id,
};
return NextResponse.json({
purchase,
error: null,
});
}
const defaultIAPVerifier = getAppleIAPVerifier();
const verificationResult = await defaultIAPVerifier.verifyTransaction(originalTransactionId);
if (!verificationResult.success) {
console.error('Apple verification failed:', verificationResult.error);
return NextResponse.json(
{
error: verificationResult.error || IAPError.UNKNOWN_ERROR,
purchase: null,
},
{ status: 400 },
);
}
const transaction = verificationResult.transaction!;
console.log('Apple verification successful:', {
transactionId: transaction.transactionId,
productId: transaction.productId,
environment: transaction.environment,
});
if (transaction.environment === 'Sandbox' && process.env.NODE_ENV === 'production') {
console.warn('Sandbox transaction in production environment');
}
const purchase = {
status: verificationResult.status!,
customerEmail: user.email!,
subscriptionId: transaction.webOrderLineItemId || transaction.originalTransactionId,
planName: getProductName(transaction.productId),
productId: transaction.productId,
platform: 'ios',
transactionId: transaction.transactionId,
originalTransactionId: transaction.originalTransactionId,
purchaseDate: verificationResult.purchaseDate?.toISOString(),
expiresDate: verificationResult.expiresDate?.toISOString() || null,
quantity: transaction.quantity,
environment: transaction.environment.toLowerCase(),
bundleId: transaction.bundleId,
webOrderLineItemId: transaction.webOrderLineItemId,
subscriptionGroupIdentifier: transaction.subscriptionGroupIdentifier,
type: transaction.type,
revocationDate: verificationResult.revocationDate?.toISOString() || null,
revocationReason: verificationResult.revocationReason,
};
try {
await updateUserSubscription(user.id, purchase);
} catch (dbError) {
console.error('Database update failed:', dbError);
return NextResponse.json(
{
error: IAPError.TRANSACTION_SERVICE_UNAVAILABLE,
purchase: null,
},
{ status: 500 },
);
}
return NextResponse.json({
purchase,
error: null,
});
} catch (error) {
console.error('IAP verification error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : IAPError.UNKNOWN_ERROR },
{ status: 500 },
);
}
}
@@ -0,0 +1,141 @@
import { NextRequest, NextResponse } from 'next/server';
import { MetadataService } from '@/services/metadata/service';
import { SearchRequest } from '@/services/metadata/types';
import { validateUserAndToken } from '@/utils/access';
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
timestamp: string;
responseTime?: number;
}
function validateSearchRequest(body: SearchRequest): {
isValid: boolean;
error?: string;
data?: SearchRequest;
} {
const { title, isbn, author, language } = body;
if (
(!title || typeof title !== 'string' || title.trim().length === 0) &&
(!isbn || typeof isbn !== 'string' || isbn.trim().length === 0)
) {
return {
isValid: false,
error: 'Either title or isbn parameter is required and must be a non-empty string',
};
}
if (title && typeof title !== 'string') {
return { isValid: false, error: 'Title must be a string if provided' };
}
if (isbn && typeof isbn !== 'string') {
return { isValid: false, error: 'ISBN must be a string if provided' };
}
if (author && typeof author !== 'string') {
return { isValid: false, error: 'Author must be a string if provided' };
}
if (isbn) {
const cleanIsbn = isbn.replace(/[-\s]/g, '');
if (!/^\d{10}(\d{3})?$/.test(cleanIsbn)) {
return { isValid: false, error: 'Invalid ISBN format. Must be 10 or 13 digits' };
}
}
return {
isValid: true,
data: {
title: title?.trim(),
isbn: isbn?.trim(),
author: author?.trim(),
language: language?.trim(),
},
};
}
function createResponse<T>(
success: boolean,
data: T | null = null,
error: string | null = null,
responseTime: number,
): ApiResponse<T> {
return {
success,
data: data || undefined,
error: error || undefined,
timestamp: new Date().toISOString(),
responseTime,
};
}
let metadataService: MetadataService;
function getMetadataService(): MetadataService {
if (!metadataService) {
metadataService = new MetadataService({
googleBooksApiKeys: process.env['GOOGLE_BOOKS_API_KEYS'],
});
}
return metadataService;
}
export async function POST(request: NextRequest) {
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
}
const startTime = Date.now();
try {
const body = await request.json();
const validation = validateSearchRequest(body);
if (!validation.isValid) {
const responseTime = Date.now() - startTime;
return NextResponse.json(createResponse(false, null, validation.error!, responseTime), {
status: 400,
});
}
const service = getMetadataService();
const result = await service.search(validation.data!);
const responseTime = Date.now() - startTime;
if (!result) {
return NextResponse.json(createResponse(false, null, 'Book not found', responseTime), {
status: 404,
});
}
return NextResponse.json(createResponse(true, result, null, responseTime), {
status: 200,
});
} catch (error) {
const responseTime = Date.now() - startTime;
console.error('Search API error:', error);
let errorMessage = 'Internal server error';
let statusCode = 500;
if (error instanceof Error) {
errorMessage = error.message;
if (error.message.includes('rate limit')) {
statusCode = 429;
} else if (error.message.includes('forbidden') || error.message.includes('API key')) {
statusCode = 403;
} else if (error.message.includes('Invalid ISBN')) {
statusCode = 400;
}
}
return NextResponse.json(createResponse(false, null, errorMessage, responseTime), {
status: statusCode,
});
}
}
@@ -0,0 +1,36 @@
import Stripe from 'stripe';
import { NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { createOrUpdateSubscription } from '@/utils/stripe';
import { validateUserAndToken } from '@/utils/access';
export async function POST(request: Request) {
const { sessionId } = await request.json();
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
}
try {
const stripe = getStripe();
const session = await stripe.checkout.sessions.retrieve(sessionId);
if (session.payment_status === 'paid') {
const customerId = session.customer as string;
const subscriptionId = session.subscription as string;
await createOrUpdateSubscription(user.id, customerId, subscriptionId);
}
return NextResponse.json({ session });
} catch (error) {
if (error instanceof Stripe.errors.StripeError) {
console.error('Stripe error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 },
);
}
}
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { validateUserAndToken } from '@/utils/access';
import { createSupabaseAdminClient } from '@/utils/supabase';
export async function POST(request: NextRequest) {
const { priceId, embedded = true, metadata = {} } = await request.json();
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
}
const enhancedMetadata = {
...metadata,
userId: user.id,
};
try {
const supabase = createSupabaseAdminClient();
const { data: customerData } = await supabase
.from('customers')
.select('stripe_customer_id')
.eq('user_id', user.id)
.single();
let customerId;
if (!customerData?.stripe_customer_id) {
const stripe = getStripe();
const customer = await stripe.customers.create({
email: user.email,
metadata: {
userId: user.id,
},
});
customerId = customer.id;
await supabase.from('customers').insert({
user_id: user.id,
stripe_customer_id: customerId,
});
} else {
customerId = customerData.stripe_customer_id;
}
const stripe = getStripe();
const successUrl = `${request.headers.get('origin')}/user/subscription/success?session_id={CHECKOUT_SESSION_ID}`;
const returnUrl = `${request.headers.get('origin')}/user`;
const session = await stripe.checkout.sessions.create({
ui_mode: embedded ? 'embedded' : 'hosted',
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
metadata: enhancedMetadata,
success_url: embedded ? undefined : successUrl,
cancel_url: embedded ? undefined : returnUrl,
redirect_on_completion: embedded ? 'never' : undefined,
});
return NextResponse.json({
url: session.url,
sessionId: session.id,
clientSecret: session.client_secret,
});
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Error creating checkout session' }, { status: 500 });
}
}
@@ -0,0 +1,40 @@
import Stripe from 'stripe';
import { NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { UserPlan } from '@/types/user';
export async function GET() {
try {
const stripe = getStripe();
const prices = await stripe.prices.list({
expand: ['data.product'],
active: true,
type: 'recurring',
});
const plans = prices.data
.filter((price) => {
const product = price.product as Stripe.Product;
return product.active === true;
})
.map((price) => {
const product = price.product as Stripe.Product & {
metadata: { plan: UserPlan };
};
return {
plan: product.metadata.plan,
price_id: price.id,
price: price.unit_amount,
currency: price.currency,
interval: price.recurring?.interval,
product: price.product,
productName: product.name,
};
});
return NextResponse.json(plans);
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Error fetching subscription plans' }, { status: 500 });
}
}
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { validateUserAndToken } from '@/utils/access';
import { createSupabaseAdminClient } from '@/utils/supabase';
export async function POST(request: NextRequest) {
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
}
try {
const supabase = createSupabaseAdminClient();
const { data: customerData } = await supabase
.from('customers')
.select('stripe_customer_id')
.eq('user_id', user.id)
.single();
if (!customerData?.stripe_customer_id) {
throw new Error('Customer not found');
}
const stripe = getStripe();
const session = await stripe.billingPortal.sessions.create({
customer: customerData.stripe_customer_id,
return_url: `${request.headers.get('origin')}/user`,
});
return NextResponse.json({ url: session.url });
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Error creating portal session' }, { status: 500 });
}
}
@@ -0,0 +1,218 @@
import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { createOrUpdateSubscription } from '@/utils/stripe';
import { createSupabaseAdminClient } from '@/utils/supabase';
export async function POST(request: NextRequest) {
try {
const body = await request.text();
const signature = request.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing Stripe signature' }, { status: 401 });
}
const stripe = getStripe();
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env['STRIPE_WEBHOOK_SECRET']!,
);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
console.error(`Webhook signature verification failed: ${message}`);
return NextResponse.json(
{
error: `Webhook signature verification failed: ${message}`,
},
{ status: 400 },
);
}
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
const userId = session.metadata?.['userId'];
if (userId) {
if (session.mode === 'subscription') {
await handleSuccessfulSubscription(session, userId);
} else {
await handleSuccessfulPayment(session, userId);
}
}
break;
case 'invoice.payment_succeeded':
await handleSuccessfulInvoice(event.data.object);
break;
case 'invoice.payment_failed':
await handleFailedInvoice(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(event.data.object);
break;
}
return NextResponse.json({ received: true });
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Webhook error:', message);
return NextResponse.json({ error: message }, { status: 500 });
}
}
async function handleSuccessfulPayment(session: Stripe.Checkout.Session, userId: string) {
const supabase = createSupabaseAdminClient();
await supabase.from('payments').insert({
user_id: userId,
stripe_checkout_id: session.id,
amount: session.amount_total,
currency: session.currency,
status: 'completed',
payment_intent: session.payment_intent,
payment_method: session.payment_method_types?.[0] || 'unknown',
created_at: new Date().toISOString(),
});
}
async function handleSuccessfulSubscription(session: Stripe.Checkout.Session, userId: string) {
const customerId = session.customer as string;
const subscriptionId = session.subscription as string;
await createOrUpdateSubscription(userId, customerId, subscriptionId);
}
async function handleSuccessfulInvoice(invoice: Stripe.Invoice) {
const customerId = invoice.customer;
const subscriptionId = invoice.parent?.subscription_details?.subscription;
if (!subscriptionId) {
return;
}
const supabase = createSupabaseAdminClient();
const { data: customerData } = await supabase
.from('customers')
.select('user_id')
.eq('stripe_customer_id', customerId)
.single();
if (!customerData?.user_id) {
console.error('Customer not found:', customerId);
return;
}
await supabase
.from('subscriptions')
.update({
status: 'active',
current_period_end: new Date(invoice.lines.data[0]!.period.end * 1000).toISOString(),
})
.eq('stripe_subscription_id', subscriptionId);
await supabase
.from('plans')
.update({
status: 'active',
})
.eq('user_id', customerData.user_id);
}
async function handleFailedInvoice(invoice: Stripe.Invoice) {
const customerId = invoice.customer;
const subscriptionId = invoice.parent?.subscription_details?.subscription;
if (!subscriptionId) {
return;
}
const supabase = createSupabaseAdminClient();
const { data: customerData } = await supabase
.from('customers')
.select('user_id')
.eq('stripe_customer_id', customerId)
.single();
if (!customerData?.user_id) {
console.error('Customer not found:', customerId);
return;
}
await supabase
.from('subscriptions')
.update({
status: 'past_due',
})
.eq('stripe_subscription_id', subscriptionId);
await supabase
.from('plans')
.update({
status: 'past_due',
})
.eq('id', customerData.user_id);
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
const subscriptionId = subscription.id;
const supabase = createSupabaseAdminClient();
const { data: subscriptionData } = await supabase
.from('subscriptions')
.select('user_id, stripe_customer_id')
.eq('stripe_subscription_id', subscriptionId)
.single();
if (!subscriptionData) {
console.error('Subscription not found:', subscriptionId);
return;
}
const { user_id, stripe_customer_id } = subscriptionData;
await createOrUpdateSubscription(user_id, stripe_customer_id, subscriptionId);
}
async function handleSubscriptionCancelled(subscription: Stripe.Subscription) {
const subscriptionId = subscription.id;
const supabase = createSupabaseAdminClient();
await supabase
.from('subscriptions')
.update({
status: 'cancelled',
cancelled_at: new Date().toISOString(),
})
.eq('stripe_subscription_id', subscriptionId);
const { data: subscriptionData } = await supabase
.from('subscriptions')
.select('user_id')
.eq('stripe_subscription_id', subscriptionId)
.single();
if (subscriptionData?.user_id) {
await supabase
.from('plans')
.update({
plan: 'free',
status: 'cancelled',
})
.eq('id', subscriptionData.user_id);
}
}
// This is needed to parse the body as a stream for the webhook signature verification
export const config = {
api: {
bodyParser: false,
},
};
+25 -25
View File
@@ -1,42 +1,42 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { handleAuthCallback } from '@/helpers/auth';
import { useAuth } from '@/context/AuthContext';
import { handleAuthCallback } from '@/helpers/auth';
export default function AuthCallback() {
const router = useRouter();
const { login } = useAuth();
if (typeof window === 'undefined') {
return null;
}
const hash = window.location.hash || '';
const params = new URLSearchParams(hash.slice(1));
useEffect(() => {
const hash = window.location.hash || '';
const params = new URLSearchParams(hash.slice(1));
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const type = params.get('type');
const next = params.get('next') ?? '/';
const error = params.get('error');
const errorDescription = params.get('error_description');
const errorCode = params.get('error_code');
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const type = params.get('type');
const next = params.get('next') ?? '/';
const error = params.get('error');
const errorDescription = params.get('error_description');
const errorCode = params.get('error_code');
handleAuthCallback({
accessToken,
refreshToken,
type,
next,
error,
errorCode,
errorDescription,
login,
navigate: router.push,
});
handleAuthCallback({
accessToken,
refreshToken,
type,
next,
error,
errorCode,
errorDescription,
login,
navigate: router.push,
});
}, [login, router]);
return (
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<span className='loading loading-infinity loading-xl w-20'></span>
<span className='loading loading-infinity loading-xl w-20' />
</div>
);
}
+89 -54
View File
@@ -22,6 +22,7 @@ import { getBaseUrl, isTauriAppPlatform } from '@/services/environment';
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
import { openUrl } from '@tauri-apps/plugin-opener';
import { invoke } from '@tauri-apps/api/core';
import { handleAuthCallback } from '@/helpers/auth';
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
@@ -69,15 +70,22 @@ export default function AuthPage() {
const { isTrafficLightVisible } = useTrafficLightStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [port, setPort] = useState<number | null>(null);
const isOAuthServerRunning = useRef(false);
const [isMounted, setIsMounted] = useState(false);
const isOAuthServerRunning = useRef(false);
const useCustomeOAuth = useRef(false);
const headerRef = useRef<HTMLDivElement>(null);
useTheme({ systemUIVisible: false });
const getTauriRedirectTo = (isOAuth: boolean) => {
if (process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN) {
// For custom OAuth mode, use a local server to handle the OAuth callback
// This is useful for development or some sandboxed environments like Flatpak
// where custom URL schemes are not supported
if (
!useCustomeOAuth.current &&
(process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN)
) {
if (appService?.isMobileApp) {
return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK;
}
@@ -171,7 +179,10 @@ export default function AuthPage() {
const startTauriOAuth = async () => {
try {
if (process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN) {
if (
!useCustomeOAuth.current &&
(process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN)
) {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
currentWindow.listen('single-instance', ({ event, payload }) => {
@@ -241,7 +252,7 @@ export default function AuthPage() {
button_label: _('Sign up'),
loading_button_label: _('Signing up...'),
social_provider_text: _('Sign in with {{provider}}'),
link_text: _('Dont have an account? Sign up'),
link_text: _("Don't have an account? Sign up"),
confirmation_text: _('Check your email for the confirmation link'),
},
magic_link: {
@@ -280,6 +291,12 @@ export default function AuthPage() {
if (isOAuthServerRunning.current) return;
isOAuthServerRunning.current = true;
invoke('get_environment_variable', { name: 'USE_CUSTOM_OAUTH' }).then((value) => {
if (value === 'true') {
useCustomeOAuth.current = true;
}
});
startTauriOAuth();
return () => {
isOAuthServerRunning.current = false;
@@ -317,64 +334,82 @@ export default function AuthPage() {
return isTauriAppPlatform() ? (
<div
className={clsx(
'fixed inset-0 z-0 flex select-none flex-col items-center overflow-y-auto',
'bg-base-100 border-base-200 border',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
'bg-base-100 inset-0 flex select-none flex-col items-center overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.isLinuxApp && 'window-border',
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<div
ref={headerRef}
className={clsx(
'fixed z-10 flex w-full items-center justify-between py-2 pe-6 ps-4',
appService?.hasTrafficLight && 'pt-11',
'flex h-full w-full flex-col items-center overflow-y-auto',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
)}
>
<button onClick={handleGoBack} className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0')}>
<IoArrowBack className='text-base-content' />
</button>
<div
ref={headerRef}
className={clsx(
'fixed z-10 flex w-full items-center justify-between py-2 pe-6 ps-4',
appService?.hasTrafficLight && 'pt-11',
)}
>
<button
onClick={handleGoBack}
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
>
<IoArrowBack className='text-base-content' />
</button>
{appService?.hasWindowBar && (
<WindowButtons
headerRef={headerRef}
showMinimize={!isTrafficLightVisible}
showMaximize={!isTrafficLightVisible}
showClose={!isTrafficLightVisible}
onClose={handleGoBack}
{appService?.hasWindowBar && (
<WindowButtons
headerRef={headerRef}
showMinimize={!isTrafficLightVisible}
showMaximize={!isTrafficLightVisible}
showClose={!isTrafficLightVisible}
onClose={handleGoBack}
/>
)}
</div>
<div
className={clsx(
'z-20 flex flex-col items-center pb-8',
appService?.hasTrafficLight ? 'mt-24' : 'mt-12',
)}
style={{ maxWidth: '420px' }}
>
<ProviderLogin
provider='google'
handleSignIn={tauriSignIn}
Icon={FcGoogle}
label={_('Sign in with Google')}
/>
)}
</div>
<div
className={clsx('z-20 pb-8', appService?.hasTrafficLight ? 'mt-24' : 'mt-12')}
style={{ maxWidth: '420px' }}
>
<ProviderLogin
provider='google'
handleSignIn={tauriSignIn}
Icon={FcGoogle}
label={_('Sign in with Google')}
/>
<ProviderLogin
provider='apple'
handleSignIn={appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn}
Icon={FaApple}
label={_('Sign in with Apple')}
/>
<ProviderLogin
provider='github'
handleSignIn={tauriSignIn}
Icon={FaGithub}
label={_('Sign in with GitHub')}
/>
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={[]}
redirectTo={getTauriRedirectTo(false)}
localization={getAuthLocalization()}
/>
<ProviderLogin
provider='apple'
handleSignIn={
appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn
}
Icon={FaApple}
label={_('Sign in with Apple')}
/>
<ProviderLogin
provider='github'
handleSignIn={tauriSignIn}
Icon={FaGithub}
label={_('Sign in with GitHub')}
/>
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
<div className='w-full'>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={[]}
redirectTo={getTauriRedirectTo(false)}
localization={getAuthLocalization()}
/>
</div>
</div>
</div>
</div>
) : (
+6 -4
View File
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { parseWebViewVersion } from '@/utils/ua';
import { handleGlobalError } from '@/utils/error';
interface ErrorPageProps {
error: Error & { digest?: string };
@@ -19,6 +20,7 @@ export default function Error({ error, reset }: ErrorPageProps) {
useEffect(() => {
setBrowserInfo(parseWebViewVersion(appService));
posthog.captureException(error);
handleGlobalError(error);
}, [appService, error]);
const handleGoHome = () => {
@@ -36,8 +38,8 @@ export default function Error({ error, reset }: ErrorPageProps) {
return (
<div className='hero bg-base-200 min-h-screen'>
<div className='hero-content text-center'>
<div className='max-w-2xl'>
<div className='mb-8'>
<div className='w-full max-w-2xl p-1'>
<div className='mb-8 mt-6'>
<div className='text-error animate-pulse text-8xl'></div>
</div>
@@ -55,8 +57,8 @@ export default function Error({ error, reset }: ErrorPageProps) {
<p className='break-words font-mono text-sm'>{error.message}</p>
{browserInfo && <p className='mt-2 font-mono text-sm'>Browser: {browserInfo}</p>}
{error.stack && (
<p className='mt-2 break-words font-mono text-sm'>
{error.stack.split('\n').slice(0, 5).join('\n')}
<p className='mt-2 whitespace-pre-wrap break-all font-mono text-sm'>
{error.stack.split('\n').slice(0, 3).join('\n')}
</p>
)}
{error.digest && <p className='mt-2 text-xs opacity-70'>Error ID: {error.digest}</p>}
@@ -11,17 +11,18 @@ import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { navigateToLogin } from '@/utils/nav';
import { formatAuthors } from '@/utils/book';
import ReadingProgress from './ReadingProgress';
import BookCover from '@/components/BookCover';
interface BookItemProps {
mode: LibraryViewModeType;
book: Book;
mode: LibraryViewModeType;
coverFit: LibraryCoverFitType;
isSelectMode: boolean;
selectedBooks: string[];
bookSelected: boolean;
transferProgress: number | null;
handleBookUpload: (book: Book) => void;
handleBookDownload: (book: Book) => void;
@@ -29,10 +30,11 @@ interface BookItemProps {
}
const BookItem: React.FC<BookItemProps> = ({
mode,
book,
mode,
coverFit,
isSelectMode,
selectedBooks,
bookSelected,
transferProgress,
handleBookUpload,
handleBookDownload,
@@ -52,24 +54,26 @@ const BookItem: React.FC<BookItemProps> = ({
<div
className={clsx(
'book-item flex',
mode === 'grid' && 'h-full flex-col',
mode === 'grid' && 'h-full flex-col justify-end',
mode === 'list' && 'h-28 flex-row gap-4 overflow-hidden',
appService?.hasContextMenu ? 'cursor-pointer' : '',
)}
>
<div
className={clsx(
'bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md',
mode === 'list' && 'min-w-20',
'relative flex aspect-[28/41] justify-center',
coverFit === 'crop' && 'overflow-hidden shadow-md',
mode === 'grid' && 'items-end',
mode === 'list' && 'min-w-20 items-center',
)}
>
<BookCover mode={mode} book={book} />
{selectedBooks.includes(book.hash) && (
<BookCover mode={mode} book={book} coverFit={coverFit} showSpine={false} />
{bookSelected && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
{isSelectMode && (
<div className='absolute bottom-1 right-1'>
{selectedBooks.includes(book.hash) ? (
{bookSelected ? (
<MdCheckCircle className='fill-blue-500' />
) : (
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
@@ -102,6 +106,10 @@ const BookItem: React.FC<BookItemProps> = ({
</div>
<div
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
style={{
height: `${iconSize15}px`,
minHeight: `${iconSize15}px`,
}}
>
{book.progress && <ReadingProgress book={book} />}
<div className='flex items-center justify-center gap-x-2'>
@@ -115,7 +123,7 @@ const BookItem: React.FC<BookItemProps> = ({
onPointerLeave={(e) => stopEvent(e)}
onClick={() => showBookDetailsModal(book)}
>
<div className='pt-[1px]'>
<div className='pt-[2px] sm:pt-[1px]'>
<LiaInfoCircleSolid size={iconSize15} />
</div>
</button>
@@ -1,17 +1,17 @@
import clsx from 'clsx';
import * as React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
import { LuFolderPlus } from 'react-icons/lu';
import { PiPlus } from 'react-icons/pi';
import { Book, BooksGroup } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useLibraryStore } from '@/store/libraryStore';
import { useTranslation } from '@/hooks/useTranslation';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav';
import { formatAuthors, formatTitle } from '@/utils/book';
import { isMd5 } from '@/utils/md5';
@@ -54,7 +54,6 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const { appService } = useEnv();
const { settings } = useSettingsStore();
const [loading, setLoading] = useState(false);
const [selectedBooks, setSelectedBooks] = useState<string[]>([]);
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [showGroupingModal, setShowGroupingModal] = useState(false);
@@ -66,9 +65,11 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const [sortOrder, setSortOrder] = useState(
searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'),
);
const [coverFit, setCoverFit] = useState(searchParams?.get('cover') || settings.libraryCoverFit);
const isImportingBook = useRef(false);
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
const allBookshelfItems =
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
@@ -106,6 +107,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const view = searchParams?.get('view') || settings.libraryViewMode;
const sort = searchParams?.get('sort') || settings.librarySortBy;
const order = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
const cover = searchParams?.get('cover') || settings.libraryCoverFit;
const params = new URLSearchParams(searchParams?.toString());
if (query) {
params.set('q', query);
@@ -132,6 +134,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
} else {
params.delete('view');
}
setCoverFit(cover);
if (cover === 'crop') {
params.delete('cover');
}
if (sort === 'updated' && order === 'desc' && view === 'grid') {
params.delete('sort');
params.delete('order');
@@ -156,18 +162,24 @@ const Bookshelf: React.FC<BookshelfProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, libraryBooks, showGroupingModal]);
const toggleSelection = (id: string) => {
setSelectedBooks((prev) =>
prev.includes(id) ? prev.filter((selectedId) => selectedId !== id) : [...prev, id],
);
};
const toggleSelection = useCallback((id: string) => {
toggleSelectedBook(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const openSelectedBooks = () => {
setTimeout(() => setLoading(true), 200);
navigateToReader(router, selectedBooks);
handleSetSelectMode(false);
if (appService?.hasWindow && settings.openBookInNewWindow) {
showReaderWindow(appService, getSelectedBooks());
} else {
setTimeout(() => setLoading(true), 200);
navigateToReader(router, getSelectedBooks());
}
};
const openBookDetails = () => {
handleSetSelectMode(false);
const selectedBooks = getSelectedBooks();
const book = libraryBooks.find((book) => book.hash === selectedBooks[0]);
if (book) {
handleShowDetailsBook(book);
@@ -176,6 +188,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const getBooksToDelete = () => {
const booksToDelete: Book[] = [];
const selectedBooks = getSelectedBooks();
selectedBooks.forEach((id) => {
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
if (book && !book.deletedAt) {
@@ -210,7 +223,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const searchTerm = new RegExp(queryTerm, 'i');
const title = formatTitle(item.title);
const authors = formatAuthors(item.author);
return searchTerm.test(title) || searchTerm.test(authors);
return (
searchTerm.test(title) ||
searchTerm.test(authors) ||
searchTerm.test(item.format) ||
(item.groupName && searchTerm.test(item.groupName)) ||
(item.metadata?.description && searchTerm.test(item.metadata?.description))
);
};
const bookSorter = (a: Book, b: Book) => {
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
@@ -275,23 +294,28 @@ const Bookshelf: React.FC<BookshelfProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSelectMode, isSelectAll, isSelectNone]);
const selectedBooks = getSelectedBooks();
return (
<div className='bookshelf'>
<div
className={clsx(
'transform-wrapper',
'bookshelf-items transform-wrapper',
viewMode === 'grid' && 'grid flex-1 grid-cols-3 gap-x-4 px-4 sm:gap-x-0 sm:px-2',
viewMode === 'grid' && 'sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
viewMode === 'list' && 'flex flex-col',
)}
>
{filteredBookshelfItems.map((item, index) => (
{filteredBookshelfItems.map((item) => (
<BookshelfItem
key={`library-item-${index}`}
mode={viewMode as LibraryViewModeType}
key={`library-item-${'hash' in item ? item.hash : item.id}`}
item={item}
mode={viewMode as LibraryViewModeType}
coverFit={coverFit as LibraryCoverFitType}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
itemSelected={
'hash' in item ? selectedBooks.includes(item.hash) : selectedBooks.includes(item.id)
}
setLoading={setLoading}
toggleSelection={toggleSelection}
handleBookUpload={handleBookUpload}
@@ -1,6 +1,7 @@
import clsx from 'clsx';
import { useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav';
import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
@@ -8,9 +9,10 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useLongPress } from '@/hooks/useLongPress';
import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { getOSPlatform } from '@/utils/misc';
import { getLocalBookFilename } from '@/utils/book';
import { LibraryViewModeType } from '@/types/settings';
import { getOSPlatform } from '@/utils/misc';
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, BookGroupType, BooksGroup } from '@/types/book';
@@ -73,8 +75,9 @@ export const generateGroupsList = (items: Book[]): BookGroupType[] => {
interface BookshelfItemProps {
mode: LibraryViewModeType;
item: BookshelfItem;
coverFit: LibraryCoverFitType;
isSelectMode: boolean;
selectedBooks: string[];
itemSelected: boolean;
transferProgress: number | null;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
toggleSelection: (hash: string) => void;
@@ -88,8 +91,9 @@ interface BookshelfItemProps {
const BookshelfItem: React.FC<BookshelfItemProps> = ({
mode,
item,
coverFit,
isSelectMode,
selectedBooks,
itemSelected,
transferProgress,
setLoading,
toggleSelection,
@@ -106,22 +110,28 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
const { settings } = useSettingsStore();
const { updateBook } = useLibraryStore();
const showBookDetailsModal = async (book: Book) => {
const showBookDetailsModal = useCallback(async (book: Book) => {
if (await makeBookAvailable(book)) {
handleShowDetailsBook(book);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const makeBookAvailable = async (book: Book) => {
if (book.uploadedAt && !book.downloadedAt) {
if (await appService?.isBookAvailable(book)) {
if (!book.downloadedAt || !book.coverDownloadedAt) {
book.downloadedAt = Date.now();
book.coverDownloadedAt = Date.now();
await updateBook(envConfig, book);
}
return true;
}
let available = false;
const loadingTimeout = setTimeout(() => setLoading(true), 200);
try {
available = await handleBookDownload(book);
updateBook(envConfig, book);
await updateBook(envConfig, book);
} finally {
if (loadingTimeout) clearTimeout(loadingTimeout);
setLoading(false);
@@ -131,24 +141,41 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
return true;
};
const handleBookClick = async (book: Book) => {
if (isSelectMode) {
toggleSelection(book.hash);
} else {
if (!(await makeBookAvailable(book))) return;
navigateToReader(router, [book.hash]);
}
};
const handleBookClick = useCallback(
async (book: Book) => {
if (isSelectMode) {
toggleSelection(book.hash);
} else {
const available = await makeBookAvailable(book);
if (!available) return;
if (appService?.hasWindow && settings.openBookInNewWindow) {
showReaderWindow(appService, [book.hash]);
} else {
setTimeout(() => {
navigateToReader(router, [book.hash]);
}, 0);
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[isSelectMode, settings.openBookInNewWindow, appService],
);
const handleGroupClick = (group: BooksGroup) => {
if (isSelectMode) {
toggleSelection(group.id);
} else {
const params = new URLSearchParams(searchParams?.toString());
params.set('group', group.id);
navigateToLibrary(router, `${params.toString()}`);
}
};
const handleGroupClick = useCallback(
(group: BooksGroup) => {
if (isSelectMode) {
toggleSelection(group.id);
} else {
const params = new URLSearchParams(searchParams?.toString());
params.set('group', group.id);
setTimeout(() => {
navigateToLibrary(router, `${params.toString()}`);
}, 0);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[isSelectMode, searchParams],
);
const bookContextMenuHandler = async (book: Book) => {
if (!appService?.hasContextMenu) return;
@@ -156,7 +183,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
const fileRevealLabel =
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
const selectBookMenuItem = await MenuItem.new({
text: selectedBooks.includes(book.hash) ? _('Deselect Book') : _('Select Book'),
text: itemSelected ? _('Deselect Book') : _('Select Book'),
action: async () => {
if (!isSelectMode) handleSetSelectMode(true);
toggleSelection(book.hash);
@@ -210,7 +237,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
const groupContextMenuHandler = async (group: BooksGroup) => {
if (!appService?.hasContextMenu) return;
const selectGroupMenuItem = await MenuItem.new({
text: selectedBooks.includes(group.id) ? _('Deselect Group') : _('Select Group'),
text: itemSelected ? _('Deselect Group') : _('Select Group'),
action: async () => {
if (!isSelectMode) handleSetSelectMode(true);
toggleSelection(group.id);
@@ -230,8 +257,9 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
menu.popup();
};
const { pressing, handlers } = useLongPress({
onLongPress: async () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleSelectItem = useCallback(
throttle(() => {
if (!isSelectMode) {
handleSetSelectMode(true);
}
@@ -240,22 +268,56 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
} else {
toggleSelection((item as BooksGroup).id);
}
},
onTap: () => {
}, 100),
[isSelectMode],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleOpenItem = useCallback(
throttle(() => {
if (isSelectMode) {
handleSelectItem();
return;
}
if ('format' in item) {
handleBookClick(item as Book);
} else {
handleGroupClick(item as BooksGroup);
}
},
onContextMenu: () => {
}, 100),
[handleSelectItem, handleBookClick, handleGroupClick],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleContextMenu = useCallback(
throttle(() => {
if ('format' in item) {
bookContextMenuHandler(item as Book);
} else {
groupContextMenuHandler(item as BooksGroup);
}
}, 100),
[itemSelected],
);
const { pressing, handlers } = useLongPress(
{
onLongPress: () => {
handleSelectItem();
},
onTap: () => {
handleOpenItem();
},
onContextMenu: () => {
if (appService?.hasContextMenu) {
handleContextMenu();
} else if (appService?.isAndroidApp) {
handleSelectItem();
}
},
},
});
[isSelectMode, handleSelectItem, handleOpenItem, handleContextMenu],
);
return (
<div className={clsx(mode === 'list' && 'sm:hover:bg-base-300/50 px-4 sm:px-6')}>
@@ -264,6 +326,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
'group',
mode === 'grid' && 'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-4 sm:px-4',
mode === 'list' && 'border-base-300 flex flex-col border-b py-2',
appService?.isMobileApp && 'no-context-menu',
pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100',
)}
style={{
@@ -276,15 +339,16 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
<BookItem
mode={mode}
book={item}
coverFit={coverFit}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
bookSelected={itemSelected}
transferProgress={transferProgress}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
showBookDetailsModal={showBookDetailsModal}
/>
) : (
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
<GroupItem group={item} isSelectMode={isSelectMode} groupSelected={itemSelected} />
)}
</div>
</div>
@@ -1,21 +1,24 @@
import clsx from 'clsx';
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { BooksGroup } from '@/types/book';
import BookCover from '@/components/BookCover';
interface GroupItemProps {
group: BooksGroup;
isSelectMode: boolean;
selectedBooks: string[];
groupSelected: boolean;
}
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, selectedBooks }) => {
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, groupSelected }) => {
const { appService } = useEnv();
const iconSize15 = useResponsiveSize(15);
return (
<div
className={clsx(
'group-item flex h-full flex-col',
'group-item flex h-full flex-col justify-end',
appService?.hasContextMenu ? 'cursor-pointer' : '',
)}
>
@@ -27,12 +30,12 @@ const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, selectedBook
</div>
))}
</div>
{selectedBooks.includes(group.id) && (
{groupSelected && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
{isSelectMode && (
<div className='absolute bottom-1 right-1'>
{selectedBooks.includes(group.id) ? (
{groupSelected ? (
<MdCheckCircle className='fill-blue-500' />
) : (
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
@@ -40,10 +43,13 @@ const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, selectedBook
</div>
)}
</div>
<div className='min-w-0 flex-1 pt-2'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold'>
{group.name}
</h4>
<div className={clsx('flex w-full flex-col pt-2')}>
<div className='min-w-0 flex-1'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold'>
{group.name}
</h4>
</div>
<div className='placeholder' style={{ height: `${iconSize15}px` }}></div>
</div>
</div>
);
@@ -22,7 +22,7 @@ import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SettingsMenu from './SettingsMenu';
import ImportMenu from './ImportMenu';
import SortMenu from './SortMenu';
import ViewMenu from './ViewMenu';
interface LibraryHeaderProps {
isSelectMode: boolean;
@@ -228,7 +228,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
>
<SortMenu />
<ViewMenu />
</Dropdown>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
@@ -33,6 +33,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const router = useRouter();
const { envConfig, appService } = useEnv();
const { user } = useAuth();
const { userPlan, quotas } = useQuotaStats(true);
const { themeMode, setThemeMode } = useThemeStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
@@ -47,8 +48,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const [isTelemetryEnabled, setIsTelemetryEnabled] = useState(settings.telemetryEnabled);
const iconSize = useResponsiveSize(16);
const { quotas } = useQuotaStats();
const showAboutReadest = () => {
setAboutDialogVisible(true);
setIsDropdownOpen?.(false);
@@ -84,6 +83,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
const toggleOpenInNewWindow = () => {
settings.openBookInNewWindow = !settings.openBookInNewWindow;
setSettings(settings);
saveSettings(envConfig, settings);
setIsDropdownOpen?.(false);
};
const toggleAlwaysOnTop = () => {
settings.alwaysOnTop = !settings.alwaysOnTop;
setSettings(settings);
@@ -152,6 +158,11 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
setIsTelemetryEnabled(settings.telemetryEnabled);
};
const handleUpgrade = () => {
navigateToProfile(router);
setIsDropdownOpen?.(false);
};
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
const userFullName = user?.user_metadata?.['full_name'];
const userDisplayName = userFullName ? userFullName.split(' ')[0] : null;
@@ -181,7 +192,9 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
}
>
<ul>
<Quota quotas={quotas} labelClassName='h-10 pl-3 pr-2' />
<div onClick={handleUserProfile} className='cursor-pointer'>
<Quota quotas={quotas} labelClassName='h-10 pl-3 pr-2' />
</div>
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
</ul>
</MenuItem>
@@ -215,6 +228,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
/>
)}
<hr className='border-base-200 my-1' />
{appService?.hasWindow && (
<MenuItem
label={_('Open Book in New Window')}
Icon={settings.openBookInNewWindow ? MdCheck : undefined}
onClick={toggleOpenInNewWindow}
/>
)}
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
{appService?.hasWindow && (
<MenuItem
@@ -248,6 +268,9 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
onClick={cycleThemeMode}
/>
<hr className='border-base-200 my-1' />
{user && userPlan === 'free' && !appService?.isIOSApp && (
<MenuItem label={_('Upgrade to Readest Premium')} onClick={handleUpgrade} />
)}
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
<MenuItem
@@ -4,15 +4,15 @@ import { MdCheck } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { LibraryCoverFitType, LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { navigateToLibrary } from '@/utils/nav';
import MenuItem from '@/components/MenuItem';
interface SortMenuProps {
interface ViewMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
}
const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
@@ -22,12 +22,18 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const viewMode = settings.libraryViewMode;
const sortBy = settings.librarySortBy;
const isAscending = settings.librarySortAscending;
const coverFit = settings.libraryCoverFit;
const viewOptions = [
{ label: _('List'), value: 'list' },
{ label: _('Grid'), value: 'grid' },
];
const coverFitOptions = [
{ label: _('Crop'), value: 'crop' },
{ label: _('Fit'), value: 'fit' },
];
const sortByOptions = [
{ label: _('Title'), value: 'title' },
{ label: _('Author'), value: 'author' },
@@ -52,6 +58,17 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
navigateToLibrary(router, `${params.toString()}`);
};
const handleToggleCropCovers = (value: LibraryCoverFitType) => {
settings.libraryCoverFit = value;
setSettings(settings);
saveSettings(envConfig, settings);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
params.set('cover', value);
navigateToLibrary(router, `${params.toString()}`);
};
const handleSetSortBy = (value: LibrarySortByType) => {
settings.librarySortBy = value;
setSettings(settings);
@@ -89,6 +106,22 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
/>
))}
<hr className='border-base-200 my-1' />
<MenuItem
label={_('Book Covers')}
buttonClass='h-8'
labelClass='text-sm sm:text-xs'
disabled
/>
{coverFitOptions.map((option) => (
<MenuItem
key={option.value}
label={option.label}
buttonClass='h-8'
Icon={coverFit === option.value ? MdCheck : undefined}
onClick={() => handleToggleCropCovers(option.value as LibraryCoverFitType)}
/>
))}
<hr className='border-base-200 my-1' />
<MenuItem
label={_('Sort by...')}
buttonClass='h-8'
@@ -118,4 +151,4 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
);
};
export default SortMenu;
export default ViewMenu;
@@ -70,8 +70,9 @@ export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
const processOldBook = async (oldBook: Book) => {
const matchingBook = syncedBooks.find((newBook) => newBook.hash === oldBook.hash);
if (matchingBook) {
if (!matchingBook.deletedAt && matchingBook.uploadedAt && !oldBook.downloadedAt) {
if (!matchingBook.deletedAt && matchingBook.uploadedAt && !oldBook.coverDownloadedAt) {
await appService?.downloadBook(oldBook, true);
oldBook.coverImageUrl = await appService?.generateCoverImageUrl(oldBook);
}
const mergedBook =
matchingBook.updatedAt > oldBook.updatedAt
+230 -117
View File
@@ -2,19 +2,27 @@
import clsx from 'clsx';
import * as React from 'react';
import { useState, useRef, useEffect, Suspense } from 'react';
import { useState, useRef, useEffect, Suspense, useCallback } from 'react';
import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/navigation';
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from 'overlayscrollbars-react';
import 'overlayscrollbars/overlayscrollbars.css';
import { Book } from '@/types/book';
import { AppService } from '@/types/system';
import { navigateToLogin, navigateToReader } from '@/utils/nav';
import { getFilename, listFormater } from '@/utils/book';
import {
formatAuthors,
formatTitle,
getFilename,
getPrimaryLanguage,
listFormater,
} from '@/utils/book';
import { eventDispatcher } from '@/utils/event';
import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
import { parseOpenWithFiles } from '@/helpers/openWith';
import { isTauriAppPlatform } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
import { impactFeedback } from '@tauri-apps/plugin-haptics';
import { getCurrentWebview } from '@tauri-apps/api/webview';
@@ -26,13 +34,13 @@ import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { usePullToRefresh } from '@/hooks/usePullToRefresh';
import { useTheme } from '@/hooks/useTheme';
import { useUICSS } from '@/hooks/useUICSS';
import { useDemoBooks } from './hooks/useDemoBooks';
import { useBooksSync } from './hooks/useBooksSync';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { lockScreenOrientation } from '@/utils/bridge';
import { mountAdditionalFonts } from '@/utils/font';
import {
tauriHandleSetAlwaysOnTop,
tauriHandleToggleFullScreen,
@@ -41,11 +49,12 @@ import {
import { AboutWindow } from '@/components/AboutWindow';
import { UpdaterWindow } from '@/components/UpdaterWindow';
import { BookMetadata } from '@/libs/document';
import { BookDetailModal } from '@/components/metadata';
import { Toast } from '@/components/Toast';
import Spinner from '@/components/Spinner';
import LibraryHeader from './components/LibraryHeader';
import Bookshelf from './components/Bookshelf';
import BookDetailModal from '@/components/BookDetailModal';
import useShortcuts from '@/hooks/useShortcuts';
import DropIndicator from '@/components/DropIndicator';
@@ -68,7 +77,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
setCheckLastOpenBooks,
} = useLibraryStore();
const _ = useTranslation();
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
const insets = useSafeAreaInsets();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [loading, setLoading] = useState(false);
@@ -84,9 +92,13 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const [pendingNavigationBookIds, setPendingNavigationBookIds] = useState<string[] | null>(null);
const [isDragging, setIsDragging] = useState(false);
const demoBooks = useDemoBooks();
const containerRef = useRef<HTMLDivElement>(null);
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
const containerRef: React.MutableRefObject<HTMLDivElement | null> = useRef(null);
const pageRef = useRef<HTMLDivElement>(null);
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
useUICSS();
useOpenWithBooks();
const { pullLibrary, pushLibrary } = useBooksSync({
@@ -110,14 +122,12 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
},
});
useEffect(() => {
mountAdditionalFonts(document);
}, []);
useEffect(() => {
const doCheckAppUpdates = async () => {
if (appService?.hasUpdater && settings.autoCheckUpdates) {
await checkForAppUpdates(_);
} else if (appService?.hasUpdater === false) {
checkAppReleaseNotes();
}
};
if (settings.alwaysOnTop) {
@@ -125,7 +135,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
doCheckAppUpdates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
}, [appService?.hasUpdater, settings]);
useEffect(() => {
if (appService?.isMobileApp) {
@@ -184,6 +194,28 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
};
const handleRefreshLibrary = useCallback(async () => {
const appService = await envConfig.getAppService();
const settings = await appService.loadSettings();
const library = await appService.loadLibraryBooks();
setSettings(settings);
setLibrary(library);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [envConfig, appService]);
useEffect(() => {
if (appService?.hasWindow) {
const currentWebview = getCurrentWebview();
const unlisten = currentWebview.listen('close-reader-window', async () => {
handleRefreshLibrary();
});
return () => {
unlisten.then((fn) => fn());
};
}
return;
}, [appService, handleRefreshLibrary]);
useEffect(() => {
const libraryPage = document.querySelector('.library-page');
if (!appService?.isMobile) {
@@ -284,7 +316,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
navigateToReader(router, bookIds);
}
}
}, [pendingNavigationBookIds, router]);
}, [pendingNavigationBookIds, appService, router]);
useEffect(() => {
if (isInitiating.current) return;
@@ -312,12 +344,15 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
// Reuse the library from the store when we return from the reader
const library = libraryBooks.length > 0 ? libraryBooks : await appService.loadLibraryBooks();
setCheckOpenWithBooks(checkOpenWithBooks && (await handleOpenWithBooks(appService, library)));
setCheckLastOpenBooks(
checkLastOpenBooks &&
settings.openLastBooks &&
(await handleOpenLastBooks(appService, settings.lastOpenBooks, library)),
);
let opened = false;
if (checkOpenWithBooks) {
opened = await handleOpenWithBooks(appService, library);
}
setCheckOpenWithBooks(opened);
if (!opened && checkLastOpenBooks && settings.openLastBooks) {
opened = await handleOpenLastBooks(appService, settings.lastOpenBooks, library);
}
setCheckLastOpenBooks(opened);
setLibrary(library);
setLibraryLoaded(true);
@@ -401,9 +436,14 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
};
const selectFilesTauri = async () => {
const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS;
const exts = appService?.isIOSApp ? [] : SUPPORTED_FILE_EXTS;
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
// Cannot filter out files on Android since some content providers may not return the file name
if (appService?.isIOSApp) {
return files.filter((file) => {
const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown';
return SUPPORTED_FILE_EXTS.includes(fileExt);
});
}
return files;
};
@@ -430,74 +470,82 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}));
}, 500);
const handleBookUpload = async (book: Book) => {
try {
await appService?.uploadBook(book, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Book uploaded: {{title}}', {
title: book.title,
}),
});
return true;
} catch (err) {
if (err instanceof Error) {
if (err.message.includes('Not authenticated') && settings.keepLogin) {
settings.keepLogin = false;
setSettings(settings);
navigateToLogin(router);
return false;
} else if (err.message.includes('Insufficient storage quota')) {
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Insufficient storage quota'),
});
return false;
const handleBookUpload = useCallback(
async (book: Book) => {
try {
await appService?.uploadBook(book, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Book uploaded: {{title}}', {
title: book.title,
}),
});
return true;
} catch (err) {
if (err instanceof Error) {
if (err.message.includes('Not authenticated') && settings.keepLogin) {
settings.keepLogin = false;
setSettings(settings);
navigateToLogin(router);
return false;
} else if (err.message.includes('Insufficient storage quota')) {
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Insufficient storage quota'),
});
return false;
}
}
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Failed to upload book: {{title}}', {
title: book.title,
}),
});
return false;
}
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Failed to upload book: {{title}}', {
title: book.title,
}),
});
return false;
}
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[appService],
);
const handleBookDownload = async (book: Book) => {
try {
await appService?.downloadBook(book, false, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
await updateBook(envConfig, book);
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Book downloaded: {{title}}', {
title: book.title,
}),
});
return true;
} catch {
eventDispatcher.dispatch('toast', {
message: _('Failed to download book: {{title}}', {
title: book.title,
}),
type: 'error',
});
return false;
}
};
const handleBookDownload = useCallback(
async (book: Book, redownload = false) => {
try {
await appService?.downloadBook(book, false, redownload, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
await updateBook(envConfig, book);
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Book downloaded: {{title}}', {
title: book.title,
}),
});
return true;
} catch {
eventDispatcher.dispatch('toast', {
message: _('Failed to download book: {{title}}', {
title: book.title,
}),
type: 'error',
});
return false;
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[appService],
);
const handleBookDelete = async (book: Book) => {
try {
await appService?.deleteBook(book, !!book.uploadedAt);
await appService?.deleteBook(book, !!book.uploadedAt, true);
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
@@ -519,19 +567,71 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
};
const handleBookDeleteCloudBackup = async (book: Book) => {
try {
await appService?.deleteBook(book, !!book.uploadedAt, false);
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Deleted cloud backup of the book: {{title}}', {
title: book.title,
}),
});
return true;
} catch (e) {
console.error(e);
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Failed to delete cloud backup of the book', {
title: book.title,
}),
});
return false;
}
};
const handleUpdateMetadata = async (book: Book, metadata: BookMetadata) => {
book.metadata = metadata;
book.title = formatTitle(metadata.title);
book.author = formatAuthors(metadata.author);
book.primaryLanguage = getPrimaryLanguage(metadata.language);
book.updatedAt = Date.now();
if (metadata.coverImageBlobUrl || metadata.coverImageUrl || metadata.coverImageFile) {
book.coverImageUrl = metadata.coverImageBlobUrl || metadata.coverImageUrl;
try {
await appService?.updateCoverImage(
book,
metadata.coverImageBlobUrl || metadata.coverImageUrl,
metadata.coverImageFile,
);
} catch (error) {
console.warn('Failed to update cover image:', error);
}
}
if (isWebAppPlatform()) {
// Clear HTTP cover image URL if cover is updated with a local file
if (metadata.coverImageBlobUrl) {
metadata.coverImageUrl = undefined;
}
} else {
metadata.coverImageUrl = undefined;
}
metadata.coverImageBlobUrl = undefined;
metadata.coverImageFile = undefined;
await updateBook(envConfig, book);
};
const handleImportBooks = async () => {
setIsSelectMode(false);
console.log('Importing books...');
let files;
if (isTauriAppPlatform()) {
if (appService?.isIOSApp) {
files = (await selectFilesWeb()) as [File];
} else {
files = (await selectFilesTauri()) as [string];
}
files = (await selectFilesTauri()) as string[];
} else {
files = (await selectFilesWeb()) as [File];
files = (await selectFilesWeb()) as File[];
}
importBooks(files);
};
@@ -579,6 +679,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
className={clsx(
'library-page bg-base-200 text-base-content flex select-none flex-col overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.isLinuxApp && 'window-border',
appService?.hasRoundedWindow && 'rounded-window',
)}
>
@@ -599,34 +700,44 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
)}
{libraryLoaded &&
(libraryBooks.some((book) => !book.deletedAt) ? (
<div
ref={containerRef}
className={clsx(
'scroll-container drop-zone flex-grow overflow-y-auto',
isDragging && 'drag-over',
)}
style={{
paddingTop: '0px',
paddingRight: `${insets.right}px`,
paddingBottom: `${insets.bottom}px`,
paddingLeft: `${insets.left}px`,
<OverlayScrollbarsComponent
defer
ref={osRef}
options={{ scrollbars: { autoHide: 'scroll' } }}
events={{
initialized: (instance) => {
const { content } = instance.elements();
if (content) {
containerRef.current = content as HTMLDivElement;
}
},
}}
>
<DropIndicator />
<Bookshelf
libraryBooks={libraryBooks}
isSelectMode={isSelectMode}
isSelectAll={isSelectAll}
isSelectNone={isSelectNone}
handleImportBooks={handleImportBooks}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
handleSetSelectMode={handleSetSelectMode}
handleShowDetailsBook={handleShowDetailsBook}
booksTransferProgress={booksTransferProgress}
/>
</div>
<div
className={clsx('scroll-container drop-zone flex-grow', isDragging && 'drag-over')}
style={{
paddingTop: '0px',
paddingRight: `${insets.right}px`,
paddingBottom: `${insets.bottom}px`,
paddingLeft: `${insets.left}px`,
}}
>
<DropIndicator />
<Bookshelf
libraryBooks={libraryBooks}
isSelectMode={isSelectMode}
isSelectAll={isSelectAll}
isSelectNone={isSelectNone}
handleImportBooks={handleImportBooks}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
handleSetSelectMode={handleSetSelectMode}
handleShowDetailsBook={handleShowDetailsBook}
booksTransferProgress={booksTransferProgress}
/>
</div>
</OverlayScrollbarsComponent>
) : (
<div className='hero drop-zone h-screen items-center justify-center'>
<DropIndicator />
@@ -653,10 +764,12 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
handleBookDeleteCloudBackup={handleBookDeleteCloudBackup}
handleBookMetadataUpdate={handleUpdateMetadata}
/>
)}
<AboutWindow />
{appService?.isAndroidApp && <UpdaterWindow />}
<UpdaterWindow />
<Toast />
</div>
);
@@ -30,8 +30,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { getGridInsets, setGridInsets, hoveredBookKey } = useReaderStore();
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
const { setGridInsets, hoveredBookKey } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const screenInsets = useSafeAreaInsets();
@@ -46,16 +46,21 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sideBarBookKey]);
const calcGridInsets = (index: number, count: number) => {
if (!screenInsets) return { top: 0, right: 0, bottom: 0, left: 0 };
const { top, right, bottom, left } = getInsetEdges(index, count, aspectRatio);
return {
top: top ? screenInsets.top : 0,
right: right ? screenInsets.right : 0,
bottom: bottom ? screenInsets.bottom : 0,
left: left ? screenInsets.left : 0,
};
};
useEffect(() => {
if (!screenInsets) return;
bookKeys.forEach((bookKey, index) => {
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
const gridInsets = {
top: top ? screenInsets.top : 0,
right: right ? screenInsets.right : 0,
bottom: bottom ? screenInsets.bottom : 0,
left: left ? screenInsets.left : 0,
};
const gridInsets = calcGridInsets(index, bookKeys.length);
setGridInsets(bookKey, gridInsets);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -65,7 +70,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
return (
<div
className={clsx('relative grid h-full flex-grow')}
className={clsx('books-grid bg-base-100 relative grid h-full flex-grow')}
style={{
gridTemplateColumns: gridTemplate.columns,
gridTemplateRows: gridTemplate.rows,
@@ -76,9 +81,9 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const gridInsets = getGridInsets(bookKey);
const gridInsets = calcGridInsets(index, bookKeys.length);
const { book, bookDoc } = bookData || {};
if (!book || !config || !bookDoc || !viewSettings || !gridInsets) return null;
if (!book || !config || !bookDoc || !viewSettings) return null;
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
@@ -101,7 +106,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
key={bookKey}
className={clsx(
'relative h-full w-full overflow-hidden',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
appService?.hasRoundedWindow && 'rounded-window',
)}
>
{isBookmarked && !hoveredBookKey && <Ribbon width={`${horizontalGapPercent}%`} />}
@@ -148,11 +153,12 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
showFooter={showFooter}
borderColor={viewSettings.borderColor}
horizontalGap={horizontalGapPercent}
contentInsets={contentInsets}
insets={viewInsets}
/>
)}
{showHeader && (
<SectionInfo
bookKey={bookKey}
section={sectionLabel}
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
isScrolled={viewSettings.scrolled}
@@ -5,7 +5,7 @@ interface DoubleBorderProps {
horizontalGap: number;
showHeader: boolean;
showFooter: boolean;
contentInsets: Insets;
insets: Insets;
}
const paddingPx = 10;
@@ -14,7 +14,7 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderColor,
showHeader,
showFooter,
contentInsets,
insets,
}) => {
return (
<div>
@@ -23,10 +23,10 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
className={'borderframe pointer-events-none absolute'}
style={{
border: `4px solid ${borderColor}`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px + ${paddingPx * 2}px)`,
top: `calc(${contentInsets.top}px - ${paddingPx}px)`,
left: `calc(${contentInsets.left}px - ${paddingPx}px)`,
right: `calc(${contentInsets.right}px - ${paddingPx}px)`,
height: `calc(100% - ${insets.top + insets.bottom}px + ${paddingPx * 2}px)`,
top: `calc(${insets.top}px - ${paddingPx}px)`,
left: `calc(${insets.left}px - ${paddingPx}px)`,
right: `calc(${insets.right}px - ${paddingPx}px)`,
}}
></div>
{/* inner frame */}
@@ -34,10 +34,10 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
className={'borderframe pointer-events-none absolute'}
style={{
border: `1px solid ${borderColor}`,
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(${contentInsets.left + (showFooter ? 32 : 0)}px`,
right: `calc(${contentInsets.right + (showHeader ? 32 : 0)}px`,
height: `calc(100% - ${insets.top + insets.bottom}px)`,
top: `${insets.top}px`,
left: `calc(${insets.left + (showFooter ? 32 : 0)}px`,
right: `calc(${insets.right + (showHeader ? 32 : 0)}px`,
}}
/>
{/* footer */}
@@ -49,9 +49,9 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderBottom: `1px solid ${borderColor}`,
borderLeft: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(${contentInsets.left}px)`,
height: `calc(100% - ${insets.top + insets.bottom}px)`,
top: `${insets.top}px`,
left: `calc(${insets.left}px)`,
}}
/>
)}
@@ -64,9 +64,9 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderBottom: `1px solid ${borderColor}`,
borderRight: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
top: `${contentInsets.top}px`,
left: `calc(100% - ${contentInsets.right}px - 32px)`,
height: `calc(100% - ${insets.top + insets.bottom}px)`,
top: `${insets.top}px`,
left: `calc(100% - ${insets.right}px - 32px)`,
}}
/>
)}
@@ -13,7 +13,13 @@ import { usePagination } from '../hooks/usePagination';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { getStyles, transformStylesheet } from '@/utils/style';
import {
applyFixedlayoutStyles,
applyImageStyle,
applyTranslationStyle,
getStyles,
transformStylesheet,
} from '@/utils/style';
import { mountAdditionalFonts } from '@/utils/font';
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
import { useUICSS } from '@/hooks/useUICSS';
@@ -30,10 +36,18 @@ import {
import { getMaxInlineSize } from '@/utils/config';
import { getDirFromUILanguage } from '@/utils/rtl';
import { isCJKLang } from '@/utils/lang';
import { isTauriAppPlatform } from '@/services/environment';
import { transformContent } from '@/services/transformService';
import { lockScreenOrientation } from '@/utils/bridge';
import { useTextTranslation } from '../hooks/useTextTranslation';
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
import { getViewInsets } from '@/utils/insets';
declare global {
interface Window {
eval(script: string): void;
}
}
const FoliateViewer: React.FC<{
bookKey: string;
@@ -52,6 +66,7 @@ const FoliateViewer: React.FC<{
const viewRef = useRef<FoliateView | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isViewCreated = useRef(false);
const doubleClickDisabled = useRef(!!viewSettings?.disableDoubleClick);
const [toastMessage, setToastMessage] = useState('');
useEffect(() => {
@@ -121,6 +136,17 @@ const FoliateViewer: React.FC<{
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
if (bookDoc.rendition?.layout === 'pre-paginated') {
applyFixedlayoutStyles(detail.doc, viewSettings);
}
applyImageStyle(detail.doc);
// Inline scripts in tauri platforms are not executed by default
if (viewSettings.allowScript && isTauriAppPlatform()) {
evalInlineScripts(detail.doc);
}
// only call on load if we have highlighting turned on.
if (viewSettings.codeHighlighting) {
manageSyntaxHighlighting(detail.doc, viewSettings);
@@ -134,7 +160,7 @@ const FoliateViewer: React.FC<{
detail.doc.addEventListener('keydown', handleKeydown.bind(null, bookKey));
detail.doc.addEventListener('mousedown', handleMousedown.bind(null, bookKey));
detail.doc.addEventListener('mouseup', handleMouseup.bind(null, bookKey));
detail.doc.addEventListener('click', handleClick.bind(null, bookKey));
detail.doc.addEventListener('click', handleClick.bind(null, bookKey, doubleClickDisabled));
detail.doc.addEventListener('wheel', handleWheel.bind(null, bookKey));
detail.doc.addEventListener('touchstart', handleTouchStart.bind(null, bookKey));
detail.doc.addEventListener('touchmove', handleTouchMove.bind(null, bookKey));
@@ -143,6 +169,22 @@ const FoliateViewer: React.FC<{
}
};
const evalInlineScripts = (doc: Document) => {
if (doc.defaultView && doc.defaultView.frameElement) {
const iframe = doc.defaultView.frameElement as HTMLIFrameElement;
const scripts = doc.querySelectorAll('script:not([src])');
scripts.forEach((script, index) => {
const scriptContent = script.textContent || script.innerHTML;
try {
console.warn('Evaluating inline scripts in iframe');
iframe.contentWindow?.eval(scriptContent);
} catch (error) {
console.error(`Error executing iframe script ${index + 1}:`, error);
}
});
}
};
const docRelocateHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
@@ -213,7 +255,9 @@ const FoliateViewer: React.FC<{
const height = viewHeight - insets.top - insets.bottom;
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
view.renderer.setStyles?.(getStyles(viewSettings));
applyTranslationStyle(viewSettings);
doubleClickDisabled.current = viewSettings.disableDoubleClick!;
const animated = viewSettings.animated!;
const maxColumnCount = viewSettings.maxColumnCount!;
const maxInlineSize = getMaxInlineSize(viewSettings);
@@ -246,20 +290,25 @@ const FoliateViewer: React.FC<{
const applyMarginAndGap = () => {
const viewSettings = getViewSettings(bookKey)!;
const viewInsets = getViewInsets(viewSettings);
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
const showDoubleBorderFooter = showDoubleBorder && viewSettings.showFooter;
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
viewRef.current?.renderer.setAttribute('margin-top', `${insets.top + moreTopInset}px`);
viewRef.current?.renderer.setAttribute('margin-right', `${insets.right + moreRightInset}px`);
viewRef.current?.renderer.setAttribute('margin-bottom', `${insets.bottom + moreBottomInset}px`);
viewRef.current?.renderer.setAttribute('margin-left', `${insets.left + moreLeftInset}px`);
const topMargin = (showTopHeader ? insets.top : viewInsets.top) + moreTopInset;
const rightMargin = insets.right + moreRightInset;
const bottomMargin = (showBottomFooter ? insets.bottom : viewInsets.bottom) + moreBottomInset;
const leftMargin = insets.left + moreLeftInset;
viewRef.current?.renderer.setAttribute('margin-top', `${topMargin}px`);
viewRef.current?.renderer.setAttribute('margin-right', `${rightMargin}px`);
viewRef.current?.renderer.setAttribute('margin-bottom', `${bottomMargin}px`);
viewRef.current?.renderer.setAttribute('margin-left', `${leftMargin}px`);
viewRef.current?.renderer.setAttribute('gap', `${viewSettings.gapPercent}%`);
if (viewSettings.scrolled) {
viewRef.current?.renderer.setAttribute('flow', 'scrolled');
@@ -270,9 +319,19 @@ const FoliateViewer: React.FC<{
if (viewRef.current && viewRef.current.renderer) {
const viewSettings = getViewSettings(bookKey)!;
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
if (bookDoc.rendition?.layout === 'pre-paginated') {
const docs = viewRef.current.renderer.getContents();
docs.forEach(({ doc }) => applyFixedlayoutStyles(doc, viewSettings));
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode, isDarkMode]);
}, [themeCode, isDarkMode, viewSettings?.overrideColor, viewSettings?.invertImgColorInDark]);
useEffect(() => {
if (viewRef.current && viewRef.current.renderer) {
doubleClickDisabled.current = !!viewSettings?.disableDoubleClick;
}
}, [viewSettings?.disableDoubleClick]);
useEffect(() => {
if (viewRef.current && viewRef.current.renderer && viewSettings) {
@@ -15,6 +15,7 @@ import { RxLineHeight } from 'react-icons/rx';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { eventDispatcher } from '@/utils/event';
@@ -45,6 +46,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
}) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getConfig, setConfig } = useBookDataStore();
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
const { getView, getViewState, getProgress, getViewSettings } = useReaderStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
@@ -55,6 +57,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
const marginIconSize = useResponsiveSize(20);
const view = getView(bookKey);
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const viewState = getViewState(bookKey);
@@ -131,15 +134,17 @@ const FooterBar: React.FC<FooterBarProps> = ({
handleSpeakText();
} else if (tab === 'toc') {
setHoveredBookKey('');
if (viewSettings) {
viewSettings.sideBarTab = 'toc';
if (config && config.viewSettings) {
config.viewSettings.sideBarTab = 'toc';
setConfig(bookKey, config);
}
setSideBarVisible(true);
} else if (tab === 'note') {
setHoveredBookKey('');
setSideBarVisible(true);
if (viewSettings) {
viewSettings.sideBarTab = 'annotations';
if (config && config.viewSettings) {
config.viewSettings.sideBarTab = 'annotations';
setConfig(bookKey, config);
}
}
};
@@ -163,6 +168,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
? (progressInfo!.current + 1) / progressInfo!.total || 0
: 0;
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
return (
<>
<div
@@ -176,10 +183,11 @@ const FooterBar: React.FC<FooterBarProps> = ({
/>
<div
className={clsx(
'footer-bar shadow-xs absolute bottom-0 z-50 flex w-full flex-col',
'footer-bar shadow-xs bottom-0 z-50 flex w-full flex-col',
'sm:h-[52px] sm:justify-center',
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
'transition-[opacity,transform] duration-300',
appService?.isMobile ? 'fixed' : 'absolute',
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
@@ -196,13 +204,13 @@ const FooterBar: React.FC<FooterBarProps> = ({
{/* Mobile footer bar */}
<div
className={clsx(
'bg-base-200 absolute bottom-16 flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
'footerbar-progress-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
actionTab === 'progress'
? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out'
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 64}px` : '64px',
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
}}
>
<div className='flex w-full items-center justify-between gap-x-6'>
@@ -250,13 +258,13 @@ const FooterBar: React.FC<FooterBarProps> = ({
</div>
<div
className={clsx(
'bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
'footerbar-font-mobile bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
actionTab === 'font'
? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out'
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 64}px` : '64px',
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
}}
>
<Slider
@@ -298,7 +306,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
)}
style={{
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 16}px` : '0px',
paddingBottom: isMobile ? `${gridInsets.bottom + 16}px` : '0px',
}}
>
<Button
@@ -328,7 +336,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
<div
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
style={{
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom / 2}px` : '0px',
bottom: isMobile ? `${gridInsets.bottom / 2}px` : '0px',
}}
>
<Button
@@ -40,7 +40,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
const { appService } = useEnv();
const headerRef = useRef<HTMLDivElement>(null);
const {
isTrafficLightVisible,
trafficLightInFullscreen,
setTrafficLightVisibility,
initializeTrafficLightStore,
initializeTrafficLightListeners,
@@ -62,21 +62,27 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
initializeTrafficLightStore(appService);
initializeTrafficLightListeners();
setTrafficLightVisibility(true);
return () => {
cleanupTrafficLightListeners();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [appService]);
useEffect(() => {
if (!appService?.hasTrafficLight) return;
if (isSideBarVisible) return;
setTrafficLightVisibility(isSideBarVisible);
if (hoveredBookKey === bookKey && isTopLeft) {
setTrafficLightVisibility(true, { x: 10, y: 20 });
} else if (!hoveredBookKey) {
setTrafficLightVisibility(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSideBarVisible]);
}, [appService, isSideBarVisible, hoveredBookKey]);
const isVisible = hoveredBookKey === bookKey || isDropdownOpen;
const isHeaderVisible = hoveredBookKey === bookKey || isDropdownOpen;
const trafficLightInHeader =
appService?.hasTrafficLight && !trafficLightInFullscreen && !isSideBarVisible && isTopLeft;
return (
<div
@@ -86,14 +92,15 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
}}
>
<div
className={clsx('absolute top-0 z-10 hidden h-11 w-full sm:flex')}
className={clsx('absolute top-0 z-10 h-11 w-full')}
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
/>
<div
className={clsx(
'bg-base-100 absolute left-0 right-0 top-0 z-10',
isVisible ? 'visible' : 'hidden',
appService?.hasRoundedWindow && 'rounded-window-top-right',
isHeaderVisible ? 'visible' : 'hidden',
)}
style={{
height: systemUIVisible ? `${Math.max(gridInsets.top, statusBarHeight)}px` : '0px',
@@ -104,11 +111,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
className={clsx(
`header-bar bg-base-100 absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
`shadow-xs transition-[opacity,margin-top] duration-300`,
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
trafficLightInHeader ? 'pl-20' : 'pl-4',
appService?.hasRoundedWindow && 'rounded-window-top-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
isHoveredAnim && 'hover-bar-anim',
isVisible ? 'pointer-events-auto visible' : 'pointer-events-none opacity-0',
isHeaderVisible ? 'pointer-events-auto visible' : 'pointer-events-none opacity-0',
isDropdownOpen && 'header-bar-pinned',
)}
style={{
@@ -118,7 +125,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
>
<div className='sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4'>
<div className='bg-base-100 sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4 pe-2'>
<div className='hidden sm:flex'>
<SidebarToggler bookKey={bookKey} />
</div>
@@ -132,7 +139,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
</h2>
</div>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4 ps-2'>
<SettingsToggler />
<NotebookToggler bookKey={bookKey} />
<Dropdown
@@ -148,10 +155,14 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
className='window-buttons flex h-full items-center'
headerRef={headerRef}
showMinimize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
bookKeys.length == 1 &&
!appService?.hasTrafficLight &&
appService?.appPlatform !== 'web'
}
showMaximize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
bookKeys.length == 1 &&
!appService?.hasTrafficLight &&
appService?.appPlatform !== 'web'
}
onClose={() => {
setHoveredBookKey(null);
@@ -29,7 +29,8 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getViewSettings } = useReaderStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
@@ -41,15 +42,18 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
? `${section.current + 1} · ${section.total}`
: `${section.current + 1} / ${section.total}`
: ''
: pageinfo
: pageinfo && pageinfo.current >= 0 && pageinfo.total > 0
? _(isVertical ? '{{currentPage}} · {{totalPage}}' : 'Loc. {{currentPage}} / {{totalPage}}', {
currentPage: pageinfo.current + 1,
totalPage: pageinfo.total,
})
: '';
const timeInfo = timeinfo
const timeLeft = timeinfo
? _('{{time}} min left in chapter', { time: Math.round(timeinfo.section) })
: '';
const { page = 0, pages = 0 } = view?.renderer || {};
const pageLeft =
pages - 1 > page ? _('{{count}} pages left in chapter', { count: pages - 1 - page }) : '';
return (
<div
@@ -76,7 +80,11 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
}
}
>
{viewSettings.showRemainingTime && <span className='text-start'>{timeInfo}</span>}
{viewSettings.showRemainingTime ? (
<span className='text-start'>{timeLeft}</span>
) : viewSettings.showRemainingPages ? (
<span className='text-start'>{pageLeft}</span>
) : null}
{viewSettings.showPageNumber && <span className='ms-auto text-end'>{pageInfo}</span>}
</div>
);
@@ -111,14 +111,15 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
settings.globalReadSettings && (
<div
className={clsx(
`reader-page bg-base-100 text-base-content select-none`,
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
`reader-page text-base-content select-none overflow-hidden`,
appService?.isLinuxApp && 'window-border',
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<Suspense>
<ReaderContent ids={ids} settings={settings} />
<AboutWindow />
{appService?.isAndroidApp && <UpdaterWindow />}
<UpdaterWindow />
<Toast />
</Suspense>
</div>
@@ -5,24 +5,27 @@ import * as React from 'react';
import { useState, useRef, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Book } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { Book } from '@/types/book';
import { SystemSettings } from '@/types/settings';
import { parseOpenWithFiles } from '@/helpers/openWith';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { UnlistenFn } from '@tauri-apps/api/event';
import { tauriHandleClose, tauriHandleOnCloseWindow } from '@/utils/window';
import { isTauriAppPlatform } from '@/services/environment';
import { uniqueId } from '@/utils/misc';
import { throttle } from '@/utils/throttle';
import { eventDispatcher } from '@/utils/event';
import { navigateToLibrary } from '@/utils/nav';
import { BOOK_IDS_SEPARATOR } from '@/services/constants';
import { BookDetailModal } from '@/components/metadata';
import useBooksManager from '../hooks/useBooksManager';
import useBookShortcuts from '../hooks/useBookShortcuts';
import BookDetailModal from '@/components/BookDetailModal';
import Spinner from '@/components/Spinner';
import SideBar from './sidebar/SideBar';
import Notebook from './notebook/Notebook';
@@ -82,12 +85,18 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
saveSettings(envConfig, settings);
}
if (isTauriAppPlatform()) tauriHandleOnCloseWindow(handleCloseBooks);
let unlistenOnCloseWindow: Promise<UnlistenFn>;
if (isTauriAppPlatform()) {
unlistenOnCloseWindow = tauriHandleOnCloseWindow(handleCloseBooks);
}
window.addEventListener('beforeunload', handleCloseBooks);
eventDispatcher.on('beforereload', handleCloseBooks);
eventDispatcher.on('quit-app', handleCloseBooks);
return () => {
window.removeEventListener('beforeunload', handleCloseBooks);
eventDispatcher.off('beforereload', handleCloseBooks);
eventDispatcher.off('quit-app', handleCloseBooks);
unlistenOnCloseWindow?.then((fn) => fn());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bookKeys]);
@@ -121,11 +130,11 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
navigateToLibrary(router);
};
const handleCloseBooks = async () => {
const handleCloseBooks = throttle(async () => {
const settings = useSettingsStore.getState().settings;
await Promise.all(bookKeys.map((key) => saveConfigAndCloseBook(key)));
await saveSettings(envConfig, settings);
};
}, 200);
const handleCloseBooksToLibrary = () => {
handleCloseBooks();
@@ -140,11 +149,16 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
dismissBook(bookKey);
if (bookKeys.filter((key) => key !== bookKey).length == 0) {
const openWithFiles = (await parseOpenWithFiles()) || [];
if (openWithFiles.length > 0 && !appService?.isMobile) {
tauriHandleClose();
} else {
saveSettingsAndGoToLibrary();
if (appService?.hasWindow) {
if (openWithFiles.length > 0) {
return await tauriHandleClose();
}
const currentWindow = getCurrentWindow();
if (currentWindow.label.startsWith('reader')) {
return await currentWindow.close();
}
}
saveSettingsAndGoToLibrary();
}
};
@@ -163,7 +177,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
}
return (
<div className={clsx('flex', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
<div className={clsx('reader-content flex', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
<Notebook />

Some files were not shown because too many files have changed in this diff Show More