Compare commits
962 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0177a03a90 | |||
| f5df80f154 | |||
| 76b239f382 | |||
| 2f430bf2ff | |||
| 0e8605d298 | |||
| 7445d5befe | |||
| e41bcfbac9 | |||
| 91bc4ddec7 | |||
| e8f70b896e | |||
| 1f4f5f8a6d | |||
| 764acf59de | |||
| eab83c6d3f | |||
| 7f62cdcab9 | |||
| e4ab06abcd | |||
| fef0aa6947 | |||
| 2935eacb1c | |||
| ad9bb58cd4 | |||
| 871d6467c3 | |||
| 3f19ce24de | |||
| 8280585e70 | |||
| 1f7483b911 | |||
| b2bb92036b | |||
| 3831f24ff7 | |||
| 3bec89c85d | |||
| 3dfe6bd65e | |||
| b00d321817 | |||
| 1af55e24da | |||
| f7b2a2432c | |||
| db3dee025b | |||
| 25352efece | |||
| 1297e33c62 | |||
| 8274c6bc4f | |||
| ed5bbd25d6 | |||
| d06e1849cc | |||
| 34ad6e6749 | |||
| b163012488 | |||
| f9a3559086 | |||
| 674a0bf16d | |||
| 9b53ccc9de | |||
| 7a605a357a | |||
| af649edd0d | |||
| 64a71e25e4 | |||
| 51a3767940 | |||
| a8572ab7c6 | |||
| 907b672313 | |||
| 8df9f909b4 | |||
| bcd7a90f27 | |||
| 5646e0cfb5 | |||
| fad27d74a0 | |||
| 5fbbfa523b | |||
| 569c6707a4 | |||
| 99f8a29326 | |||
| 04737d6f35 | |||
| 8850e6c00f | |||
| 93b96d64eb | |||
| 9f8894c1e0 | |||
| dac4f2e8ec | |||
| 54bc1514df | |||
| 97cab2d70b | |||
| 13588b4a65 | |||
| 0f5bd5ca1c | |||
| a74c1a56a9 | |||
| 1e9bd1d821 | |||
| 02cc0a9ebb | |||
| 5273ef75dc | |||
| bd957a4eb8 | |||
| 38552a0c2e | |||
| 0609e828b1 | |||
| 35ade2c855 | |||
| b68c14da1f | |||
| 5c41394961 | |||
| 5685944599 | |||
| 94e761f681 | |||
| 7f636a2072 | |||
| 480b8b98a3 | |||
| d1fb67316f | |||
| f0e9ddd2ae | |||
| b16a4445ae | |||
| 515d47e64d | |||
| 3fd78c4ed8 | |||
| 152a95941a | |||
| bf5805910d | |||
| 431d14f4a4 | |||
| 0d2e5b7c76 | |||
| e949476d27 | |||
| 0afff573a1 | |||
| d4bb61f12b | |||
| 09c45b4615 | |||
| d8eef87bf0 | |||
| 66d2fdf999 | |||
| d8e0ceeff1 | |||
| f3ad97b989 | |||
| 1bb49ab023 | |||
| f9a0b39586 | |||
| c533da498d | |||
| b50bc0b854 | |||
| 96c465931c | |||
| 2bd54ac236 | |||
| 6ad549d13c | |||
| 67249370c9 | |||
| fe3ab011ca | |||
| 6cb4278b98 | |||
| 51468862a2 | |||
| b3a44d066f | |||
| 68d4538d40 | |||
| 80105af839 | |||
| 3904c1e8e7 | |||
| 99be6c58e2 | |||
| 664cc772d0 | |||
| 21208adbcf | |||
| 70eb59e2d6 | |||
| 3e7b57282e | |||
| 534582b125 | |||
| 4465e6986e | |||
| a535f6419e | |||
| 65da9c1d47 | |||
| 5f71fd9e47 | |||
| dcf75e07d1 | |||
| 79ba9b3818 | |||
| 128b238bcb | |||
| e26f7e6a2c | |||
| 4c5ff59bcf | |||
| 99b2a34bd2 | |||
| 40f3268ef3 | |||
| 4dac0850c5 | |||
| 118538ba35 | |||
| 4a92cacd84 | |||
| ce53cd2b47 | |||
| b99c1bc19a | |||
| eec2c39f19 | |||
| c6ae85484e | |||
| 7a9f46e93c | |||
| b9f6578127 | |||
| e75a3d254e | |||
| ea15906acf | |||
| 15d2784725 | |||
| ae3bb9da9a | |||
| 239b32fcc2 | |||
| f64739419b | |||
| af8f036ca3 | |||
| 24a87508c6 | |||
| 548d50a882 | |||
| 1e81ab5205 | |||
| a47a480aa2 | |||
| 411f9e236d | |||
| 950bbd0821 | |||
| 2824e50b51 | |||
| 129720c916 | |||
| fd3533dba1 | |||
| 920032155b | |||
| 226bf7033e | |||
| 9444be7fcc | |||
| c9a69a922b | |||
| 9a11b05833 | |||
| 6e603ee38f | |||
| 03fd6e2e6f | |||
| 7dd11c0fb0 | |||
| 968597e52c | |||
| a534050b19 | |||
| 0be828fd66 | |||
| c6d4e2bdd6 | |||
| bf72ab86cd | |||
| fb49ddf484 | |||
| d8817a88b9 | |||
| 475becafe3 | |||
| df165576e6 | |||
| 051f2e5b13 | |||
| 83f0d135c5 | |||
| 1ed84a3256 | |||
| 9d6394fe2b | |||
| fe9603ffb8 | |||
| 681e065ac4 | |||
| f64fc5723e | |||
| a9a1dc8e70 | |||
| b1a1e35790 | |||
| 3c538c3746 | |||
| 9834bd57cf | |||
| b89171a4d8 | |||
| 92116e7455 | |||
| 8dfab2c963 | |||
| 9f261f12e9 | |||
| e74615ac1d | |||
| 3b350d6945 | |||
| 9e0e3fde7d | |||
| 52c49ddef1 | |||
| e3d52891fb | |||
| 9cd88fe839 | |||
| fca3917a12 | |||
| c90de6967b | |||
| bbbd378f9b | |||
| 9f7147f8f8 | |||
| 8a468a6d1f | |||
| c848a319ff | |||
| 5cc80db438 | |||
| 94930baa1b | |||
| 1294ef90c1 | |||
| 570598520f | |||
| 7b4fc91994 | |||
| fb387c2384 | |||
| 4ce1ebe477 | |||
| c1460f4b85 | |||
| bc94f3f790 | |||
| c8c761b017 | |||
| bdb25999c9 | |||
| c58e172a54 | |||
| 160efcd37b | |||
| d7470f4139 | |||
| 01b4e3530d | |||
| 09c62d442f | |||
| d62ad60ce8 | |||
| 8cd34c8aaa | |||
| a52d9e3a2b | |||
| 64a1ea531a | |||
| 19f9f5ea24 | |||
| ffc51e75de | |||
| 2100071991 | |||
| 563d3478ba | |||
| 2c54e9ae2f | |||
| c2eb2e2fcc | |||
| e592f40429 | |||
| 4bd7cfe57c | |||
| c31ece6742 | |||
| aba9e87fc1 | |||
| 2c4df601d8 | |||
| 8bfc90a5b0 | |||
| 8bea05478a | |||
| 42fee90a27 | |||
| 45e57c3943 | |||
| 51b0b8483f | |||
| cbf7a501b7 | |||
| edfcb75ba5 | |||
| ce76843ccc | |||
| 5bbc5ceccc | |||
| 224acd68b1 | |||
| 6c0f1b8b5f | |||
| 481d8198e9 | |||
| b1153ba051 | |||
| aad532bfdd | |||
| a71347c897 | |||
| 034f41ca10 | |||
| 539ad8dea2 | |||
| 48920a87bf | |||
| d1d0d2d59c | |||
| ea811c90c6 | |||
| 5cd5b65e83 | |||
| ea24b5a97a | |||
| 06d620db83 | |||
| 0c25b85a8a | |||
| c536450ab0 | |||
| c83e380c5a | |||
| 894a7551aa | |||
| f875ba88ac | |||
| d9a6cffe78 | |||
| 1704736bc8 | |||
| 6ce9a263ee | |||
| 038eca4267 | |||
| f0722ec0fe | |||
| fd299a61a7 | |||
| 36b7386e30 | |||
| 2670b0dc0b | |||
| c44705e269 | |||
| 7e618d456e | |||
| b28ac99a9e | |||
| 2d7d6b08a9 | |||
| b1419f9c01 | |||
| 32ea42a835 | |||
| 6083de3293 | |||
| 1c9cfa49b3 | |||
| f85d6d4293 | |||
| 017d0b0b39 | |||
| 3146ae48a7 | |||
| 7f26e45ae7 | |||
| 7d97826e4b | |||
| 9c9c79176d | |||
| ed476a4fce | |||
| 44b4f7995b | |||
| da5e3a0bd3 | |||
| ba4678cc23 | |||
| e5eb3014b4 | |||
| ed77b0bc7f | |||
| 434a44e62c | |||
| aaee04c290 | |||
| 48e2bfa82c | |||
| c04f19ffb4 | |||
| 4a624e3902 | |||
| f53cee9616 | |||
| 30385ee5ec | |||
| b30dfb3e23 | |||
| c0d6102857 | |||
| 4537c55e84 | |||
| aff94c0ab8 | |||
| fd70836308 | |||
| 1b0c94b9a5 | |||
| 7cb523eefc | |||
| 941be80cc4 | |||
| 5eecc735aa | |||
| cfe51d01ee | |||
| 20ae09c52d | |||
| b868146129 | |||
| a312080f7c | |||
| 462ca46fee | |||
| 93228c4b2a | |||
| 2ff10f781f | |||
| 9614c62360 | |||
| 5ffaac5e67 | |||
| 71af91608f | |||
| 9603b61776 | |||
| eed84a059a | |||
| 604ef65719 | |||
| 483d536ca4 | |||
| 69a51c5880 | |||
| ca8d25341e | |||
| 8a263235ed | |||
| fb41ff5d0c | |||
| a5e09e8454 | |||
| 730ee21651 | |||
| dea43445c3 | |||
| 4f0ae78d43 | |||
| d9116d619a | |||
| c080e6fdd3 | |||
| d3752dadc6 | |||
| e21ef53a3d | |||
| 71e97998b6 | |||
| 22f9c45232 | |||
| a42897ec5c | |||
| 58a5c1625c | |||
| d53fc09e1e | |||
| 11fecb5dc0 | |||
| f4587663b5 | |||
| b76a2adf61 | |||
| 988fbc8c85 | |||
| f944ad9b9f | |||
| 335d91b9c9 | |||
| 173404eaad | |||
| 674fed5230 | |||
| 12d284cd22 | |||
| 77037c8adb | |||
| 6984393ed1 | |||
| 3abef6ea75 | |||
| 4ae1ab7dd0 | |||
| 69fb22119b | |||
| 3a6c18c6d5 | |||
| 7db1bc460d | |||
| a460e609fa | |||
| 4b4ebdbdaa | |||
| 9358a06839 | |||
| a7baf6cc9f | |||
| 15dc669f35 | |||
| c853957512 | |||
| 8a4e22e423 | |||
| 8a43c58fd4 | |||
| 54fdf5f1fd | |||
| fe50b513b3 | |||
| 17c7fa8f41 | |||
| 2533560d11 | |||
| 5850a16afd | |||
| 7063d62b13 | |||
| 0bd6a217ae | |||
| b7df294d78 | |||
| 5aa78f2554 | |||
| e740571c33 | |||
| e6d9913f4e | |||
| 1869a863a3 | |||
| 524de92f5e | |||
| c1530cc5c4 | |||
| 730fadb834 | |||
| 383e5c61b1 | |||
| 6d42086fa7 | |||
| 5a20fae204 | |||
| 34fd64c5c4 | |||
| 0874fb0764 | |||
| e03ed5b604 | |||
| 41edc89ac7 | |||
| f0a470398d | |||
| 51008d81fb | |||
| 9828904674 | |||
| 2670d835b3 | |||
| 8b7bafc4b6 | |||
| ca759e0246 | |||
| 5141be1c3f | |||
| 669d3950e2 | |||
| b95895cecf | |||
| 80e11bb0ce | |||
| de3a539621 | |||
| b425bfdc89 | |||
| 1d1fbdffdb | |||
| 50cd7f80c6 | |||
| 6eb7d91122 | |||
| 1fb468b3a6 | |||
| 3c7d95cf10 | |||
| ba3f060cc4 | |||
| 11bc7497e8 | |||
| fb5d149413 | |||
| 42b47d73b7 | |||
| 00f36af03a | |||
| b78466ca93 | |||
| 4e6f146b8f | |||
| cbdd4940d0 | |||
| d022cb984a | |||
| 8de6fa267e | |||
| b08b7de8e9 | |||
| a232a39f0e | |||
| fad7966fc4 | |||
| a1487fd60c | |||
| 9606e315d4 | |||
| 978673268b | |||
| 70158a7f15 | |||
| 18d65a2c5b | |||
| 1b0c2afad7 | |||
| cef444d374 | |||
| 75f6efe27a | |||
| 852f9f40ec | |||
| b9dadc0f4f | |||
| d120cf6573 | |||
| d2b3c165c7 | |||
| 742f06ce05 | |||
| e28dabce65 | |||
| 33ef781e21 | |||
| 02e885fbb7 | |||
| 2ca3561093 | |||
| 97f021da87 | |||
| bd2b45e1c1 | |||
| 2b6f7b71b0 | |||
| 0bad380a50 | |||
| ecc20e8521 | |||
| d5c813fac1 | |||
| d9b3757b1c | |||
| fd7f90236d | |||
| df9d21393d | |||
| 1f73f15ad8 | |||
| ba2aa4bee6 | |||
| f709a657fa | |||
| 514780a572 | |||
| 0c51a625f3 | |||
| 6f8b2d1dc4 | |||
| 0087ce2f19 | |||
| b98c2796c8 | |||
| 37d56b3205 | |||
| 5a54c0fb60 | |||
| 99b259836b | |||
| a54daaaa90 | |||
| fa66e6fca6 | |||
| b7864dded2 | |||
| 72d9698f38 | |||
| a7937cd657 | |||
| 998b14c5b0 | |||
| bff9c2a770 | |||
| b5acdffc87 | |||
| 127609160c | |||
| 8f22a5c570 | |||
| c792c18e01 | |||
| cd614e3845 | |||
| 40673f9cb8 | |||
| 911aa4c73d | |||
| 74b4cc2ceb | |||
| c0c463977d | |||
| 273bcafe01 | |||
| 9028059919 | |||
| c86af457e7 | |||
| b8e979be55 | |||
| 8849c19e8e | |||
| f1b4d02323 | |||
| 4aa8847037 | |||
| d2389400a9 | |||
| 721e70d027 | |||
| 3c1a7eac86 | |||
| f8e21dbd4c | |||
| d16a35d26f | |||
| f881caf794 | |||
| eb6fa276de | |||
| e987c8b37a | |||
| 26d76e27ac | |||
| 975549fca0 | |||
| 9a6bed52dd | |||
| efd9ad12a9 | |||
| 93a71a0fd0 | |||
| d373d7ac28 | |||
| e0e991b599 | |||
| 6d5b16ea8b | |||
| bfac67d3f0 | |||
| 0fd316d775 | |||
| c42fcf9ac4 | |||
| 04ade02a06 | |||
| 8ee53d3367 | |||
| 42111945b8 | |||
| 6fde157047 | |||
| 65ec5c9842 | |||
| a6444b5b33 | |||
| c7238cb04c | |||
| 366e5fafad | |||
| fb5710c134 | |||
| c4d9652335 | |||
| df6027cf9f | |||
| e3f7abf7f1 | |||
| c539917d7e | |||
| 5eb870c978 | |||
| b075c46ba3 | |||
| 4b6c7776f5 | |||
| 745a14195d | |||
| fb0f660eaf | |||
| 04a3030b79 | |||
| 5a3114de48 | |||
| b808fc5954 | |||
| 0ec4e37c13 | |||
| 9d301631ca | |||
| ec7145bb01 | |||
| f79d84b5fa | |||
| d00f1e0def | |||
| 637a813732 | |||
| 4f33c9280b | |||
| d54c752637 | |||
| 984d5d198d | |||
| 2568778a87 | |||
| 0d805a64f6 | |||
| 066d1c5b1e | |||
| cdc1950c3a | |||
| 7142874513 | |||
| 409c2f62be | |||
| 82922c5053 | |||
| 9c5bcb13e2 | |||
| 1d87dcdb7f | |||
| c7ba44a91e | |||
| 19b79c5b35 | |||
| 45216763e3 | |||
| 123293dc0e | |||
| 78e61060d2 | |||
| dd5371d2fd | |||
| da041d1f38 | |||
| d01599a2b4 | |||
| e80a2268ac | |||
| 3b86229c8f | |||
| c4d498d183 | |||
| 9bc46914e2 | |||
| 75f982e677 | |||
| f58bfeb1d0 | |||
| ebb077f6e0 | |||
| fdf6908fc6 | |||
| 7936660868 | |||
| 554dd3798f | |||
| 7f40d8e12c | |||
| 31d6f44ff5 | |||
| 86efcde927 | |||
| 4acbc33762 | |||
| 7cd3ebfcba | |||
| 55c97cab7e | |||
| 5bde091704 | |||
| 70489b46a5 | |||
| d7e89cc01f | |||
| a1f6030e75 | |||
| e7b2d8435e | |||
| f890a9633b | |||
| 5bdcd3124b | |||
| 752b3f8e4c | |||
| 001e836ae3 | |||
| 50d7f9a9dd | |||
| 34a5e58872 | |||
| 34fbea6a44 | |||
| 8737535b90 | |||
| 28ef414c3d | |||
| 16279949c1 | |||
| 444e22dd0e | |||
| a231506713 | |||
| 5dcabba73c | |||
| cd71c494da | |||
| f66642b8ec | |||
| 2283e1b0f2 | |||
| e0eb725d8b | |||
| 05e9549410 | |||
| 35fbce3506 | |||
| c1d6825793 | |||
| ae6c6970b4 | |||
| f8fef57cf3 | |||
| 4dc191735d | |||
| 4f11f437e1 | |||
| 5773e31990 | |||
| 9dda5d4c88 | |||
| 7f5b6959e1 | |||
| 126981d085 | |||
| 854a578929 | |||
| 6dd0daa597 | |||
| fd24a5e9ac | |||
| 948b35f244 | |||
| 96cc182a8c | |||
| e2291ed5b1 | |||
| 60ddb17547 | |||
| 9a991a31ba | |||
| 6de10c6c3b | |||
| c7d49c1504 | |||
| c721bd4d97 | |||
| 5b0a1968d8 | |||
| ad9064fa46 | |||
| f1c92e5702 | |||
| 92c1441922 | |||
| 3820950c7c | |||
| 08e558bd41 | |||
| 00af4a3d60 | |||
| ada427b134 | |||
| f696b9a573 | |||
| 195beeacac | |||
| 5b23ed35b6 | |||
| 42c8702704 | |||
| ad81376da8 | |||
| 562fea3a36 | |||
| b69d9ed69f | |||
| ad6a21a68f | |||
| 173aa0fcd1 | |||
| 788e08a75a | |||
| ec397a2daf | |||
| 2a5758fbc4 | |||
| 562ccd4b9e | |||
| 4d577774f3 | |||
| 4c10c16491 | |||
| 7695f31a5a | |||
| 06b27f8d39 | |||
| e1691661d9 | |||
| bc8e419d51 | |||
| ccad1ca0d9 | |||
| 75c315fca8 | |||
| bdef593cdd | |||
| d8943fc0c5 | |||
| cdd274e5ca | |||
| 2230741779 | |||
| 0e6950a60f | |||
| 1d4541e353 | |||
| 0a1e0212e2 | |||
| 25b44176b9 | |||
| e1deff341d | |||
| 3fc4c05e50 | |||
| 1843a74dbb | |||
| 9aa6a8e6b9 | |||
| 1de89069ce | |||
| 1e6525d085 | |||
| d9d71b7d38 | |||
| e85dec2b49 | |||
| c0f77b3368 | |||
| 8c9855d167 | |||
| 7c12bc8595 | |||
| 5102f6b0ff | |||
| dd40b9bae0 | |||
| 98b4026990 | |||
| b0640ba55b | |||
| c9a1557674 | |||
| 855f98722c | |||
| 21a82b70c7 | |||
| 519e222883 | |||
| 488a242787 | |||
| 523a03d212 | |||
| d49f3442e8 | |||
| 48dc570809 | |||
| 509320813f | |||
| 54d7e73acb | |||
| 1bc0eb3a4b | |||
| c0bf2d40dd | |||
| e30a39f9cb | |||
| 9727e4e9b8 | |||
| 3380912cdb | |||
| 91a5454b46 | |||
| e0c5acf4c8 | |||
| 7e062be0d0 | |||
| b23876c2b9 | |||
| e9a152683e | |||
| 34f1af727f | |||
| dfc24f3803 | |||
| ec233f7b37 | |||
| bb34b1ba59 | |||
| b1fb477359 | |||
| 3c877a19fc | |||
| b47baa6b74 | |||
| 14c85cbadf | |||
| a88b9139f8 | |||
| 251a4b52c0 | |||
| 1aaeaf5ebe | |||
| 9fd152d727 | |||
| 2571ceede4 | |||
| 932cfa3b9f | |||
| 01fde8573c | |||
| d95656b37b | |||
| 210eda0340 | |||
| 0d8b877d73 | |||
| 79d39b2295 | |||
| e03b0af58a | |||
| 71c2143cba | |||
| 70a7f5be19 | |||
| d2242423f5 | |||
| 782e9c8b20 | |||
| 55b2c678a4 | |||
| 48d754c184 | |||
| ff0443ace7 | |||
| cc3cc58de0 | |||
| 80d60266c2 | |||
| f06cdb5899 | |||
| dfe68988aa | |||
| d0264de3eb | |||
| 77c4303938 | |||
| 5724d2eeec | |||
| 64007b1188 | |||
| 50ee7bd8be | |||
| 2bcbde4bf1 | |||
| 99f75dd3b6 | |||
| 2b9132b4a8 | |||
| 82782be16e | |||
| 7067129887 | |||
| 6e706ac8f7 | |||
| 39b902c927 | |||
| 94a9cd7c9d | |||
| 0dae7db140 | |||
| 32954e025c | |||
| 61dda9a517 | |||
| 99a4cb50ac | |||
| f4b00d6dec | |||
| f69422a61c | |||
| 7f4c157a90 | |||
| 7d9724ff8b | |||
| 06db7f36f8 | |||
| f0c249bce2 | |||
| e8a96fba95 | |||
| 92b01c40a0 | |||
| 6003eeadba | |||
| 856ed4d3b3 | |||
| 267fe58a8c | |||
| 1b00d8c41c | |||
| cb126613b1 | |||
| 515bfee2a1 | |||
| 584f3511f8 | |||
| 3ea54e6725 | |||
| 48212d892f | |||
| 977c61b914 | |||
| 66fceb2313 | |||
| 76feefff00 | |||
| 1be43ff704 | |||
| b6d4a547e5 | |||
| 5ee860f1e8 | |||
| 0b01132d88 | |||
| b98c487919 | |||
| b43b08d423 | |||
| 3ddb6e6422 | |||
| ea9146be5b | |||
| 78cb1f45e4 | |||
| e472d1cf1e | |||
| 75dc04d598 | |||
| fc4c033cf3 | |||
| dbee054838 | |||
| 45b805dcd4 | |||
| a8ac54b51c | |||
| 86f705eae9 | |||
| d390209dab | |||
| 149f94be4c | |||
| ef1a1eab98 | |||
| dc7c6e14bc | |||
| 8c31dc85f9 | |||
| 7b8d1ddc35 | |||
| 8c12399732 | |||
| 93a4b1a448 | |||
| 04b0752b22 | |||
| 5137c08204 | |||
| ec0b44fb9f | |||
| 0bf83dfe67 | |||
| b151b70cb9 | |||
| f2640359a0 | |||
| 079aeaedb0 | |||
| bfacadb964 | |||
| 2e98ed44ee | |||
| 952d2a3553 | |||
| 225aa0ca59 | |||
| 37f718b84b | |||
| db8f53a6e1 | |||
| ce345ad67f | |||
| 8e53e625a4 | |||
| 3350bdbdd3 | |||
| 634d789654 | |||
| 0dfa8e96f5 | |||
| 3698e6ca28 | |||
| 595608bd62 | |||
| b8bb1ee71d | |||
| 4298213ce4 | |||
| 5671460618 | |||
| 452dc9f3e6 | |||
| 09e65211b4 | |||
| f46be89036 | |||
| f2f744a2fe | |||
| ceb1963b88 | |||
| 23d74b567a | |||
| 5014932a8f | |||
| 6a5694d2b2 | |||
| df074082a8 | |||
| 847d514441 | |||
| b2a71da2b0 | |||
| 3e5e4d2946 | |||
| e3c05e7648 | |||
| df9ca2d2fa | |||
| 1631ece2bd | |||
| 2ae25f3bbe | |||
| 511093a6ef | |||
| 1ef126f820 | |||
| 1788b86ced | |||
| 3111e93054 | |||
| 288abc678c | |||
| 7e559c5b90 | |||
| eaaeb844f3 | |||
| 097fde203d | |||
| efd85e67d9 | |||
| bd4bf459e3 | |||
| 8fb1745dbb | |||
| 47c6d6d058 | |||
| 564693d456 | |||
| aa3cd2a0c5 | |||
| c581f8ab47 | |||
| 63ed763499 | |||
| 213fc0d21e | |||
| 9bc1494582 | |||
| 33b2ba1609 | |||
| b9add62ba7 | |||
| 23fa2dc8b2 | |||
| 67ac09a290 | |||
| f10c14ae96 | |||
| 438d3ae0c2 | |||
| 842062fe44 | |||
| d01897d62a | |||
| 6eeb8e7580 | |||
| 9cebadba57 | |||
| e5ec9abca3 | |||
| 5e7f09d5c8 | |||
| ffa193d116 | |||
| 686f8988bc | |||
| 8d5c98004b | |||
| 214411919f | |||
| 42c2825e90 | |||
| 8974a87168 | |||
| f3e9983742 | |||
| beaf034035 | |||
| a5f18ab024 | |||
| d1f5934947 | |||
| f03d6f6861 | |||
| 20523b7eaf | |||
| f31c9b2a98 | |||
| 93071e727f | |||
| 2d12210606 | |||
| 88ccc528d4 | |||
| 9d3fc07865 | |||
| 6d465a40fb | |||
| 01c7e4c2e2 | |||
| 4005858242 | |||
| c24f951375 | |||
| dbe1efd1e9 | |||
| 0299cce343 | |||
| ca9f504329 | |||
| df3527ceca | |||
| 73d0838291 | |||
| 8e3574509e | |||
| 9bdf375304 | |||
| effa922597 | |||
| fe484257a0 | |||
| cc0cd9a114 | |||
| 916d63806e | |||
| 59d0118067 | |||
| 957c6b9f32 | |||
| 9c5a1147fa | |||
| 60ad6706ff | |||
| d9199d1182 | |||
| 219edb9faf | |||
| 30a0227ef3 | |||
| 29b5a7cb88 | |||
| abb727870e | |||
| af84a8b110 | |||
| 0dea5d3094 | |||
| 527b95521e | |||
| 396b23ddb4 | |||
| ee0d1549bd | |||
| 6da6b85530 | |||
| cde055dcb7 | |||
| 08957ce3d7 | |||
| 33515f1f09 | |||
| e3c411f043 | |||
| eda71725da | |||
| f19f45c00e | |||
| 6fb7897092 | |||
| 2de941d7fb | |||
| 3e4b7ebb13 | |||
| 2b52f82b57 | |||
| 9fb7e80757 | |||
| 38869ce438 | |||
| 1dbf93c1cf | |||
| aafcab8232 | |||
| bcfe1b8830 | |||
| 5db1847f7e | |||
| 165ca4a6e9 | |||
| 9556e1305f | |||
| f5b686abd9 | |||
| 74e1b733a9 | |||
| a9e0090275 | |||
| 68d57da9ca | |||
| 3cef529ebd | |||
| 85ac399e2c | |||
| e8d09773a2 | |||
| 23905407ca | |||
| 2ab1304b6d | |||
| d94236e127 | |||
| 630025c123 | |||
| 2b73c5a85a | |||
| a03f2c585f | |||
| 8a0216dcf8 | |||
| 59013be586 | |||
| c6a638dbfd | |||
| e54440ad80 | |||
| 446b014f7d | |||
| 9190433ea1 | |||
| d78d135600 | |||
| 04b6c89e58 | |||
| 8eefba2bee | |||
| a254139200 | |||
| 0514bed5b7 | |||
| 49978c5e3f | |||
| b959bf070c | |||
| 49e6877810 | |||
| bb759597e9 | |||
| bf0bb80009 | |||
| 5d61a9808f | |||
| bc36da6b25 | |||
| 280cb75ca4 | |||
| d5d0a9abd3 | |||
| 9f343992b2 | |||
| 7ab7058dba | |||
| c3113ade13 | |||
| 7737c05c8f | |||
| 4fde9955b2 | |||
| 3875c8f239 | |||
| 7bd1b03652 | |||
| 73878c14e3 | |||
| bc2c3a2c9a | |||
| 460d37c656 | |||
| fff9e3221e | |||
| f77fa17cbc | |||
| d4565f7d60 | |||
| e8b1976f4f | |||
| 6b4bb64885 | |||
| fc0c42f7db | |||
| 41e5f27d4d | |||
| a042b4c739 | |||
| a1c1a9f983 | |||
| 423ae77160 | |||
| 05c61a4b1a | |||
| ce8f06b7fb | |||
| f815844577 | |||
| 7fe4d38c51 | |||
| 43edcb4e9e | |||
| e259b29afa | |||
| cd5f57a004 | |||
| 8c0cdb0e22 | |||
| c0df9f1cb6 | |||
| cb1ddaeedb | |||
| 4447dd7b9a | |||
| dcb7602837 | |||
| 60e48742fe | |||
| dd43569778 | |||
| 8433dd6950 | |||
| 379802f580 | |||
| 2dcb5d7c1e | |||
| 98870d9f3e | |||
| 31aeb2289c | |||
| 0f66ecbba1 | |||
| f0d3c9dd4e | |||
| 92aae340d9 |
@@ -0,0 +1,31 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Rust build artifacts
|
||||
target
|
||||
**/target
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Build outputs
|
||||
.next
|
||||
**/.next
|
||||
out
|
||||
**/out
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
@@ -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']
|
||||
@@ -8,6 +8,6 @@ updates:
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*" # Group all Actions updates into a single larger pull request
|
||||
- '*' # Group all Actions updates into a single larger pull request
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
name: PR checks
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
permissions:
|
||||
@@ -10,20 +12,30 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: '-C target-cpu=skylake'
|
||||
SCCACHE_GHA_ENABLED: 'true'
|
||||
RUSTC_WRAPPER: sccache
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'true'
|
||||
- name: setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- name: Install minimal stable with clippy and rustfmt
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Cache apt packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: apt-rust-lint-${{ runner.os }}
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev
|
||||
- name: Format
|
||||
- name: Format check
|
||||
working-directory: apps/readest-app/src-tauri
|
||||
run: cargo fmt --check
|
||||
- name: Clippy Check
|
||||
@@ -33,27 +45,111 @@ jobs:
|
||||
build_web_app:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.15.1
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: cache Next.js build
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/readest-app/.next/cache
|
||||
key: nextjs-web-${{ github.sha }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
nextjs-web-${{ github.sha }}-
|
||||
nextjs-web-
|
||||
|
||||
- name: install Dependencies
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm install && pnpm setup-pdfjs
|
||||
pnpm install && pnpm setup-vendors
|
||||
|
||||
- name: build the web App
|
||||
- name: run format check
|
||||
run: |
|
||||
pnpm format:check || (pnpm format && git diff && exit 1)
|
||||
|
||||
- name: install playwright browsers
|
||||
working-directory: apps/readest-app
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: run web tests
|
||||
working-directory: apps/readest-app
|
||||
run: pnpm test:pr:web
|
||||
|
||||
- name: run lint
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build-web
|
||||
pnpm lint
|
||||
|
||||
- name: build the web app
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build-web && pnpm check:all
|
||||
|
||||
build_tauri_app:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: 'true'
|
||||
RUSTC_WRAPPER: sccache
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: cache Next.js build
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/readest-app/.next/cache
|
||||
key: nextjs-tauri-${{ github.sha }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
nextjs-tauri-${{ github.sha }}-
|
||||
nextjs-tauri-
|
||||
|
||||
- name: install Dependencies
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm install && pnpm setup-vendors
|
||||
|
||||
- name: setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: tauri-cargo
|
||||
cache-all-crates: 'true'
|
||||
|
||||
- name: Cache apt packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: apt-tauri-${{ runner.os }}
|
||||
- name: install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev xvfb
|
||||
|
||||
- name: run tauri tests
|
||||
working-directory: apps/readest-app
|
||||
run: xvfb-run pnpm test:pr:tauri
|
||||
|
||||
@@ -17,14 +17,14 @@ jobs:
|
||||
release_version: ${{ steps.get-release-notes.outputs.release_version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
- name: get version
|
||||
run: echo "PACKAGE_VERSION=$(node -p "require('./apps/readest-app/package.json').version")" >> $GITHUB_ENV
|
||||
- name: get release
|
||||
id: get-release
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.getLatestRelease({
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
core.setOutput('release_tag', data.tag_name);
|
||||
- name: get release notes
|
||||
id: get-release-notes
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
@@ -48,6 +48,63 @@ jobs:
|
||||
core.setOutput('release_version', version);
|
||||
core.setOutput('release_note', releaseNote);
|
||||
|
||||
update-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: get-release
|
||||
|
||||
steps:
|
||||
- name: update release
|
||||
id: update-release
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
release_id: ${{ needs.get-release.outputs.release_id }}
|
||||
release_tag: ${{ needs.get-release.outputs.release_tag }}
|
||||
release_note: ${{ needs.get-release.outputs.release_note }}
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.generateReleaseNotes({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: process.env.release_tag,
|
||||
})
|
||||
const notes = process.env.release_note.split(/\d+\.\s/).filter(Boolean);
|
||||
const formattedNotes = notes.map(note => `* ${note.trim()}`).join("\n");
|
||||
const body = `## Release Highlight\n${formattedNotes}\n\n${data.body}`;
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.release_id,
|
||||
body: body,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
})
|
||||
|
||||
build-koreader-plugin:
|
||||
needs: get-release
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: create KOReader plugin zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
plugin_zip="Readest-${version}-1.koplugin.zip"
|
||||
meta_file="apps/readest.koplugin/_meta.lua"
|
||||
perl -i -pe "s/^}/ version = \"${version}\",\n}/" "${meta_file}"
|
||||
|
||||
cd apps
|
||||
zip -r ../${plugin_zip} readest.koplugin
|
||||
cd ..
|
||||
|
||||
echo "Uploading ${plugin_zip} to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} ${plugin_zip} --clobber
|
||||
|
||||
build-tauri:
|
||||
needs: get-release
|
||||
permissions:
|
||||
@@ -91,25 +148,23 @@ jobs:
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: initialize git submodules
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.14.4
|
||||
uses: pnpm/action-setup@v5
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: setup Java (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
@@ -120,13 +175,13 @@ jobs:
|
||||
|
||||
- name: install NDK (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
run: sdkmanager "ndk;27.0.11902837"
|
||||
run: sdkmanager "ndk;28.2.13676358"
|
||||
|
||||
- name: install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: copy pdfjs-dist to public directory
|
||||
run: pnpm --filter @readest/readest-app setup-pdfjs
|
||||
- name: copy pdfjs-dist and simplecc-dist to public directory
|
||||
run: pnpm --filter @readest/readest-app setup-vendors
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -167,7 +222,7 @@ jobs:
|
||||
if: matrix.config.release == 'android'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.0.11902837
|
||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: |
|
||||
@@ -229,10 +284,15 @@ jobs:
|
||||
echo "Uploading updated latest.json to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber
|
||||
|
||||
- name: Override tauri-cli with custom AppImage format (Linux)
|
||||
if: matrix.config.release == 'linux'
|
||||
run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
if: matrix.config.release != 'android'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: 'true'
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -255,12 +315,22 @@ jobs:
|
||||
echo "Uploading release notes to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} apps/readest-app/release-notes.json --clobber
|
||||
|
||||
- name: upload portable binaries (Windows only)
|
||||
- name: build and upload portable binaries (Windows only)
|
||||
if: matrix.config.os == 'windows-latest'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Building Portable Binaries"
|
||||
pushd apps/readest-app/
|
||||
echo "NEXT_PUBLIC_PORTABLE_APP=true" >> .env.local
|
||||
pnpm tauri build ${{ matrix.config.args }}
|
||||
|
||||
popd
|
||||
echo "Uploading Portable Binaries"
|
||||
arch=${{ matrix.config.arch }}
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
@@ -283,35 +353,49 @@ jobs:
|
||||
echo "Uploading $bin_file to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file --clobber
|
||||
|
||||
update-release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-release, build-tauri]
|
||||
echo "Signing portable binary"
|
||||
pushd apps/readest-app/
|
||||
pnpm tauri signer sign "../../$bin_file"
|
||||
popd
|
||||
echo "Uploading signature to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file.sig --clobber
|
||||
|
||||
steps:
|
||||
- name: update release
|
||||
id: update-release
|
||||
uses: actions/github-script@v7
|
||||
- name: download and update latest.json for Windows portable release
|
||||
if: matrix.config.os == 'windows-latest'
|
||||
env:
|
||||
release_id: ${{ needs.get-release.outputs.release_id }}
|
||||
release_tag: ${{ needs.get-release.outputs.release_tag }}
|
||||
release_note: ${{ needs.get-release.outputs.release_note }}
|
||||
with:
|
||||
script: |
|
||||
const { data } = await github.rest.repos.generateReleaseNotes({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: process.env.release_tag,
|
||||
})
|
||||
const notes = process.env.release_note.split(/(?:\d\.\s)/).filter(Boolean);
|
||||
const formattedNotes = notes.map(note => `* ${note.trim()}`).join("\n");
|
||||
const body = `## Release Highlight\n${formattedNotes}\n\n${data.body}`;
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.release_id,
|
||||
body: body,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
})
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -sL https://github.com/readest/readest/releases/latest/download/latest.json -o latest.json
|
||||
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
arch=${{ matrix.config.arch }}
|
||||
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
bin_file="Readest_${version}_x64-portable.exe"
|
||||
platform_key="windows-x86_64-portable"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
bin_file="Readest_${version}_arm64-portable.exe"
|
||||
platform_key="windows-aarch64-portable"
|
||||
else
|
||||
echo "Unknown architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
portable_url="https://github.com/readest/readest/releases/download/${{ needs.get-release.outputs.release_tag }}/$bin_file"
|
||||
portable_sig=$(cat $bin_file.sig)
|
||||
|
||||
jq --arg url "$portable_url" \
|
||||
--arg sig "$portable_sig" \
|
||||
--arg key "$platform_key" \
|
||||
'.platforms[$key] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json
|
||||
|
||||
echo "Uploading updated latest.json to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber
|
||||
|
||||
upload-to-r2:
|
||||
needs: [get-release, build-tauri]
|
||||
uses: ./.github/workflows/upload-to-r2.yml
|
||||
with:
|
||||
tag: ${{ needs.get-release.outputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'true'
|
||||
- uses: amondnet/vercel-action@v25
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
docker/.env
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
@@ -38,3 +39,16 @@ next-env.d.ts
|
||||
|
||||
# Rust build
|
||||
target
|
||||
|
||||
fastlane/report.xml
|
||||
|
||||
*.koplugin.zip
|
||||
|
||||
# nix
|
||||
result*
|
||||
|
||||
.playwright-mcp/
|
||||
|
||||
.claude/worktrees
|
||||
.claude/settings.local.json
|
||||
|
||||
|
||||
@@ -4,3 +4,15 @@
|
||||
[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
|
||||
[submodule "packages/simplecc-wasm"]
|
||||
path = packages/simplecc-wasm
|
||||
url = https://github.com/readest/simplecc-wasm.git
|
||||
[submodule "apps/readest-app/src-tauri/plugins/tauri-plugin-turso"]
|
||||
path = apps/readest-app/src-tauri/plugins/tauri-plugin-turso
|
||||
url = https://github.com/readest/tauri-plugin-turso.git
|
||||
[submodule "apps/readest-app/.claude/skills/gstack"]
|
||||
path = apps/readest-app/.claude/skills/gstack
|
||||
url = https://github.com/garrytan/gstack.git
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pnpm exec lint-staged
|
||||
@@ -0,0 +1,2 @@
|
||||
pnpm -C apps/readest-app lint
|
||||
pnpm -C apps/readest-app test
|
||||
@@ -1 +1,38 @@
|
||||
packages/foliate-js/
|
||||
# Dependencies
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Build Artifacts (Web & Rust)
|
||||
.next
|
||||
.open-next
|
||||
.build
|
||||
.tauri
|
||||
out
|
||||
build
|
||||
dist
|
||||
target
|
||||
fastlane
|
||||
.wrangler
|
||||
|
||||
# Autogenerated Tauri files
|
||||
gen
|
||||
**/autogenerated
|
||||
**/schemas
|
||||
|
||||
# Submodules (External Repos)
|
||||
packages
|
||||
|
||||
# Claude Code Skills & Config
|
||||
apps/readest-app/.claude
|
||||
|
||||
# Vendored Assets (Generated/External Code)
|
||||
apps/readest-app/public/*.js
|
||||
apps/readest-app/public/vendor
|
||||
apps/readest-app/src-tauri/plugins/tauri-plugin-turso
|
||||
|
||||
# Environment & Editor
|
||||
.env
|
||||
.env.*
|
||||
.vscode
|
||||
.idea
|
||||
*.log
|
||||
@@ -5,5 +5,6 @@
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"endOfLine": "lf",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
@@ -13,4 +13,19 @@
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.format.enable": false,
|
||||
"typescript.format.enable": false,
|
||||
}
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"prettier.documentSelectors": ["**/*.{js,jsx,ts,tsx,css,json,md,html,yml}"]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -43,8 +43,8 @@ git submodule update --init --recursive
|
||||
```bash
|
||||
# might need to rerun this when code is updated
|
||||
pnpm install
|
||||
# copy pdfjs-dist to Next.js public directory
|
||||
pnpm --filter @readest/readest-app setup-pdfjs
|
||||
# copy vendors dist libs to public directory
|
||||
pnpm --filter @readest/readest-app setup-vendors
|
||||
```
|
||||
|
||||
#### 3. Verify Dependencies Installation
|
||||
|
||||
@@ -2,9 +2,26 @@
|
||||
members = [
|
||||
"apps/readest-app/src-tauri",
|
||||
"packages/tauri/crates/tauri",
|
||||
"packages/tauri-plugins/plugins/fs"
|
||||
]
|
||||
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"
|
||||
zbus = "5.9"
|
||||
dunce = "1"
|
||||
url = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Bilingify LLC"]
|
||||
homepage = "https://readest.com"
|
||||
@@ -15,4 +32,5 @@ edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[patch.crates-io]
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri-plugin-fs = { path = "packages/tauri-plugins/plugins/fs" }
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
FROM node:22-slim
|
||||
|
||||
ENV PNPM_HOME="/root/.local/share/pnpm"
|
||||
ENV PATH="${PATH}:${PNPM_HOME}"
|
||||
|
||||
RUN npm install --global pnpm
|
||||
|
||||
# Install necessary packages
|
||||
RUN apt update -y && apt install -y --no-install-recommends \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
ca-certificates \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Rust and Cargo
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
COPY . /app
|
||||
|
||||
FROM docker.io/node:22-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@10.29.3 --activate
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY apps/readest-app/package.json ./apps/readest-app/
|
||||
COPY patches/ ./patches/
|
||||
COPY packages/ ./packages/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
RUN pnpm --filter @readest/readest-app setup-pdfjs
|
||||
FROM base AS dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm --filter @readest/readest-app setup-vendors
|
||||
|
||||
FROM dependencies AS development-stage
|
||||
COPY . .
|
||||
WORKDIR /app/apps/readest-app
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["pnpm", "dev-web", "-H", "0.0.0.0"]
|
||||
|
||||
FROM base AS build
|
||||
ARG NEXT_PUBLIC_SUPABASE_URL
|
||||
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
ARG NEXT_PUBLIC_APP_PLATFORM
|
||||
ARG NEXT_PUBLIC_API_BASE_URL
|
||||
ARG NEXT_PUBLIC_OBJECT_STORAGE_TYPE
|
||||
ARG NEXT_PUBLIC_STORAGE_FIXED_QUOTA
|
||||
ARG NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA
|
||||
COPY --from=dependencies /app/node_modules /app/node_modules
|
||||
COPY --from=dependencies /app/apps/readest-app/node_modules /app/apps/readest-app/node_modules
|
||||
COPY --from=dependencies /app/apps/readest-app/public/vendor /app/apps/readest-app/public/vendor
|
||||
COPY --from=dependencies /app/packages/foliate-js/node_modules /app/packages/foliate-js/node_modules
|
||||
COPY . .
|
||||
WORKDIR /app/apps/readest-app
|
||||
RUN pnpm build-web
|
||||
|
||||
ENTRYPOINT ["pnpm", "start-web"]
|
||||
FROM build as production-stage
|
||||
ENTRYPOINT ["pnpm", "start-web", "-H", "0.0.0.0"]
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -5,18 +5,22 @@
|
||||
<h1>Readest</h1>
|
||||
<br>
|
||||
|
||||
[Readest][link-website] is an open-source ebook reader designed for immersive and deep reading experiences. Built as a modern rewrite of [Foliate](https://github.com/johnfactotum/foliate), it leverages [Next.js 15](https://github.com/vercel/next.js) and [Tauri v2](https://github.com/tauri-apps/tauri) to deliver a smooth, cross-platform experience across macOS, Windows, Linux, Android, iOS, and the Web.
|
||||
[Readest][link-website] is an open-source ebook reader designed for immersive and deep reading experiences. Built as a modern rewrite of [Foliate](https://github.com/johnfactotum/foliate), it leverages [Next.js 16](https://github.com/vercel/next.js) and [Tauri v2](https://github.com/tauri-apps/tauri) to deliver a smooth, cross-platform experience across macOS, Windows, Linux, Android, iOS, and the Web.
|
||||
|
||||
[![Website][badge-website]][link-website]
|
||||
[![Web App][badge-web-app]][link-web-readest]
|
||||
[![OS][badge-platforms]][link-website]
|
||||
<br>
|
||||
[![][badge-hellogithub]][link-hellogithub]
|
||||
[![][badge-discord]][link-discord]
|
||||
[![Discord][badge-discord]][link-discord]
|
||||
[![Reddit][badge-reddit]][link-reddit]
|
||||
[![AGPL Licence][badge-license]](LICENSE)
|
||||
[![Language Coverage][badge-language-coverage]][link-locales]
|
||||
[![Donate][badge-donate]][link-donate]
|
||||
[![Latest release][badge-release]][link-gh-releases]
|
||||
[![Last commit][badge-last-commit]][link-gh-commits]
|
||||
[![Commits][badge-commit-activity]][link-gh-pulse]
|
||||
[![][badge-hellogithub]][link-hellogithub]
|
||||
[![Ask DeepWiki][badge-deepwiki]][link-deepwiki]
|
||||
|
||||
</div>
|
||||
|
||||
@@ -27,6 +31,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>
|
||||
|
||||
@@ -40,38 +45,38 @@
|
||||
|
||||
<div align="left">✅ Implemented</div>
|
||||
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| --------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF (experimental) | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
|
||||
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| **Translate with DeepL** | From a single sentence to the entire book—translate instantly with DeepL. | ✅ |
|
||||
| **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. | ✅ |
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience and use instant mode for quicker interactions. | ✅ |
|
||||
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
|
||||
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
|
||||
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
|
||||
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
|
||||
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
|
||||
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
|
||||
| **OPDS/Calibre Integration** | Integrate OPDS/Calibre to access online libraries and catalogs. | ✅ |
|
||||
| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ |
|
||||
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
|
||||
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
|
||||
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | ✅ |
|
||||
| **Accessibility** | Provides full keyboard navigation and supports for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ |
|
||||
| **Visual & Focus Aids** | Reading ruler, paragraph-by-paragraph reading mode, and speed reading features. | ✅ |
|
||||
|
||||
## Planned Features
|
||||
|
||||
<div align="left">🛠 Building</div>
|
||||
<div align="left">🔄 Planned</div>
|
||||
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| **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. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🔄 |
|
||||
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
|
||||
| **Feature** | **Description** | **Priority** |
|
||||
| ------------------------------- | -------------------------------------------------------------------------- | ------------ |
|
||||
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
|
||||
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🛠 |
|
||||
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
|
||||
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
|
||||
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
|
||||
|
||||
Stay tuned for continuous improvements and updates! Contributions and suggestions are always welcome—let's build the ultimate reading experience together. 😊
|
||||
|
||||
@@ -87,17 +92,27 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
The Readest app is available for download! 🥳 🚀
|
||||
### Mobile Apps
|
||||
|
||||
- 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].
|
||||
- Web: Visit [https://web.readest.com][link-web-readest].
|
||||
<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>
|
||||
<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 and install **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 and download **Readest** at [https://readest.com][link-website] or the [Releases on GitHub][link-gh-releases].
|
||||
- Linux users can also install [Readest on Flathub][link-flathub].
|
||||
- Web: Visit and use **Readest for Web** at [https://web.readest.com][link-web-readest].
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -107,8 +122,8 @@ The Readest app is available for download! 🥳 🚀
|
||||
For the best experience to build Readest for yourself, use a recent version of Node.js and Rust. Refer to the [Tauri documentation](https://v2.tauri.app/start/prerequisites/) for details on setting up the development environment prerequisites on different platforms.
|
||||
|
||||
```bash
|
||||
nvm install v22
|
||||
nvm use v22
|
||||
nvm install v24
|
||||
nvm use v24
|
||||
npm install -g pnpm
|
||||
rustup update
|
||||
```
|
||||
@@ -122,16 +137,16 @@ To get started with Readest, follow these steps to clone and build the project.
|
||||
```bash
|
||||
git clone https://github.com/readest/readest.git
|
||||
cd readest
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
# might need to rerun this when code is updated
|
||||
git submodule update --init --recursive
|
||||
pnpm install
|
||||
# copy pdfjs-dist to Next.js public directory
|
||||
pnpm --filter @readest/readest-app setup-pdfjs
|
||||
# copy vendors dist libs to public directory
|
||||
pnpm --filter @readest/readest-app setup-vendors
|
||||
```
|
||||
|
||||
### 3. Verify Dependencies Installation
|
||||
@@ -153,13 +168,17 @@ 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:
|
||||
|
||||
```bash
|
||||
# Initialize the Android environment (run once)
|
||||
rm apps/readest-app/src-tauri/gen/android
|
||||
pnpm tauri android init
|
||||
git checkout apps/readest-app/src-tauri/gen/android
|
||||
|
||||
pnpm tauri android dev
|
||||
# or if you want to dev on a real device
|
||||
@@ -185,6 +204,9 @@ pnpm tauri android build
|
||||
pnpm tauri ios build
|
||||
```
|
||||
|
||||
Please refer to our release script if you experience any issues:
|
||||
https://github.com/readest/readest/blob/main/.github/workflows/release.yml
|
||||
|
||||
### 6. Setup dev environment with Nix
|
||||
|
||||
If you have Nix installed, you can leverage flake to enter a development shell
|
||||
@@ -233,6 +255,32 @@ Please check the [wiki][link-gh-wiki] of this project for more information on de
|
||||
|
||||
- See Issue [readest/readest#358](https://github.com/readest/readest/issues/358) for further details, or head over to our [Discord][link-discord] server and open a support discussion with detailed logs of your environment and the steps you’ve taken.
|
||||
|
||||
### 2. AppImage Launches but Only Shows a Taskbar Icon
|
||||
|
||||
On some Arch Linux systems—especially those using Wayland—the Readest AppImage may briefly show an icon in the taskbar and then exit without opening a window.
|
||||
|
||||
You might see logs such as:
|
||||
|
||||
```
|
||||
Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...
|
||||
```
|
||||
|
||||
This behavior is usually caused by compatibility issues between the bundled AppImage libraries and the system’s EGL / Wayland environment.
|
||||
|
||||
**Workaround 1: Launch with LD_PRELOAD (recommended)**
|
||||
|
||||
You can preload the system Wayland client library before launching the AppImage:
|
||||
|
||||
```
|
||||
LD_PRELOAD=/usr/lib/libwayland-client.so /path/to/Readest.AppImage
|
||||
```
|
||||
|
||||
This workaround has been confirmed to resolve the issue on affected systems.
|
||||
|
||||
**Workaround 2: Use the Flatpak Version**
|
||||
|
||||
If you prefer a more reliable out-of-the-box experience on Arch Linux, consider using the [Flatpak build on Flathub][link-flathub] instead. The Flatpak runtime helps avoid system library mismatches and tends to behave more consistently across different Wayland and X11 setups.
|
||||
|
||||
## Contributors
|
||||
|
||||
Readest is open-source, and contributions are welcome! Feel free to open issues, suggest features, or submit pull requests. Please **review our [contributing guidelines](CONTRIBUTING.md) before you start**. We also welcome you to join our [Discord][link-discord] community for either support or contributing guidance.
|
||||
@@ -243,6 +291,18 @@ 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. You can [become a sponsor on GitHub](https://github.com/sponsors/readest), [donate via Stripe](https://donate.stripe.com/4gMcN5aZdcE52kW3TFgjC01), or [donate with crypto](https://donate.readest.com). Your contribution helps us squash bugs faster, improve performance, and keep building great features.
|
||||
|
||||
### Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a title="Browser testing via TestMu AI" href="https://www.testmuai.com/?utm_medium=sponsor&utm_source=readest" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/readest/readest/refs/heads/main/data/sponsors/testmu-ai-logo.png" style="vertical-align: middle;" width="250" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 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.
|
||||
@@ -262,7 +322,9 @@ The following libraries and frameworks are used in this software:
|
||||
|
||||
The following fonts are utilized in this software, either bundled within the application or provided through web fonts:
|
||||
|
||||
[Bitter](https://fonts.google.com/?query=Bitter), [Fira Code](https://fonts.google.com/?query=Fira+Code), [Literata](https://fonts.google.com/?query=Literata), [Merriweather](https://fonts.google.com/?query=Merriweather), [Noto Sans](https://fonts.google.com/?query=Noto+Sans), [Roboto](https://fonts.google.com/?query=Roboto), [LXGW WenKai](https://github.com/lxgw/LxgwWenKai), [MiSans](https://hyperos.mi.com/font/en/), [Source Han](https://github.com/adobe-fonts/source-han-sans/), [WenQuanYi Micro Hei](http://wenq.org/wqy2/)
|
||||
[Bitter](https://fonts.google.com/specimen/Bitter), [Fira Code](https://fonts.google.com/specimen/Fira+Code), [Inter](https://fonts.google.com/specimen/Inter), [Literata](https://fonts.google.com/specimen/Literata), [Merriweather](https://fonts.google.com/specimen/Merriweather), [Noto Sans](https://fonts.google.com/specimen/Noto+Sans), [Roboto](https://fonts.google.com/specimen/Roboto), [LXGW WenKai](https://github.com/lxgw/LxgwWenKai), [MiSans](https://hyperos.mi.com/font/en/), [Source Han](https://github.com/adobe-fonts/source-han-sans/), [WenQuanYi Micro Hei](http://wenq.org/wqy2/)
|
||||
|
||||
We would also like to thank the [Web Chinese Fonts Plan](https://chinese-font.netlify.app) for offering open-source tools that enable the use of Chinese fonts on the web.
|
||||
|
||||
---
|
||||
|
||||
@@ -273,12 +335,18 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[badge-license]: https://img.shields.io/github/license/readest/readest?color=teal
|
||||
[badge-release]: https://img.shields.io/github/release/readest/readest?color=green
|
||||
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%2C%20Windows%2C%20Linux%2C%20Android%2C%20iOS%2C%20Web%2C%20PWA-green
|
||||
[badge-last-commit]: https://img.shields.io/github/last-commit/readest/readest?color=green
|
||||
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
|
||||
[badge-last-commit]: https://img.shields.io/github/last-commit/readest/readest?color=blue
|
||||
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest?color=blue
|
||||
[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
|
||||
[badge-reddit]: https://img.shields.io/reddit/subreddit-subscribers/readest?style=flat&logo=reddit&color=F37E41
|
||||
[badge-language-coverage]: https://img.shields.io/badge/coverage-53%25%20population%20🌍-green
|
||||
[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-flathub]: https://flathub.org/en/apps/com.bilingify.readest
|
||||
[link-web-readest]: https://web.readest.com
|
||||
[link-gh-releases]: https://github.com/readest/readest/releases
|
||||
[link-gh-commits]: https://github.com/readest/readest/commits/main
|
||||
@@ -288,3 +356,7 @@ 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
|
||||
[link-locales]: https://github.com/readest/readest/tree/main/apps/readest-app/public/locales
|
||||
[link-kosync-wiki]: https://github.com/readest/readest/wiki/Sync-with-Koreader-devices
|
||||
[link-reddit]: https://reddit.com/r/readest/
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Readest Project Memory
|
||||
|
||||
## Key Reference Documents
|
||||
- [Bug Fixing Patterns](bug-patterns.md) - Common bug categories, root causes, and fix strategies
|
||||
- [CSS & Style Fixes](css-style-fixes.md) - EPUB CSS override patterns and the style.ts pipeline
|
||||
- [TTS Fixes](tts-fixes.md) - Text-to-Speech architecture and bug patterns
|
||||
- [Layout & UI Fixes](layout-ui-fixes.md) - Safe insets, z-index, platform-specific UI issues
|
||||
- [Platform Compat Fixes](platform-compat-fixes.md) - Android, iOS, Linux, macOS platform-specific bugs
|
||||
- [Annotator & Reader Fixes](annotator-reader-fixes.md) - Highlight, selection, accessibility bugs
|
||||
|
||||
## Critical Files (Most Bug-Prone)
|
||||
- `src/utils/style.ts` - Central EPUB CSS transformation hub (14+ bug fixes)
|
||||
- `packages/foliate-js/paginator.js` - Page layout, image sizing, backgrounds
|
||||
- `src/services/tts/TTSController.ts` - TTS state machine, section tracking
|
||||
- `src/hooks/useSafeAreaInsets.ts` - Safe area inset management
|
||||
- `src/app/reader/components/FoliateViewer.tsx` - Reader view orchestration
|
||||
- `src/app/reader/components/annotator/Annotator.tsx` - Annotation lifecycle
|
||||
|
||||
## Architecture Notes
|
||||
- foliate-js is a git submodule at `packages/foliate-js/`
|
||||
- Multiview paginator: loads adjacent sections in background, multiple View/Overlayer instances per book
|
||||
- Style overrides: `getLayoutStyles()` (always), `getColorStyles()` (when overriding color)
|
||||
- `transformStylesheet()` does regex-based EPUB CSS rewriting at load time
|
||||
- TTS uses independent section tracking (`#ttsSectionIndex`) decoupled from view
|
||||
- Safe area insets flow: Native plugin -> useSafeAreaInsets hook -> component styles
|
||||
- Dropdown menus use `DropdownContext` (not blur-based) for screen reader compat
|
||||
@@ -0,0 +1,90 @@
|
||||
# Annotator & Reader Fixes Reference
|
||||
|
||||
## Annotation System Architecture
|
||||
|
||||
### Key Components
|
||||
- `Annotator.tsx` - Annotation lifecycle, popup display, style/color management
|
||||
- `AnnotationRangeEditor.tsx` - Drag handles for adjusting selection range
|
||||
- `MagnifierLoupe.tsx` - Magnifying glass during handle drag (mobile only)
|
||||
- `useTextSelector.ts` - Text selection detection and processing
|
||||
- `useAnnotationEditor.ts` - Editing existing annotations
|
||||
- `useInstantAnnotation.ts` - Creating new annotations on selection
|
||||
|
||||
### Highlight Rendering
|
||||
- Highlights rendered by foliate-js `Overlayer` (SVG overlayer in paginator shadow DOM, not iframe)
|
||||
- Each view in multiview paginator has its own `Overlayer` instance with unique clipPath ID
|
||||
- `Overlayer.add()` stores range + draw function; `redraw()` recalculates positions from stored ranges
|
||||
- Colors stored as color names mapped to custom hex via `globalReadSettings.customHighlightColors`
|
||||
- Sidebar uses `color-mix()` CSS function with custom colors, not Tailwind utility classes (#3273)
|
||||
- Rounded highlight style supported via `vertical` option passed to overlayer (#3208)
|
||||
|
||||
### Multiview Overlayer Pitfalls
|
||||
- **Duplicate SVG IDs**: Each overlayer creates `<clipPath>` for loupe hole — IDs MUST be unique per instance or `url(#id)` resolves to wrong element, clipping everything
|
||||
- **docLoadHandler scope**: `FoliateViewer.tsx` re-adds annotations on `load` event — MUST filter by `detail.index` (loaded section), not re-add ALL annotations (overwrites drag edits)
|
||||
- **MagnifierLoupe lifecycle**: Don't destroy/recreate loupe on every drag tick — `hideLoupe()` should only run on unmount, `showLoupe()` fast path updates position only
|
||||
- **Stale closures in useTextSelector**: `getProgress()` must be called inside callbacks, not captured at hook top-level (useFoliateEvents deps are `[view]` only)
|
||||
|
||||
## Fix History
|
||||
|
||||
| Issue | Problem | Root Cause | Fix |
|
||||
|-------|---------|------------|-----|
|
||||
| #3286 | Selection stuck on first annotation | `initializedRef` guard blocked re-computation | Remove guard, consolidate style/color effects |
|
||||
| #3273 | Custom colors not in sidebar | Hardcoded Tailwind classes | Use inline `style` with `color-mix()` |
|
||||
| #3234 | Letter-by-letter selection on mobile | No word boundary snapping | Add `snapRangeToWords()` using `Intl.Segmenter` |
|
||||
| #3208 | Hard rectangular highlights | No border radius support | Pass `vertical` option, update foliate-js |
|
||||
| #3002 | Can't see text under finger | No magnification UI | New `MagnifierLoupe` component using `view.renderer.showLoupe()` |
|
||||
| #3082 | No page numbers on annotations | `pageNumber` field missing | Add `pageNumber` to BookNote type, compute on create |
|
||||
| #3225 | Android tools unresponsive | Premature `makeSelection()` call | Remove premature re-selection in Android path |
|
||||
|
||||
## Common Annotation Bugs
|
||||
|
||||
### Selection Issues
|
||||
- **Word snapping**: Uses `Intl.Segmenter` with `granularity: 'word'` to snap selection to word boundaries
|
||||
- **Android re-selection**: Don't call `makeSelection(sel, index, true)` immediately on pointer-up; let the popup flow complete
|
||||
- **Range editor handles**: Remove `initializedRef` guards that prevent re-computation when switching annotations
|
||||
|
||||
### Color/Style Issues
|
||||
- **Custom colors in sidebar**: Use inline `style={{ backgroundColor: 'color-mix(...)' }}` not Tailwind classes
|
||||
- **Style synchronization**: Consolidate `selectedStyle` and `selectedColor` into one `useEffect`
|
||||
- **Switching annotations**: Must call `setShowAnnotPopup(false)` and `setEditingAnnotation(null)` before setting up new annotation
|
||||
|
||||
## Reader/Content Fixes
|
||||
|
||||
### Progress Display
|
||||
- Use physical `view.renderer.page` and `view.renderer.pages` for page counts (#3213, #3200)
|
||||
- Last page shows 100% by fixing boundary condition (#3383)
|
||||
- FB2 subsections need special handling for progress (#3136)
|
||||
|
||||
### Translation View (#3078)
|
||||
- Problem: Page jumps back during full-text translation
|
||||
- Root cause: DOM mutations from sequential translation insertions cause paginator relayout
|
||||
- Fix: Batch DOM updates with 50ms timer, use bounded concurrent queue (max 5), show loading overlay
|
||||
|
||||
### TOC Navigation (#3124)
|
||||
- Problem: Expanding TOC chapter scrolls back to current chapter
|
||||
- Fix: Only scroll-into-view on navigation, not on expand/collapse
|
||||
|
||||
## Accessibility (a11y) Fixes
|
||||
|
||||
### Screen Reader (TalkBack) Support
|
||||
- **Page indicator updates** (#2276): Add focus handlers on `<p>` elements that call `view.goTo(cfi)` to update position
|
||||
- **Navigation buttons** (#3036): Always show prev/next buttons when screen reader active; `PageNavigationButtons.tsx`
|
||||
- **Dropdown menus** (#3035): Use `DropdownContext` with overlay dismiss instead of blur-based closing
|
||||
|
||||
### Dropdown Architecture for a11y
|
||||
- `DropdownContext` (`src/context/DropdownContext.tsx`) manages which dropdown is open globally
|
||||
- Uses `useId()` for unique identification
|
||||
- One dropdown open at a time
|
||||
- `<Overlay>` for dismissal (tap/click outside) instead of `onBlur`
|
||||
- `<details>` element with `open={isOpen}` for semantic structure
|
||||
- No auto-focus-first-item (conflicts with TalkBack)
|
||||
|
||||
## E-ink Readability
|
||||
- Use `not-eink:` Tailwind variant for colors and opacity (#3258)
|
||||
- Don't use `text-primary` (blue) or low opacity on e-ink
|
||||
- Highlights use foreground color in dark mode e-ink (#3299)
|
||||
|
||||
## Key Utility Functions
|
||||
- `snapRangeToWords()` in `src/utils/sel.ts` - Word boundary snapping
|
||||
- `handleAccessibilityEvents()` in `src/utils/a11y.ts` - Screen reader focus handling
|
||||
- `color-mix()` CSS function for custom highlight colors with opacity
|
||||
@@ -0,0 +1,119 @@
|
||||
# Bug Fixing Patterns & Strategies
|
||||
|
||||
## Common Root Cause Categories
|
||||
|
||||
### 1. Overly Broad CSS Selectors
|
||||
**Pattern:** A CSS rule targets too many elements, causing unintended visual side effects.
|
||||
**Examples:**
|
||||
- `hr { mix-blend-mode: multiply }` applied to ALL hr elements instead of only decorative ones (#3086)
|
||||
- `p img { mix-blend-mode }` applied to block images, not just inline (#3112)
|
||||
- `svg, img { height: auto; width: auto }` overrode explicit HTML width/height attributes (#3274)
|
||||
- Background-color override applied unconditionally instead of only when user enabled color override (#3316)
|
||||
|
||||
**Fix Strategy:** Narrow selectors with class qualifiers (`.background-img`, `.has-text-siblings`) or attribute pseudo-selectors (`:where(:not([width]))`). Check if the rule should be conditional on a user setting.
|
||||
|
||||
### 2. Conditional vs Unconditional Style Overrides
|
||||
**Pattern:** CSS rules meant for "Override Book Color/Layout" mode are placed in the always-active stylesheet.
|
||||
**Examples:**
|
||||
- Calibre `.calibre { color: unset }` was in `getLayoutStyles()` instead of `getColorStyles()` (#3448)
|
||||
- Image background-color override applied without checking `overrideColor` flag (#3316, #3377)
|
||||
|
||||
**Fix Strategy:** Move rules to the correct conditional block: `getColorStyles()` for color overrides, `getLayoutStyles()` for layout overrides. Check the `overrideColor`/`overrideLayout` flags.
|
||||
|
||||
### 3. Missing EPUB Stylesheet Transformations
|
||||
**Pattern:** EPUB stylesheets contain CSS that conflicts with app functionality.
|
||||
**Examples:**
|
||||
- `user-select: none` prevents text selection (#3370) -> regex replace in `transformStylesheet()`
|
||||
- `font-family: serif/sans-serif` on body bypasses user font (#3334) -> detect and unset
|
||||
- Hardcoded Calibre backgrounds persist in dark mode (#3448) -> unset in color override
|
||||
|
||||
**Fix Strategy:** Add regex-based transformation passes in `transformStylesheet()` in `style.ts`.
|
||||
|
||||
### 4. Stale State / Refs Not Reset
|
||||
**Pattern:** A `useRef` or state variable is set once and never properly reset, blocking re-entry.
|
||||
**Examples:**
|
||||
- TTS `ttsOnRef` prevented restarting TTS from a new location (#3292)
|
||||
- `initializedRef` in AnnotationRangeEditor prevented handle position updates (#3286)
|
||||
- `view.tts` not nulled on shutdown prevented clean TTS restart (#3400)
|
||||
- TTS safety timeout fired after pause, advancing to next sentence (#3244)
|
||||
|
||||
**Fix Strategy:** Check all refs/guards in the affected flow. Ensure cleanup in shutdown/unmount. Remove overly aggressive guards that prevent re-entry.
|
||||
|
||||
### 5. Platform API Differences
|
||||
**Pattern:** A Web API behaves differently or is unavailable on certain platforms.
|
||||
**Examples:**
|
||||
- `navigator.getGamepads()` returns null on older Android WebView (#3245)
|
||||
- `CompressionStream` unavailable on some Android versions (#3255)
|
||||
- `btoa()` throws on non-ASCII characters (#3436)
|
||||
- View Transitions API unsupported in WebKitGTK/Linux (#3417)
|
||||
- `document.startViewTransition()` crashes on Linux
|
||||
|
||||
**Fix Strategy:** Always check API availability before use. Add fallback paths. Use feature detection, not platform detection when possible.
|
||||
|
||||
### 6. Safe Area Inset Issues
|
||||
**Pattern:** UI elements overlap system bars (status bar, navigation bar, notch) on mobile.
|
||||
**Examples:**
|
||||
- Zoom controls behind status bar (#3426)
|
||||
- Android navigation bar overlap (#3466)
|
||||
- iPad sidebar insets incorrect (#3395)
|
||||
- Reader page layout jump after system UI change (#3469)
|
||||
|
||||
**Fix Strategy:** Use `gridInsets` and `statusBarHeight` from `useSafeAreaInsets`. Use `env(safe-area-inset-*)` CSS functions. Call `onUpdateInsets()` after system UI visibility changes. See `docs/safe-area-insets.md`.
|
||||
|
||||
### 7. Z-Index Layering Issues
|
||||
**Pattern:** Interactive elements rendered behind other layers, becoming unclickable.
|
||||
**Examples:**
|
||||
- Navigation buttons invisible on mobile (#3201) -> added `z-10`
|
||||
- Annotation nav bar too prominent (#3386) -> reduced from `z-30` to `z-10`
|
||||
- Page nav buttons behind TTS control (#3184)
|
||||
|
||||
**Fix Strategy:** Check z-index ordering. Use minimum necessary z-index. Reference the z-index hierarchy in the codebase.
|
||||
|
||||
### 8. Event Handling Race Conditions
|
||||
**Pattern:** Timing issues between pointer events, native menus, and React state updates.
|
||||
**Examples:**
|
||||
- macOS context menu steals pointer event loop (#3324) -> 100ms setTimeout delay
|
||||
- Traffic light buttons flicker due to timeout race (#3488, #3129)
|
||||
- Android tool buttons unresponsive due to premature re-selection (#3225)
|
||||
|
||||
**Fix Strategy:** Add small delays before native menu calls. Check event state machine consistency. Remove premature re-triggers on Android.
|
||||
|
||||
### 9. foliate-js Rendering Issues
|
||||
**Pattern:** Bugs in the lower-level EPUB renderer (paginator.js, epub.js).
|
||||
**Examples:**
|
||||
- Image size not constrained in double-page mode (#3432)
|
||||
- Background not shown in scrolled mode (#3344)
|
||||
- Section content cached incorrectly after mode switch (#3242, #3206)
|
||||
- Swipe sensitivity too low for non-animated paging (#3310)
|
||||
|
||||
**Fix Strategy:** Check both `columnize()` and `scrolled()` code paths in paginator.js. Verify CSS variables (`--available-width`, `--available-height`) are computed correctly. Test in both paginated and scrolled modes.
|
||||
|
||||
### 10. Progress/Navigation Calculation Errors
|
||||
**Pattern:** Page counts, progress percentages, or position tracking are wrong.
|
||||
**Examples:**
|
||||
- Progress shows 99.9% at last page (#3383) -> boundary condition
|
||||
- Pages left shows estimated instead of physical count (#3213, #3200)
|
||||
- FB2 subsection progress wrong (#3136) -> nested structure not handled
|
||||
- TOC auto-scrolls on expand (#3124) -> scroll-into-view triggered too broadly
|
||||
|
||||
**Fix Strategy:** Use physical `view.renderer.page`/`view.renderer.pages` instead of estimated section metadata. Check boundary conditions (0-indexed vs 1-indexed, inclusive vs exclusive).
|
||||
|
||||
### 11. Multiview Paginator Side Effects
|
||||
**Pattern:** The multiview paginator (e925e9d+) loads adjacent sections in background. Events from these loads can interfere with user interactions on the primary section.
|
||||
**Examples:**
|
||||
- `load` event from adjacent section triggers `docLoadHandler` which re-adds ALL annotations, overwriting drag edits
|
||||
- Multiple overlayers with duplicate SVG `<clipPath>` IDs cause `url(#id)` to resolve to wrong element
|
||||
- `MagnifierLoupe` destroying/recreating body clone on every drag tick triggers ResizeObserver → expand → redraw
|
||||
|
||||
**Fix Strategy:** Scope event handlers to the loaded section's index. Use unique IDs for SVG elements across overlayer instances. Minimize iframe DOM mutations during drag operations.
|
||||
|
||||
## Debugging Workflow
|
||||
|
||||
1. **Identify the category** from the issue description
|
||||
2. **Check `style.ts`** first for any CSS-related visual bugs
|
||||
3. **Check foliate-js** for rendering/layout bugs
|
||||
4. **Check platform-specific code** for mobile/desktop differences
|
||||
5. **Write a failing test** before implementing the fix
|
||||
6. **Test in both paginated and scrolled modes** for layout changes
|
||||
7. **Test on multiple platforms** for any UI change
|
||||
8. **Run `pnpm build-check`** before submitting
|
||||
@@ -0,0 +1,74 @@
|
||||
# CSS & Style Fixes Reference
|
||||
|
||||
## The `style.ts` Pipeline (`src/utils/style.ts`)
|
||||
|
||||
This is the most bug-prone file in the codebase (14+ fixes). It handles all EPUB CSS transformations.
|
||||
|
||||
### Key Functions
|
||||
|
||||
#### `getLayoutStyles()`
|
||||
- Always-active styles applied to every EPUB section
|
||||
- Controls: line-height, hyphens, image sizing, table display
|
||||
- Rules here should NOT be conditional on user settings
|
||||
- Common mistake: putting color-related rules here instead of `getColorStyles()`
|
||||
|
||||
#### `getColorStyles()`
|
||||
- Conditionally applied when user enables "Override Book Color"
|
||||
- Controls: foreground/background colors, mix-blend modes, image backgrounds
|
||||
- Gate rules on `overrideColor` flag
|
||||
|
||||
#### `transformStylesheet()`
|
||||
- Regex-based rewriting of EPUB CSS at load time
|
||||
- Runs on every stylesheet loaded from the EPUB
|
||||
- Used to neutralize problematic EPUB CSS declarations
|
||||
|
||||
#### `applyTableStyle()`
|
||||
- Post-render function that scales tables to fit available width
|
||||
- Uses `getComputedStyle()` (not inline `style.width`) to read actual width
|
||||
- Has two scaling paths: column-width-based and parent-container-based
|
||||
|
||||
### Fix History by Issue
|
||||
|
||||
| Issue | Problem | Fix in style.ts |
|
||||
|-------|---------|-----------------|
|
||||
| #3494 | Line spacing not on `<li>` | Added `li` CSS rule for `line-height` and `hyphens` |
|
||||
| #3448 | Calibre colors persist | Moved `.calibre` unset to `getColorStyles()`, added `background-color: unset` |
|
||||
| #3441 | Body padding/margin | Added `padding: unset; margin: unset` to body in `getLayoutStyles()` |
|
||||
| #3316 | Image bg unconditional | Made `background-color` rule conditional on `overrideColor` |
|
||||
| #3377 | Image bg override | Same pattern as #3316, only override when `overrideColor` is true |
|
||||
| #3334 | Generic font-family | `transformStylesheet()` replaces `font-family: serif/sans-serif` with `unset` on body |
|
||||
| #3370 | user-select: none | `transformStylesheet()` replaces all `user-select: none` with `unset` |
|
||||
| #3284 | Table scaling | Added fallback when `totalTableWidth` is 0 but parent has width |
|
||||
| #3351 | Table display broken | Added `display: table !important` to table rule |
|
||||
| #3274 | Image dimensions | Changed selectors to `:where(:not([width]))` and `:where(:not([height]))` |
|
||||
| #3205 | Table width reading | Changed from `style.width` to `getComputedStyle().width`, fixed CSS var unit |
|
||||
| #3112 | Mix-blend on all images | Narrowed selector to `.has-text-siblings` class |
|
||||
| #3086 | Mix-blend on hr | Narrowed selector to `hr.background-img` |
|
||||
| #3012 | Vertical alignment | Fixed available dimensions (subtract insets), replaced `100vw/vh` with CSS vars |
|
||||
|
||||
### Common Patterns
|
||||
|
||||
1. **Adding new element rules:** Copy the pattern from `p` rules (e.g., adding `li` for #3494)
|
||||
2. **EPUB CSS neutralization:** Add regex in `transformStylesheet()` to replace problematic declarations
|
||||
3. **Conditional overrides:** Use `overrideColor`/`overrideLayout` flags to gate rules
|
||||
4. **Selector narrowing:** Use class qualifiers or attribute pseudo-selectors to avoid over-matching
|
||||
5. **Table fixes:** Always use `getComputedStyle()`, not inline style. Check both width paths.
|
||||
|
||||
### CSS Variables from foliate-js
|
||||
|
||||
- `--available-width` - Usable content width (set by paginator.js)
|
||||
- `--available-height` - Usable content height
|
||||
- `--full-width` - Full viewport width (numeric, multiply by 1px)
|
||||
- `--full-height` - Full viewport height (numeric, multiply by 1px)
|
||||
- `--overlayer-highlight-opacity` - Highlight transparency (default 0.3)
|
||||
|
||||
### foliate-js Rendering (`packages/foliate-js/paginator.js`)
|
||||
|
||||
Key functions:
|
||||
- `columnize()` - Paginated layout path
|
||||
- `scrolled()` - Scrolled layout path
|
||||
- `setImageSize()` - Constrains image dimensions to available space
|
||||
- `#replaceBackground()` - Transfers EPUB backgrounds to paginator layer
|
||||
- `snap()` - Swipe gesture detection for page turning
|
||||
|
||||
Common issue: A fix applied to `columnize()` but not `scrolled()` (or vice versa). Always check both paths.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Layout & UI Fixes Reference
|
||||
|
||||
## Safe Area Insets
|
||||
|
||||
### Architecture
|
||||
- Native plugins push inset values: iOS (`NativeBridgePlugin.swift`), Android (`NativeBridgePlugin.kt`)
|
||||
- `useSafeAreaInsets` hook (`src/hooks/useSafeAreaInsets.ts`) reads and caches values
|
||||
- Components use `gridInsets` for positioning relative to safe areas
|
||||
- CSS: `env(safe-area-inset-top/bottom/left/right)` for CSS-level insets
|
||||
|
||||
### Common Issues
|
||||
- **Stale insets after system UI change** (#3469): Call `onUpdateInsets()` after `setSystemUIVisibility()`
|
||||
- **iPad sidebar insets wrong** (#3395): Different inset handling needed for sidebar vs main view
|
||||
- **Android nav bar overlap** (#3466): Use `calc(env(safe-area-inset-bottom) + 16px)` for Android bottom padding
|
||||
- **Zoom controls behind status bar** (#3426): Pass `gridInsets` through component chain, use `Math.max(gridInsets.top, statusBarHeight)`
|
||||
|
||||
### Rules (see also `docs/safe-area-insets.md`)
|
||||
- Always pass `gridInsets` to overlay/floating components near screen edges
|
||||
- On Android, account for system navigation bar with `env(safe-area-inset-bottom)`
|
||||
- After toggling system UI visibility, force-refresh insets via `onUpdateInsets()`
|
||||
|
||||
## Z-Index Hierarchy
|
||||
- Navigation buttons: `z-10` (when visible)
|
||||
- Annotation nav bar: `z-10` (reduced from z-30 in #3386)
|
||||
- TTS control button: ensure above page nav buttons
|
||||
- Floating overlays: check against `gridInsets` positioning
|
||||
|
||||
## macOS Traffic Lights
|
||||
- Managed by `trafficLightStore.ts` and `HeaderBar.tsx`
|
||||
- **Fullscreen check required** (#3129): `await currentWindow.isFullscreen()` before hiding
|
||||
- **Timeout for visibility toggle** (#3488): Use 100ms delay to prevent flickering
|
||||
- **Sidebar interaction** (#3488): Check `getIsSideBarVisible()` before hiding traffic lights
|
||||
- Never hide traffic lights when sidebar is open
|
||||
|
||||
## Touch/Input Issues
|
||||
- **Slider hit area on iOS** (#3382): Use min-h-12, strip browser appearance with CSS
|
||||
- **Context menu on macOS** (#3324): 100ms delay before `onContextMenu()` to let pointer events complete
|
||||
- **Swipe sensitivity** (#3310): Use average velocity (distance/time) instead of instantaneous velocity for non-animated paging
|
||||
- **Touchpad natural scrolling** (#3127): Respect system natural scrolling setting in `usePagination.ts`
|
||||
|
||||
## Dialog/Menu Layout
|
||||
- **Dialog header** (#3352): Use `px-2 sm:pe-3 sm:ps-2` padding to align with border radius
|
||||
- **Settings alignment** (#3151): Use `Menu` component instead of raw `div` for consistent styling
|
||||
- **Dropdown for screen readers** (#3035): Use `DropdownContext` with overlay dismiss, not blur-based closing
|
||||
|
||||
## Component-Specific Fixes
|
||||
|
||||
### HeaderBar.tsx
|
||||
- Traffic light visibility management
|
||||
- Sidebar toggle persistent position (#3193)
|
||||
- Library button placement
|
||||
|
||||
### PageNavigationButtons.tsx
|
||||
- z-10 when visible (#3201)
|
||||
- Always shown for screen readers (#3036)
|
||||
- Toggle via `showPageNavButtons` setting
|
||||
|
||||
### ProgressInfo.tsx
|
||||
- Use physical `page`/`pages` from renderer, not estimated values (#3213, #3200)
|
||||
- CSS classes: `time-left-label`, `pages-left-label`, `progress-info-label` (#3343)
|
||||
|
||||
### ReadingRuler.tsx
|
||||
- Remove `containerStyle` from overlay so dimmed area covers full screen (#3304)
|
||||
|
||||
### NavigationBar.tsx
|
||||
- Handle `gridInsets` internally, not via pre-computed `navPadding` (#3466)
|
||||
|
||||
### ContentNavBar.tsx (annotation search results)
|
||||
- Floating buttons with drop shadow, not full-width bar (#3386)
|
||||
- z-10 z-index
|
||||
@@ -0,0 +1,85 @@
|
||||
# Platform Compatibility Fixes Reference
|
||||
|
||||
## Android
|
||||
|
||||
### WebView API Issues
|
||||
- **`navigator.getGamepads()`** returns null on older WebView (#3245): Always null-check before `.some()`
|
||||
- **`CompressionStream`** unavailable on some Android WebView versions (#3255): Add fallback zip compression path
|
||||
- **Annotation tools unresponsive** (#3225): Don't call `makeSelection()` immediately on pointer-up; let popup flow complete naturally
|
||||
- **Safe inset updates** (#3469): Call `onUpdateInsets()` after `setSystemUIVisibility()`
|
||||
- **Navigation bar overlap** (#3466): Use `calc(env(safe-area-inset-bottom) + 16px)` for bottom padding
|
||||
|
||||
### General Android Rules
|
||||
- Test with older WebView versions (97+)
|
||||
- Always check API availability before calling Web APIs
|
||||
- Touch event handling differs from iOS - avoid premature re-selection
|
||||
|
||||
## iOS
|
||||
|
||||
### Common Issues
|
||||
- **Slider touch dead zones** (#3382): Strip native appearance, use larger hit areas (min-h-12)
|
||||
- **Safe area insets stale** (#3395): Native Swift plugin must push updated insets
|
||||
- **Section content caching** (#3242, #3206): Don't cache section content in foliate-js when updating subitems; cached content retains stale styles after mode switch
|
||||
- **CompressionStream** (#3255): Also broken on iOS 15.x; zip.js has its own native API disable
|
||||
- **zip.js native API** (#3170): Disable native `CompressionStream`/`DecompressionStream` on iOS 15.x
|
||||
|
||||
### iOS-Specific Code
|
||||
- `src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift`
|
||||
- Slider CSS: `-webkit-appearance: none; appearance: none` in globals.css
|
||||
- `useSafeAreaInsets.ts` hook
|
||||
|
||||
## macOS
|
||||
|
||||
### Traffic Lights (Window Controls)
|
||||
- Check `isFullscreen()` before hiding (#3129)
|
||||
- Use timeouts (100ms) for visibility transitions (#3488)
|
||||
- Don't hide when sidebar is open (#3488)
|
||||
|
||||
### Input Issues
|
||||
- **Context menu steals event loop** (#3324): 100ms setTimeout before calling menu.popup()
|
||||
- **Touchpad natural scrolling** (#3127): Respect system setting in `usePagination.ts`
|
||||
- **Two-finger swipe** (#3127): Support trackpad two-finger swipe for pagination
|
||||
|
||||
### macOS-Specific Code
|
||||
- `src-tauri/src/macos/` - Platform-specific Rust code
|
||||
- `src/store/trafficLightStore.ts`
|
||||
- `src/hooks/useTrafficLight.ts`
|
||||
|
||||
## Linux
|
||||
|
||||
### WebKitGTK Issues
|
||||
- **View Transitions API unsupported** (#3417): Feature-detect `document.startViewTransition` before calling
|
||||
- Use `useAppRouter.ts` to avoid transitions on Linux
|
||||
|
||||
## E-ink Devices
|
||||
|
||||
### Legibility Issues
|
||||
- **Low contrast colors** (#3258): Use `not-eink:` Tailwind variant prefix for colors/opacity
|
||||
- **Links invisible** (#3258): Don't apply `text-primary` (blue) on e-ink; use default text color
|
||||
- **Opacity too low** (#3258): Don't apply `opacity-60`/`opacity-75` on e-ink devices
|
||||
- **Highlight visibility** (#3299): Use foreground color for highlights in dark mode e-ink
|
||||
|
||||
### E-ink CSS Pattern
|
||||
```
|
||||
// Instead of:
|
||||
className="text-primary opacity-60"
|
||||
// Use:
|
||||
className="not-eink:text-primary not-eink:opacity-60"
|
||||
```
|
||||
|
||||
## Docker/Self-Hosting
|
||||
- **Missing submodules** (#3233): Run `git submodule update --init --recursive` before build
|
||||
- Simplecc WASM module must be initialized
|
||||
|
||||
## OPDS
|
||||
- **Non-ASCII credentials** (#3436): Use `TextEncoder` + manual Base64 instead of `btoa()`
|
||||
- **Author parsing** (#3120): Handle varied metadata structures in OPDS 2.0 feeds
|
||||
- **Responsive layout** (#3418): Ensure catalog and download button layout works on small screens
|
||||
|
||||
## Cross-Platform Testing Checklist
|
||||
1. Android (old WebView + current)
|
||||
2. iOS (15.x + current)
|
||||
3. macOS (traffic lights, trackpad)
|
||||
4. Linux (WebKitGTK)
|
||||
5. E-ink devices (contrast, colors)
|
||||
6. Web (CloudFlare Workers deployment)
|
||||
@@ -0,0 +1,50 @@
|
||||
# TTS (Text-to-Speech) Fixes Reference
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Components
|
||||
- `TTSController` (`src/services/tts/TTSController.ts`) - Core state machine
|
||||
- `EdgeTTSClient` (`src/services/tts/EdgeTTSClient.ts`) - Edge TTS provider
|
||||
- `useTTSControl` hook (`src/app/reader/hooks/useTTSControl.ts`) - React integration
|
||||
- `useTTSMediaSession` hook (`src/app/reader/hooks/useTTSMediaSession.ts`) - Media controls
|
||||
|
||||
### Section-Aware TTS Model
|
||||
TTS tracks its own section independently from the view via `#ttsSectionIndex`:
|
||||
- `#initTTSForSection()` - Creates TTS document for a section without changing the view
|
||||
- `#initTTSForNextSection()` / `#initTTSForPrevSection()` - Navigate TTS across sections
|
||||
- `#getHighlighter()` - Only returns highlighter if view section matches TTS section
|
||||
- `onSectionChange` callback - Notifies UI when TTS crosses section boundary
|
||||
- Highlights use CFI strings (not raw Range objects) for cross-section compatibility
|
||||
|
||||
### State Management Pitfalls
|
||||
1. **`#ttsSectionIndex` must match view section for highlights to work**
|
||||
- If `-1`, all highlight calls are suppressed
|
||||
- `shutdown()` sets it to `-1` but must also null out `this.view.tts`
|
||||
|
||||
2. **Guards/Refs that block re-entry:**
|
||||
- The old `ttsOnRef` guard blocked TTS restart from annotations (removed in #3292)
|
||||
- `view.tts` reference surviving shutdown blocked re-initialization (#3400)
|
||||
|
||||
3. **Timeouts that fire after pause:**
|
||||
- Edge TTS had a safety timeout that advanced sentences even when paused (#3244)
|
||||
- Solution: removed the entire `ontimeupdate` safety timeout mechanism
|
||||
|
||||
## Fix History
|
||||
|
||||
| Issue | Problem | Root Cause | Fix |
|
||||
|-------|---------|------------|-----|
|
||||
| #3100 | TTS scrolls too far | TTS coupled to view section | Added `#ttsSectionIndex`, "Back to TTS Location" button |
|
||||
| #3198 | TTS doesn't follow to next section | No `onSectionChange` callback | Added section change notification, extracted hooks |
|
||||
| #3244 | Paused TTS advances | Safety timeout fires after pause | Removed `ontimeupdate` timeout mechanism |
|
||||
| #3291 | TTS fails without lang attribute | Invalid SSML from missing lang | Set lang/xml:lang on html element from `ttsLang` |
|
||||
| #3292 | Can't restart TTS from annotation | `ttsOnRef` blocks re-entry | Removed the guard ref entirely |
|
||||
| #3400 | TTS highlight stops after restart | `view.tts` not nulled on shutdown | Added `this.view.tts = null` in `shutdown()` |
|
||||
|
||||
## Debugging TTS Issues
|
||||
|
||||
1. **TTS doesn't start:** Check `#initTTSForSection()` - does `view.tts.doc === doc` shortcut early?
|
||||
2. **No highlights:** Check `#ttsSectionIndex` matches view's section index
|
||||
3. **Advances when paused:** Look for setTimeout/timer callbacks that bypass pause state
|
||||
4. **Can't restart:** Check for refs/guards that prevent re-entry into speak handlers
|
||||
5. **Fails on some chapters:** Check if chapter has lang attribute and XHTML namespace
|
||||
6. **SSML errors:** Check `src/utils/ssml.ts` for proper namespace/lang handling
|
||||
@@ -0,0 +1,5 @@
|
||||
## Test-First Development
|
||||
|
||||
- Always write a failing unit test **before** implementing a fix.
|
||||
- Run the test to confirm it reproduces the bug or fails as expected, then apply the fix and verify the test passes.
|
||||
- Run the full test suite (`pnpm test`) after changes to ensure no regressions.
|
||||
@@ -0,0 +1,5 @@
|
||||
## TypeScript
|
||||
|
||||
- Never use the `any` type. Use `unknown`, proper types, or generics instead.
|
||||
- Strict mode is enabled. Target is ES2022.
|
||||
- Unused vars prefixed with `_` are allowed (ESLint configured).
|
||||
@@ -0,0 +1,8 @@
|
||||
## Verification (done-conditions)
|
||||
|
||||
Before marking work complete, all applicable checks must pass:
|
||||
|
||||
1. `pnpm test` — unit tests
|
||||
2. `pnpm lint` — ESLint
|
||||
3. `pnpm fmt:check` — Rust format check (only when `src-tauri/` files changed)
|
||||
4. `pnpm clippy:check` — Rust lint (only when `src-tauri/` files changed)
|
||||
@@ -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="
|
||||
|
||||
@@ -28,3 +28,5 @@ S3_ACCESS_KEY_ID=PLACE_HOLDER
|
||||
S3_SECRET_ACCESS_KEY=PLACE_HOLDER
|
||||
S3_BUCKET_NAME=PLACE_HOLDER
|
||||
S3_REGION=PLACE_HOLDER
|
||||
|
||||
TEMP_STORAGE_PUBLIC_BUCKET_NAME=PLACE_HOLDER
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
NEXT_PUBLIC_APP_PLATFORM=tauri
|
||||
NEXT_PUBLIC_APP_PLATFORM=tauri
|
||||
|
||||
DBUS_ID=com.bilingify.readest
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_APP_PLATFORM=tauri
|
||||
AI_GATEWAY_API_KEY=your_key_here
|
||||
@@ -0,0 +1,3 @@
|
||||
NEXT_PUBLIC_APP_PLATFORM=web
|
||||
AI_GATEWAY_API_KEY=your_key_here
|
||||
NEXT_PUBLIC_AI_GATEWAY_API_KEY=your_key_here
|
||||
@@ -48,6 +48,9 @@ tauri.*.conf.json
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
|
||||
#generated
|
||||
src-tauri/gen
|
||||
|
||||
@@ -60,3 +63,8 @@ src-tauri/gen
|
||||
/public/fallback-*.js
|
||||
/public/swe-worker-*.js
|
||||
|
||||
/dist/
|
||||
|
||||
.claude/settings.local.json
|
||||
.claude/skills
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
## Project Overview
|
||||
|
||||
Readest is a cross-platform ebook reader built as a **Next.js 16 + Tauri v2** hybrid app. It's part of a pnpm monorepo at `/apps/readest-app/`. The app runs on web (CloudFlare Workers), desktop (macOS/Windows/Linux via Tauri), and mobile (iOS/Android via Tauri).
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm dev-web # Web-only dev server (no Rust compilation needed)
|
||||
pnpm tauri dev # Desktop dev with Tauri (compiles Rust backend)
|
||||
|
||||
# Building
|
||||
pnpm build # Build Next.js for Tauri
|
||||
pnpm build-web # Build Next.js for web deployment
|
||||
|
||||
# Testing (see [docs/testing.md](docs/testing.md) for full details)
|
||||
pnpm test # Unit tests (vitest + jsdom)
|
||||
pnpm test -- src/__tests__/utils/misc.test.ts # Run a single test file
|
||||
pnpm test -- --watch # Watch mode
|
||||
pnpm test:browser # Browser tests (Chromium via Playwright)
|
||||
pnpm tauri:dev:test # Start Tauri app with webdriver
|
||||
pnpm test:tauri # Run Tauri integration tests
|
||||
|
||||
# Linting & Formatting
|
||||
pnpm lint # ESLint
|
||||
pnpm format # Prettier (runs from monorepo root)
|
||||
pnpm format:check # Check formatting without writing
|
||||
|
||||
# Rust
|
||||
pnpm fmt:check # Check formatting Rust code (src-tauri)
|
||||
pnpm clippy:check # Lint Rust code (src-tauri)
|
||||
```
|
||||
|
||||
### Source Layout
|
||||
|
||||
| Directory | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| `src/app/` | Next.js App Router pages and API routes |
|
||||
| `src/components/` | React components (reader, settings, library, assistant, etc.) |
|
||||
| `src/services/` | Business logic: TTS, translators, OPDS, sync, AI, metadata |
|
||||
| `src/store/` | Zustand state stores |
|
||||
| `src/hooks/` | Custom React hooks |
|
||||
| `src/libs/` | Document loaders, payment, storage, sync |
|
||||
| `src/utils/` | Pure utility functions |
|
||||
| `src/types/` | TypeScript type definitions |
|
||||
| `src/context/` | React Context providers (Auth, Env, Sync, etc.) |
|
||||
| `src/workers/` | Web Workers for background tasks |
|
||||
| `src-tauri/` | Rust backend: Tauri plugins, platform-specific code |
|
||||
|
||||
### Path Aliases (tsconfig)
|
||||
|
||||
- `@/*` → `./src/*`
|
||||
- `@/components/ui/*` → `./src/components/primitives/*`
|
||||
|
||||
### Rust Backend (`src-tauri/`)
|
||||
|
||||
Platform-specific code lives in `src-tauri/src/{macos,windows,android,ios}/`. Custom Tauri plugins are in `src-tauri/plugins/`.
|
||||
|
||||
## Project Rules
|
||||
|
||||
Rules are in `.claude/rules/`: test-first, typescript, verification.
|
||||
|
||||
### i18n
|
||||
|
||||
See [docs/i18n.md](docs/i18n.md) for the key-as-content translation approach, `stubTranslation` usage in non-React modules, and extraction workflow.
|
||||
|
||||
### Safe Area Insets
|
||||
|
||||
See [docs/safe-area-insets.md](docs/safe-area-insets.md) for rules on handling top/bottom insets for UI elements near screen edges.
|
||||
|
||||
## Web Browsing & QA (gstack)
|
||||
|
||||
For all web browsing, QA testing, and site interaction, use the `/browse` skill from gstack. **Never use `mcp__claude-in-chrome__*` tools directly.**
|
||||
|
||||
Available gstack skills:
|
||||
|
||||
- `/plan-ceo-review` — CEO/founder-mode plan review
|
||||
- `/plan-eng-review` — Eng manager-mode plan review
|
||||
- `/plan-design-review` — Designer's eye review of a live site
|
||||
- `/design-consultation` — Design system consultation
|
||||
- `/review` — Pre-landing PR review
|
||||
- `/ship` — Ship workflow (merge, test, review, bump, PR)
|
||||
- `/browse` — Fast headless browser for QA and site interaction
|
||||
- `/qa` — QA test and fix bugs
|
||||
- `/qa-only` — QA report only (no fixes)
|
||||
- `/qa-design-review` — Designer's eye QA with fixes
|
||||
- `/setup-browser-cookies` — Import cookies for authenticated testing
|
||||
- `/retro` — Weekly engineering retrospective
|
||||
- `/document-release` — Post-ship documentation update
|
||||
|
||||
If gstack skills aren't working, run `cd .claude/skills/gstack && ./setup` to build the binary and register skills.
|
||||
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/libs",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
## i18n Guide
|
||||
|
||||
Readest uses a **key-as-content** approach — English strings are the translation keys. The English locale (`en/translation.json`) is empty because keys serve as content. Other locales contain actual translations.
|
||||
|
||||
### In React Components
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
const _ = useTranslation();
|
||||
_('Progress synced');
|
||||
```
|
||||
|
||||
### In Non-React Modules
|
||||
|
||||
Two-step process:
|
||||
|
||||
**1. Declaration** — Use `stubTranslation` to mark strings for scanner extraction (returns key as-is, does NOT translate):
|
||||
|
||||
```ts
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
|
||||
// These calls only register keys for extraction
|
||||
_('Reveal in Finder');
|
||||
_('Reveal in Explorer');
|
||||
```
|
||||
|
||||
**2. Usage** — In the React component that consumes the value, apply the real `_()` from `useTranslation`:
|
||||
|
||||
```tsx
|
||||
const _ = useTranslation();
|
||||
const label = _(getRevealLabel()); // translates at runtime
|
||||
```
|
||||
|
||||
### Extraction & Translation
|
||||
|
||||
```bash
|
||||
pnpm i18n:extract # Scans codebase, adds new keys with __STRING_NOT_TRANSLATED__
|
||||
```
|
||||
|
||||
- Translation files: `public/locales/<locale>/translation.json`
|
||||
- Only `_('KEY')` and `_('KEY', options)` patterns are recognized by i18next-scanner
|
||||
|
||||
### Rules
|
||||
|
||||
- `stubTranslation` is for extraction only — always apply `_()` from `useTranslation` in the component for runtime translation.
|
||||
- Fallback: when no translation exists, the English key itself is displayed.
|
||||
- Error messages: register keys with `stubTranslation` in utility modules (e.g. `src/services/errors.ts`), return the English key from helpers, wrap with `_()` in the component.
|
||||
@@ -0,0 +1,52 @@
|
||||
## Safe Area Insets
|
||||
|
||||
The app runs on devices with notches, status bars, and rounded corners (iOS, Android). UI elements near screen edges must account for safe area insets to avoid being obscured.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **`gridInsets: Insets`** — Per-view insets derived from view settings (header/footer visibility, margins). Calculated by `getViewInsets()` in `src/utils/insets.ts`. Passed as a prop from `BooksGrid` → child components.
|
||||
- **`statusBarHeight: number`** — OS status bar height (default 24px). Stored in `themeStore`.
|
||||
- **`systemUIVisible: boolean`** — Whether the system UI (status bar, navigation bar) is currently shown. Stored in `themeStore`.
|
||||
- **`appService?.hasSafeAreaInset`** — Whether the platform requires safe area handling (mobile devices).
|
||||
|
||||
### Top Inset Rules
|
||||
|
||||
For UI elements anchored to the **top** of the screen (headers, close buttons, overlays):
|
||||
|
||||
```tsx
|
||||
// When system UI is visible, use the larger of gridInsets.top and statusBarHeight
|
||||
// When system UI is hidden, use gridInsets.top alone
|
||||
style={{
|
||||
marginTop: systemUIVisible
|
||||
? `${Math.max(gridInsets.top, statusBarHeight)}px`
|
||||
: `${gridInsets.top}px`,
|
||||
}}
|
||||
```
|
||||
|
||||
For containers that need safe area padding at the top:
|
||||
|
||||
```tsx
|
||||
style={{
|
||||
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
|
||||
}}
|
||||
```
|
||||
|
||||
### Bottom Inset Rules
|
||||
|
||||
For UI elements anchored to the **bottom** of the screen (footer bars, controls, progress indicators), use `gridInsets.bottom * 0.33` as padding — a fraction of the full inset since bottom bars don't need as much clearance as the home indicator area:
|
||||
|
||||
```tsx
|
||||
style={{
|
||||
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0,
|
||||
}}
|
||||
```
|
||||
|
||||
### Passing `gridInsets`
|
||||
|
||||
When creating overlay components (image viewers, table viewers, zoom controls, etc.), always pass `gridInsets` as a prop so they can position their controls correctly:
|
||||
|
||||
```tsx
|
||||
<ImageViewer gridInsets={gridInsets} ... />
|
||||
<TableViewer gridInsets={gridInsets} ... />
|
||||
<ZoomControls gridInsets={gridInsets} ... />
|
||||
```
|
||||
@@ -0,0 +1,110 @@
|
||||
# Testing
|
||||
|
||||
Readest uses three test tiers, all powered by [Vitest](https://vitest.dev/).
|
||||
|
||||
## Unit Tests (`pnpm test`)
|
||||
|
||||
Runs tests in a **jsdom** environment. No browser or Tauri runtime required.
|
||||
|
||||
```bash
|
||||
pnpm test # Run all unit tests
|
||||
pnpm test -- src/__tests__/utils/misc.test.ts # Run a single file
|
||||
pnpm test -- --watch # Watch mode
|
||||
```
|
||||
|
||||
- **Config:** `vitest.config.mts`
|
||||
- **Pattern:** `src/**/*.test.ts` (excludes `*.browser.test.ts` and `*.tauri.test.ts`)
|
||||
- **Environment:** jsdom
|
||||
- **Use for:** Pure logic, utilities, services that don't need real browser APIs or Tauri IPC.
|
||||
|
||||
## Browser Tests (`pnpm test:browser`)
|
||||
|
||||
Runs tests in a **real Chromium** browser via Playwright. Required for code that depends on Web Workers, SharedArrayBuffer, OPFS, or other browser-only APIs.
|
||||
|
||||
```bash
|
||||
pnpm test:browser
|
||||
```
|
||||
|
||||
- **Config:** `vitest.browser.config.mts`
|
||||
- **Pattern:** `src/**/*.browser.test.ts`
|
||||
- **Browser:** Chromium (headless, via `@vitest/browser-playwright`)
|
||||
- **Use for:** WASM modules (e.g. `@tursodatabase/database-wasm`), Web Worker integration, browser-specific storage APIs.
|
||||
|
||||
## Tauri Integration Tests (`pnpm test:tauri`)
|
||||
|
||||
Runs Vitest tests **inside the Tauri WebView**, with access to Tauri IPC and native plugin commands. Tests execute in the actual app environment.
|
||||
|
||||
### Step 1: Start the Tauri App
|
||||
|
||||
In one terminal, start the app with the `webdriver` feature enabled:
|
||||
|
||||
```bash
|
||||
pnpm tauri:dev:test # Dev mode (uses tauri dev server, faster iteration)
|
||||
pnpm tauri:build:test # Debug release build (closer to production)
|
||||
```
|
||||
|
||||
These commands compile the Rust backend with `--features webdriver`, which:
|
||||
|
||||
- Includes `tauri-plugin-webdriver` (embeds a W3C WebDriver server on port 4445)
|
||||
- Adds a runtime capability granting plugin permissions to remote URLs (`http://127.0.0.1:*`), so Vitest's browser-mode iframe can call Tauri IPC
|
||||
|
||||
Keep this running while you run tests.
|
||||
|
||||
### Step 2: Run Tests
|
||||
|
||||
In another terminal:
|
||||
|
||||
```bash
|
||||
pnpm test:tauri
|
||||
```
|
||||
|
||||
Vitest connects directly to the embedded WebDriver server (port 4445) in the running Tauri app and executes tests inside its WebView.
|
||||
|
||||
- **Config:** `vitest.tauri.config.mts`
|
||||
- **Pattern:** `src/**/*.tauri.test.ts`
|
||||
- **Browser provider:** `@vitest/browser-webdriverio` (connects to port 4445)
|
||||
- **Use for:** Tauri plugin commands (turso, native-tts, etc.), native filesystem, Tauri IPC.
|
||||
|
||||
### Writing Tauri Tests
|
||||
|
||||
Tests access Tauri IPC via a shared helper:
|
||||
|
||||
```typescript
|
||||
import { invoke } from '../tauri/tauri-invoke';
|
||||
|
||||
it('calls a plugin command', async () => {
|
||||
const result = await invoke('plugin:turso|load', { options: { path: 'sqlite::memory:' } });
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
The `invoke()` helper accesses `window.top.__TAURI_INTERNALS__` (Vitest runs in an iframe, Tauri injects IPC into the main frame).
|
||||
|
||||
**Limitations:** Only custom invoke commands and plugin commands listed in the webdriver capability work. Standard Tauri JS APIs (e.g. `@tauri-apps/api`) that rely on `URL: local` may not work from the Vitest iframe.
|
||||
|
||||
## E2E Tests (WDIO)
|
||||
|
||||
Full end-to-end tests using WebDriverIO, for UI-level testing against the running Tauri app. Same two-step workflow as Tauri integration tests.
|
||||
|
||||
```bash
|
||||
# Terminal 1: start the app (same as for Tauri integration tests)
|
||||
pnpm tauri:dev:test
|
||||
|
||||
# Terminal 2: run E2E tests
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
- **Config:** `wdio.conf.ts`
|
||||
- **Pattern:** `e2e/**/*.e2e.ts`
|
||||
- **Framework:** Mocha (via `@wdio/mocha-framework`)
|
||||
- **Connects to:** port 4445 (embedded WebDriver server)
|
||||
- **Use for:** UI interaction tests, window management, navigation flows.
|
||||
|
||||
## Test File Naming
|
||||
|
||||
| Suffix | Runner | Environment |
|
||||
| ------------------- | ------------------- | --------------------- |
|
||||
| `*.test.ts` | `pnpm test` | jsdom |
|
||||
| `*.browser.test.ts` | `pnpm test:browser` | Chromium (Playwright) |
|
||||
| `*.tauri.test.ts` | `pnpm test:tauri` | Tauri WebView |
|
||||
| `*.e2e.ts` | `pnpm test:e2e` | Tauri app (WDIO) |
|
||||
@@ -0,0 +1,123 @@
|
||||
describe('Readest App Launch', () => {
|
||||
it('should have a visible body element', async () => {
|
||||
const body = await $('body');
|
||||
await body.waitForDisplayed({ timeout: 10000 });
|
||||
expect(await body.isDisplayed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have the correct window handle', async () => {
|
||||
const handle = await browser.getWindowHandle();
|
||||
expect(handle).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return the page source', async () => {
|
||||
const source = await browser.getPageSource();
|
||||
expect(source).toContain('html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Library Page', () => {
|
||||
it('should navigate to the library page', async () => {
|
||||
const url = await browser.getUrl();
|
||||
expect(url).toMatch(/library|localhost/);
|
||||
});
|
||||
|
||||
it('should display the library container', async () => {
|
||||
const library = await $('[aria-label="Your Library"]');
|
||||
await library.waitForExist({ timeout: 15000 });
|
||||
expect(await library.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display the library header', async () => {
|
||||
const header = await $('[aria-label="Library Header"]');
|
||||
await header.waitForExist({ timeout: 10000 });
|
||||
expect(await header.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display the bookshelf area', async () => {
|
||||
const bookshelf = await $('[aria-label="Bookshelf"]');
|
||||
await bookshelf.waitForExist({ timeout: 10000 });
|
||||
expect(await bookshelf.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a search input', async () => {
|
||||
const searchInput = await $('.search-input');
|
||||
await searchInput.waitForExist({ timeout: 10000 });
|
||||
expect(await searchInput.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow typing in the search input', async () => {
|
||||
const searchInput = await $('.search-input');
|
||||
await searchInput.waitForDisplayed({ timeout: 10000 });
|
||||
await searchInput.setValue('test search');
|
||||
const value = await searchInput.getValue();
|
||||
expect(value).toBe('test search');
|
||||
});
|
||||
|
||||
it('should show the clear search button after typing', async () => {
|
||||
const clearBtn = await $('[aria-label="Clear Search"]');
|
||||
await clearBtn.waitForExist({ timeout: 5000 });
|
||||
expect(await clearBtn.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear the search input when clear button is clicked', async () => {
|
||||
const clearBtn = await $('[aria-label="Clear Search"]');
|
||||
await clearBtn.click();
|
||||
const searchInput = await $('.search-input');
|
||||
const value = await searchInput.getValue();
|
||||
expect(value).toBe('');
|
||||
});
|
||||
|
||||
it('should have a select books button', async () => {
|
||||
const selectBtn = await $('[aria-label="Select Books"]');
|
||||
await selectBtn.waitForExist({ timeout: 10000 });
|
||||
expect(await selectBtn.isExisting()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have an import books button', async () => {
|
||||
const importBtn = await $('[aria-label="Import Books"]');
|
||||
await importBtn.waitForExist({ timeout: 10000 });
|
||||
expect(await importBtn.isExisting()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Window Management', () => {
|
||||
it('should return the window size', async () => {
|
||||
const size = await browser.getWindowSize();
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JavaScript Execution', () => {
|
||||
it('should execute JavaScript in the app context', async () => {
|
||||
const result = await browser.execute(() => {
|
||||
return document.readyState;
|
||||
});
|
||||
expect(result).toBe('complete');
|
||||
});
|
||||
|
||||
it('should access the document title via JS', async () => {
|
||||
const title = await browser.execute(() => {
|
||||
return document.title;
|
||||
});
|
||||
expect(title).toContain('Readest');
|
||||
});
|
||||
|
||||
it('should detect the app platform globals', async () => {
|
||||
const hasCLIAccess = await browser.execute(() => {
|
||||
return (window as unknown as Record<string, unknown>).__READEST_CLI_ACCESS === true;
|
||||
});
|
||||
expect(hasCLIAccess).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate back to library after visiting another route', async () => {
|
||||
const currentUrl = await browser.getUrl();
|
||||
await browser.url(currentUrl.replace(/\/[^/]*$/, '/library'));
|
||||
const library = await $('[aria-label="Your Library"]');
|
||||
await library.waitForExist({ timeout: 15000 });
|
||||
expect(await library.isExisting()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["mocha", "@wdio/globals/types"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -1,13 +1,41 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import next from 'eslint-config-next';
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||
import tseslint from 'eslint-config-next/typescript';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
export default [...compat.extends("next/core-web-vitals", "next/typescript")];
|
||||
const eslintConfig = defineConfig([
|
||||
...tseslint,
|
||||
...next,
|
||||
...nextVitals,
|
||||
{
|
||||
rules: {
|
||||
...jsxA11y.configs.recommended.rules,
|
||||
'@next/next/no-img-element': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
globalIgnores([
|
||||
'node_modules/**',
|
||||
'.next/**',
|
||||
'.open-next/**',
|
||||
'.wrangler/**',
|
||||
'.claude/**',
|
||||
'dist/**',
|
||||
'out/**',
|
||||
'build/**',
|
||||
'public/**',
|
||||
'src-tauri/**',
|
||||
'next-env.d.ts',
|
||||
'i18next-scanner.config.cjs',
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories-next"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-rs-sys",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_thumbnail"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"directories-next",
|
||||
"image",
|
||||
"md5",
|
||||
"once_cell",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "windows_thumbnail"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
name = "windows_thumbnail"
|
||||
path = "src/mod.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
base64 = "0.22"
|
||||
directories-next = "2.0"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
|
||||
md5 = "0.8"
|
||||
once_cell = "1.19"
|
||||
zip = { version = "6.0", default-features = false, features = ["deflate"] }
|
||||
windows = { version = "0.62", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Security",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Registry",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
windows-core = "0.62"
|
||||
@@ -0,0 +1,134 @@
|
||||
# Windows Thumbnail Provider for Readest
|
||||
|
||||
This crate provides Windows Explorer thumbnail support for eBook files when Readest is set as the default application.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Cover Extraction**: Extracts cover images from EPUB, MOBI, AZW, AZW3, FB2, CBZ, CBR files
|
||||
- **Readest Branding**: Adds a small Readest icon overlay at the bottom-right corner
|
||||
- **Smart Caching**: Caches generated thumbnails for faster subsequent loads
|
||||
- **File Association Aware**: Only shows thumbnails when Readest is the default app for the file type
|
||||
- **COM Integration**: Full Windows Shell extension implementation via `IThumbnailProvider`
|
||||
|
||||
## Supported Formats
|
||||
|
||||
| Format | Extension | Cover Source |
|
||||
| ---------- | ----------------------- | ---------------------------- |
|
||||
| EPUB | `.epub` | OPF manifest cover reference |
|
||||
| MOBI/AZW | `.mobi`, `.azw`, `.prc` | EXTH cover offset |
|
||||
| AZW3/KF8 | `.azw3`, `.kf8` | KF8 format cover |
|
||||
| FB2 | `.fb2` | `<binary>` coverpage element |
|
||||
| Comic Book | `.cbz`, `.cbr` | First image in archive |
|
||||
| Plain Text | `.txt` | Generated placeholder |
|
||||
|
||||
## Building
|
||||
|
||||
### Library Only
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### COM DLL (for Windows Explorer integration)
|
||||
|
||||
```bash
|
||||
cargo build --release --features com
|
||||
```
|
||||
|
||||
### CLI Tool
|
||||
|
||||
```bash
|
||||
cargo build --release --features cli
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
The thumbnail provider DLL is automatically registered when Readest is installed via the NSIS installer.
|
||||
|
||||
### Manual Registration (for development)
|
||||
|
||||
```powershell
|
||||
# Register the DLL
|
||||
regsvr32 /s target\release\windows_thumbnail.dll
|
||||
|
||||
# Unregister the DLL
|
||||
regsvr32 /s /u target\release\windows_thumbnail.dll
|
||||
|
||||
# Refresh Explorer to see changes
|
||||
ie4uinit.exe -show
|
||||
```
|
||||
|
||||
After registration, you may need to restart Windows Explorer or log out/in for changes to take effect.
|
||||
|
||||
## Usage (Development / Manual testing)
|
||||
|
||||
For local development and testing, build the Windows DLL (or the library) from the Readest Tauri app folder and register it manually. The legacy CLI test harness used to live in the separate `packages/tauri` workspace, but the thumbnail handler implementation now lives inside Readest's Tauri app.
|
||||
|
||||
Build the DLL (for Windows explorer integration):
|
||||
|
||||
```powershell
|
||||
cd apps/readest-app/src-tauri
|
||||
cargo build --release --manifest-path Cargo.toml --features com
|
||||
```
|
||||
|
||||
The standalone CLI test harness is no longer distributed with the app. To test the thumbnail provider locally, build and register the DLL as shown above and use a small test harness that imports `readestlib`'s thumbnail code or use Explorer after registering the handler.
|
||||
|
||||
Manual registration for development (register the generated DLL):
|
||||
|
||||
```powershell
|
||||
# Register the DLL
|
||||
regsvr32 /s target\release\windows_thumbnail.dll
|
||||
|
||||
# Unregister the DLL
|
||||
regsvr32 /s /u target\release\windows_thumbnail.dll
|
||||
|
||||
# Refresh Explorer to see changes
|
||||
ie4uinit.exe -show
|
||||
```
|
||||
|
||||
This generates a thumbnail with the Readest overlay at the specified size.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Windows Explorer │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ IThumbnailProvider ───┼──► ThumbnailProvider │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ Check File Association │
|
||||
│ │ (is Readest the default?) │
|
||||
│ │ │ │
|
||||
│ │ ▼ (if yes) │
|
||||
│ │ Extract Cover Image │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ Add Readest Overlay │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ Return HBITMAP │
|
||||
│ │ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## COM Details
|
||||
|
||||
- **CLSID**: `{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}`
|
||||
- **Shell Thumbnail Handler GUID**: `{e357fccd-a995-4576-b01f-234630154e96}`
|
||||
- **Threading Model**: Apartment
|
||||
|
||||
## How It Works
|
||||
|
||||
1. When Windows Explorer needs a thumbnail, it queries the registered shell extension
|
||||
2. The COM DLL implements `IInitializeWithItem` to receive the file path
|
||||
3. It checks if Readest.exe is the default application for that file type using `AssocQueryStringW`
|
||||
4. If Readest is the default, it extracts the cover and generates the thumbnail
|
||||
5. If Readest is NOT the default, it returns `S_FALSE` to let Windows use other handlers
|
||||
|
||||
This ensures thumbnails only appear for files the user has associated with Readest.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details.
|
||||
@@ -0,0 +1,506 @@
|
||||
/// Windows COM Thumbnail Provider for Readest
|
||||
///
|
||||
/// Implements IThumbnailProvider and IInitializeWithItem for Windows Shell integration.
|
||||
/// This allows Windows Explorer to show book covers as thumbnails for eBook files.
|
||||
///
|
||||
/// **Important**: Thumbnails are only shown when Readest.exe is the default application
|
||||
/// for the file type.
|
||||
///
|
||||
/// ## CLSID: {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
|
||||
use std::cell::UnsafeCell;
|
||||
use std::ffi::c_void;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicIsize, AtomicU32, Ordering};
|
||||
|
||||
use windows::core::{IUnknown, Interface, GUID, HRESULT, PCWSTR, PWSTR};
|
||||
use windows::Win32::Foundation::{
|
||||
CLASS_E_NOAGGREGATION, E_FAIL, E_INVALIDARG, E_NOINTERFACE, HMODULE, S_FALSE, S_OK,
|
||||
};
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
CreateDIBSection, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HBITMAP,
|
||||
};
|
||||
use windows::Win32::System::Com::{CoTaskMemFree, IClassFactory, IClassFactory_Impl};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleFileNameW;
|
||||
use windows::Win32::System::Registry::{
|
||||
RegCloseKey, RegCreateKeyExW, RegDeleteTreeW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT,
|
||||
KEY_WRITE, REG_OPTION_NON_VOLATILE, REG_SZ,
|
||||
};
|
||||
use windows::Win32::UI::Shell::{
|
||||
AssocQueryStringW, IInitializeWithItem, IInitializeWithItem_Impl, IShellItem,
|
||||
IThumbnailProvider, IThumbnailProvider_Impl, ASSOCF_NONE, ASSOCSTR_EXECUTABLE,
|
||||
SIGDN_FILESYSPATH, WTSAT_ARGB, WTS_ALPHATYPE,
|
||||
};
|
||||
use windows_core::BOOL;
|
||||
use windows_core::{implement, Ref};
|
||||
|
||||
use super::cached_thumbnail_for_path;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// CLSID for Readest Thumbnail Provider
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// CLSID: {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
|
||||
pub const CLSID_READEST_THUMBNAIL: GUID = GUID::from_u128(0xA1B2C3D4_E5F6_7890_ABCD_EF1234567890);
|
||||
|
||||
/// Supported file extensions
|
||||
pub const SUPPORTED_EXTENSIONS: &[&str] = &[
|
||||
".epub", ".mobi", ".azw", ".azw3", ".kf8", ".prc", ".fb2", ".cbz", ".cbr", ".txt",
|
||||
];
|
||||
|
||||
// DLL reference counting
|
||||
static DLL_REF_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||
static DLL_MODULE_PTR: AtomicIsize = AtomicIsize::new(0);
|
||||
|
||||
fn dll_add_ref() {
|
||||
DLL_REF_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
fn dll_release() {
|
||||
DLL_REF_COUNT.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn set_dll_module(h: HMODULE) {
|
||||
DLL_MODULE_PTR.store(h.0 as isize, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn get_dll_module() -> Option<HMODULE> {
|
||||
let ptr = DLL_MODULE_PTR.load(Ordering::SeqCst);
|
||||
if ptr == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(HMODULE(ptr as *mut c_void))
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// File Association Check
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Check if Readest.exe is the default application for a given file extension.
|
||||
fn is_readest_default_for_extension(ext: &str) -> bool {
|
||||
let ext_wide: Vec<u16> = ext.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut buffer = [0u16; 260];
|
||||
let mut buffer_size = buffer.len() as u32;
|
||||
|
||||
unsafe {
|
||||
let result = AssocQueryStringW(
|
||||
ASSOCF_NONE,
|
||||
ASSOCSTR_EXECUTABLE,
|
||||
PCWSTR(ext_wide.as_ptr()),
|
||||
None,
|
||||
Some(PWSTR(buffer.as_mut_ptr())),
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
if result.is_ok() {
|
||||
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
|
||||
let path = String::from_utf16_lossy(&buffer[..len]).to_lowercase();
|
||||
return path.contains("readest");
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if Readest is the default app for a specific file path.
|
||||
fn is_readest_default_for_file(path: &PathBuf) -> bool {
|
||||
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
let ext_with_dot = format!(".{}", ext.to_lowercase());
|
||||
return is_readest_default_for_extension(&ext_with_dot);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ThumbnailProvider
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Interior mutability wrapper for COM single-threaded apartment
|
||||
struct ComCell<T>(UnsafeCell<T>);
|
||||
|
||||
impl<T> ComCell<T> {
|
||||
fn new(value: T) -> Self {
|
||||
Self(UnsafeCell::new(value))
|
||||
}
|
||||
fn get(&self) -> &T {
|
||||
unsafe { &*self.0.get() }
|
||||
}
|
||||
fn set(&self, value: T) {
|
||||
unsafe {
|
||||
*self.0.get() = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: COM thumbnail providers run in single-threaded apartment (STA)
|
||||
unsafe impl<T> Sync for ComCell<T> {}
|
||||
unsafe impl<T> Send for ComCell<T> {}
|
||||
|
||||
#[implement(IThumbnailProvider, IInitializeWithItem)]
|
||||
pub struct ThumbnailProvider {
|
||||
file_path: ComCell<Option<PathBuf>>,
|
||||
file_ext: ComCell<Option<String>>,
|
||||
should_provide: ComCell<bool>,
|
||||
}
|
||||
|
||||
impl ThumbnailProvider {
|
||||
pub fn new() -> Self {
|
||||
dll_add_ref();
|
||||
Self {
|
||||
file_path: ComCell::new(None),
|
||||
file_ext: ComCell::new(None),
|
||||
should_provide: ComCell::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThumbnailProvider {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ThumbnailProvider {
|
||||
fn drop(&mut self) {
|
||||
dll_release();
|
||||
}
|
||||
}
|
||||
|
||||
impl IInitializeWithItem_Impl for ThumbnailProvider_Impl {
|
||||
fn Initialize(&self, psi: Ref<'_, IShellItem>, _grfmode: u32) -> windows::core::Result<()> {
|
||||
let item = psi.ok()?;
|
||||
|
||||
unsafe {
|
||||
let path_pwstr = item.GetDisplayName(SIGDN_FILESYSPATH)?;
|
||||
|
||||
let mut len = 0usize;
|
||||
let mut ptr = path_pwstr.0;
|
||||
while *ptr != 0 {
|
||||
len += 1;
|
||||
ptr = ptr.add(1);
|
||||
}
|
||||
|
||||
let slice = std::slice::from_raw_parts(path_pwstr.0, len);
|
||||
let path_str = String::from_utf16_lossy(slice);
|
||||
let path = PathBuf::from(&path_str);
|
||||
|
||||
CoTaskMemFree(Some(path_pwstr.0 as *const c_void));
|
||||
|
||||
let is_default = is_readest_default_for_file(&path);
|
||||
self.should_provide.set(is_default);
|
||||
|
||||
if !is_default {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|s| s.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
self.file_path.set(Some(path));
|
||||
self.file_ext.set(Some(ext));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IThumbnailProvider_Impl for ThumbnailProvider_Impl {
|
||||
fn GetThumbnail(
|
||||
&self,
|
||||
cx: u32,
|
||||
phbmp: *mut HBITMAP,
|
||||
pdwalpha: *mut WTS_ALPHATYPE,
|
||||
) -> windows::core::Result<()> {
|
||||
if !*self.should_provide.get() {
|
||||
return Err(E_FAIL.into());
|
||||
}
|
||||
|
||||
let path = self.file_path.get().as_ref().ok_or(E_FAIL)?;
|
||||
let ext = self.file_ext.get().as_ref().ok_or(E_FAIL)?;
|
||||
|
||||
let png_bytes = cached_thumbnail_for_path(path, ext, cx).map_err(|_| E_FAIL)?;
|
||||
let img = image::load_from_memory(&png_bytes).map_err(|_| E_FAIL)?;
|
||||
let rgba = img.to_rgba8();
|
||||
let (width, height) = (rgba.width(), rgba.height());
|
||||
|
||||
let bmi = BITMAPINFO {
|
||||
bmiHeader: BITMAPINFOHEADER {
|
||||
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
|
||||
biWidth: width as i32,
|
||||
biHeight: -(height as i32),
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB.0,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut bits: *mut c_void = std::ptr::null_mut();
|
||||
|
||||
unsafe {
|
||||
let hbmp = CreateDIBSection(None, &bmi, DIB_RGB_COLORS, &mut bits, None, 0)
|
||||
.map_err(|_| E_FAIL)?;
|
||||
if bits.is_null() {
|
||||
return Err(E_FAIL.into());
|
||||
}
|
||||
|
||||
// RGBA -> BGRA
|
||||
let dst =
|
||||
std::slice::from_raw_parts_mut(bits as *mut u8, (width * height * 4) as usize);
|
||||
let src = rgba.as_raw();
|
||||
for i in 0..(width * height) as usize {
|
||||
let si = i * 4;
|
||||
dst[si] = src[si + 2]; // B
|
||||
dst[si + 1] = src[si + 1]; // G
|
||||
dst[si + 2] = src[si]; // R
|
||||
dst[si + 3] = src[si + 3]; // A
|
||||
}
|
||||
|
||||
*phbmp = hbmp;
|
||||
*pdwalpha = WTSAT_ARGB;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ClassFactory
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[implement(IClassFactory)]
|
||||
pub struct ThumbnailProviderFactory;
|
||||
|
||||
impl ThumbnailProviderFactory {
|
||||
pub fn new() -> Self {
|
||||
dll_add_ref();
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThumbnailProviderFactory {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ThumbnailProviderFactory {
|
||||
fn drop(&mut self) {
|
||||
dll_release();
|
||||
}
|
||||
}
|
||||
|
||||
impl IClassFactory_Impl for ThumbnailProviderFactory_Impl {
|
||||
fn CreateInstance(
|
||||
&self,
|
||||
punkouter: Ref<'_, IUnknown>,
|
||||
riid: *const GUID,
|
||||
ppvobject: *mut *mut c_void,
|
||||
) -> windows::core::Result<()> {
|
||||
unsafe {
|
||||
if ppvobject.is_null() {
|
||||
return Err(E_INVALIDARG.into());
|
||||
}
|
||||
*ppvobject = std::ptr::null_mut();
|
||||
if !punkouter.is_null() {
|
||||
return Err(CLASS_E_NOAGGREGATION.into());
|
||||
}
|
||||
|
||||
let provider: IThumbnailProvider = ThumbnailProvider::new().into();
|
||||
provider.query(&*riid, ppvobject).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn LockServer(&self, flock: BOOL) -> windows::core::Result<()> {
|
||||
if flock.as_bool() {
|
||||
dll_add_ref();
|
||||
} else {
|
||||
dll_release();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DLL Exports
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn DllMain(hinstance: HMODULE, reason: u32, _reserved: *mut c_void) -> BOOL {
|
||||
const DLL_PROCESS_ATTACH: u32 = 1;
|
||||
if reason == DLL_PROCESS_ATTACH {
|
||||
set_dll_module(hinstance);
|
||||
}
|
||||
BOOL::from(true)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn DllCanUnloadNow() -> HRESULT {
|
||||
if DLL_REF_COUNT.load(Ordering::SeqCst) == 0 {
|
||||
S_OK
|
||||
} else {
|
||||
S_FALSE
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn DllGetClassObject(
|
||||
rclsid: *const GUID,
|
||||
riid: *const GUID,
|
||||
ppv: *mut *mut c_void,
|
||||
) -> HRESULT {
|
||||
if ppv.is_null() || rclsid.is_null() || riid.is_null() {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
*ppv = std::ptr::null_mut();
|
||||
|
||||
if *rclsid != CLSID_READEST_THUMBNAIL {
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
if *riid != IClassFactory::IID && *riid != IUnknown::IID {
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
let factory: IClassFactory = ThumbnailProviderFactory::new().into();
|
||||
factory.query(&*riid, ppv)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn DllRegisterServer() -> HRESULT {
|
||||
match register_server_impl() {
|
||||
Ok(()) => S_OK,
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn DllUnregisterServer() -> HRESULT {
|
||||
let _ = unregister_server_impl();
|
||||
S_OK
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Registry helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_dll_path() -> Option<String> {
|
||||
let module = get_dll_module()?;
|
||||
let mut buffer = [0u16; 260];
|
||||
unsafe {
|
||||
let len = GetModuleFileNameW(Some(module), &mut buffer);
|
||||
if len == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf16_lossy(&buffer[..len as usize]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clsid_string() -> String {
|
||||
format!(
|
||||
"{{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}",
|
||||
CLSID_READEST_THUMBNAIL.data1,
|
||||
CLSID_READEST_THUMBNAIL.data2,
|
||||
CLSID_READEST_THUMBNAIL.data3,
|
||||
CLSID_READEST_THUMBNAIL.data4[0],
|
||||
CLSID_READEST_THUMBNAIL.data4[1],
|
||||
CLSID_READEST_THUMBNAIL.data4[2],
|
||||
CLSID_READEST_THUMBNAIL.data4[3],
|
||||
CLSID_READEST_THUMBNAIL.data4[4],
|
||||
CLSID_READEST_THUMBNAIL.data4[5],
|
||||
CLSID_READEST_THUMBNAIL.data4[6],
|
||||
CLSID_READEST_THUMBNAIL.data4[7]
|
||||
)
|
||||
}
|
||||
|
||||
fn to_wide(s: &str) -> Vec<u16> {
|
||||
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
unsafe fn set_reg_value(key: HKEY, name: &str, value: &str) -> Result<(), HRESULT> {
|
||||
let name_w = to_wide(name);
|
||||
let value_w = to_wide(value);
|
||||
let bytes: &[u8] = std::slice::from_raw_parts(value_w.as_ptr() as *const u8, value_w.len() * 2);
|
||||
if RegSetValueExW(key, PCWSTR(name_w.as_ptr()), Some(0), REG_SZ, Some(bytes)).is_err() {
|
||||
Err(E_FAIL)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_reg_key(parent: HKEY, subkey: &str) -> Result<HKEY, HRESULT> {
|
||||
let subkey_w = to_wide(subkey);
|
||||
let mut hkey = HKEY::default();
|
||||
let result = RegCreateKeyExW(
|
||||
parent,
|
||||
PCWSTR(subkey_w.as_ptr()),
|
||||
Some(0),
|
||||
None,
|
||||
REG_OPTION_NON_VOLATILE,
|
||||
KEY_WRITE,
|
||||
None,
|
||||
&mut hkey,
|
||||
None,
|
||||
);
|
||||
if result.is_err() {
|
||||
Err(E_FAIL)
|
||||
} else {
|
||||
Ok(hkey)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn register_server_impl() -> Result<(), HRESULT> {
|
||||
let dll_path = get_dll_path().ok_or(E_FAIL)?;
|
||||
let clsid = clsid_string();
|
||||
|
||||
// CLSID key
|
||||
let clsid_key = create_reg_key(HKEY_CLASSES_ROOT, &format!("CLSID\\{}", clsid))?;
|
||||
set_reg_value(clsid_key, "", "Readest Thumbnail Provider")?;
|
||||
|
||||
// CRITICAL: DisableProcessIsolation = 1
|
||||
let disable_isolation_name = to_wide("DisableProcessIsolation");
|
||||
let value: u32 = 1;
|
||||
let _ = windows::Win32::System::Registry::RegSetValueExW(
|
||||
clsid_key,
|
||||
PCWSTR(disable_isolation_name.as_ptr()),
|
||||
Some(0),
|
||||
windows::Win32::System::Registry::REG_DWORD,
|
||||
Some(std::slice::from_raw_parts(
|
||||
&value as *const u32 as *const u8,
|
||||
4,
|
||||
)),
|
||||
);
|
||||
|
||||
let inproc_key = create_reg_key(clsid_key, "InprocServer32")?;
|
||||
set_reg_value(inproc_key, "", &dll_path)?;
|
||||
set_reg_value(inproc_key, "ThreadingModel", "Apartment")?;
|
||||
let _ = RegCloseKey(inproc_key);
|
||||
let _ = RegCloseKey(clsid_key);
|
||||
|
||||
// Register ShellEx thumbnail handler for each extension
|
||||
for ext in SUPPORTED_EXTENSIONS {
|
||||
let ext_shellex_path =
|
||||
format!("{}\\ShellEx\\{{e357fccd-a995-4576-b01f-234630154e96}}", ext);
|
||||
if let Ok(ext_shellex_key) = create_reg_key(HKEY_CLASSES_ROOT, &ext_shellex_path) {
|
||||
let _ = set_reg_value(ext_shellex_key, "", &clsid);
|
||||
let _ = RegCloseKey(ext_shellex_key);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn unregister_server_impl() -> Result<(), HRESULT> {
|
||||
let clsid = clsid_string();
|
||||
let clsid_path = to_wide(&format!("CLSID\\{}", clsid));
|
||||
let _ = RegDeleteTreeW(HKEY_CLASSES_ROOT, PCWSTR(clsid_path.as_ptr()));
|
||||
|
||||
for ext in SUPPORTED_EXTENSIONS {
|
||||
let ext_path = to_wide(&format!(
|
||||
"{}\\ShellEx\\{{e357fccd-a995-4576-b01f-234630154e96}}",
|
||||
ext
|
||||
));
|
||||
let _ = RegDeleteTreeW(HKEY_CLASSES_ROOT, PCWSTR(ext_path.as_ptr()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
/// Cover image extraction for various eBook formats
|
||||
///
|
||||
/// Supports: EPUB, MOBI/AZW3/KF8, FB2, CBZ/CBR, TXT
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine as _;
|
||||
use directories_next::ProjectDirs;
|
||||
use image::{imageops, DynamicImage, Rgba};
|
||||
use md5::Context;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
use zip::ZipArchive;
|
||||
|
||||
/// Thumbnail cache directory (per-user)
|
||||
static CACHE_DIR: Lazy<Option<std::path::PathBuf>> = Lazy::new(|| {
|
||||
ProjectDirs::from("app", "Readest", "").map(|pd| {
|
||||
let dir = pd.cache_dir().join("thumbnails");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir
|
||||
})
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// EPUB extraction
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Extract cover image bytes from an EPUB file.
|
||||
pub fn extract_epub_cover_bytes<R: Read + Seek>(reader: R) -> Result<Vec<u8>> {
|
||||
let mut archive = ZipArchive::new(reader)?;
|
||||
|
||||
// Pass 1: Look for files with "cover" in the name
|
||||
let mut candidates: Vec<(usize, String, u64)> = Vec::new();
|
||||
for i in 0..archive.len() {
|
||||
let file = archive.by_index(i)?;
|
||||
let name = file.name().to_lowercase();
|
||||
let size = file.size();
|
||||
drop(file);
|
||||
|
||||
if is_image_extension(&name) && (name.contains("cover") || name.contains("front")) {
|
||||
candidates.push((i, name, size));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority: exact "cover" match first, then by size
|
||||
if !candidates.is_empty() {
|
||||
candidates.sort_by(|a, b| {
|
||||
let a_exact = a.1.contains("cover.") || a.1.ends_with("cover");
|
||||
let b_exact = b.1.contains("cover.") || b.1.ends_with("cover");
|
||||
match (a_exact, b_exact) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => b.2.cmp(&a.2),
|
||||
}
|
||||
});
|
||||
|
||||
let idx = candidates[0].0;
|
||||
let mut file = archive.by_index(idx)?;
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
return Ok(buf);
|
||||
}
|
||||
|
||||
// Pass 2: Parse container.xml to find OPF, then parse OPF for cover
|
||||
let container_xml = read_zip_file_to_string(&mut archive, "META-INF/container.xml");
|
||||
if let Ok(xml) = container_xml {
|
||||
if let Some(rootfile) = extract_attribute(&xml, "rootfile", "full-path") {
|
||||
let opf_content = read_zip_file_to_string(&mut archive, &rootfile);
|
||||
if let Ok(opf) = opf_content {
|
||||
if let Some(cover_id) = find_cover_id_in_opf(&opf) {
|
||||
if let Some(href) = find_href_by_id_in_opf(&opf, &cover_id) {
|
||||
let base = Path::new(&rootfile).parent().unwrap_or(Path::new(""));
|
||||
let cover_path = base.join(&href).to_string_lossy().replace('\\', "/");
|
||||
if let Ok(bytes) = read_zip_file_to_bytes(&mut archive, &cover_path) {
|
||||
return Ok(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(href) = find_first_image_in_manifest(&opf) {
|
||||
let base = Path::new(&rootfile).parent().unwrap_or(Path::new(""));
|
||||
let cover_path = base.join(&href).to_string_lossy().replace('\\', "/");
|
||||
if let Ok(bytes) = read_zip_file_to_bytes(&mut archive, &cover_path) {
|
||||
return Ok(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: Just grab the largest image file
|
||||
let mut largest: Option<(usize, u64)> = None;
|
||||
for i in 0..archive.len() {
|
||||
let file = archive.by_index(i)?;
|
||||
let name = file.name().to_lowercase();
|
||||
let size = file.size();
|
||||
drop(file);
|
||||
|
||||
if is_image_extension(&name) && (largest.is_none() || size > largest.unwrap().1) {
|
||||
largest = Some((i, size));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((idx, _)) = largest {
|
||||
let mut file = archive.by_index(idx)?;
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
return Ok(buf);
|
||||
}
|
||||
|
||||
Err(anyhow!("No cover image found in EPUB"))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MOBI/AZW3/KF8 extraction
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Extract cover image from MOBI/AZW3/KF8 files.
|
||||
pub fn extract_mobi_cover_bytes<R: Read + Seek>(mut reader: R) -> Result<Vec<u8>> {
|
||||
let mut header = [0u8; 78];
|
||||
reader.read_exact(&mut header)?;
|
||||
|
||||
if &header[60..68] != b"BOOKMOBI" {
|
||||
return Err(anyhow!("Not a valid MOBI file"));
|
||||
}
|
||||
|
||||
let num_records = u16::from_be_bytes([header[76], header[77]]) as usize;
|
||||
|
||||
let mut record_offsets: Vec<u32> = Vec::with_capacity(num_records);
|
||||
for _ in 0..num_records {
|
||||
let mut rec = [0u8; 8];
|
||||
reader.read_exact(&mut rec)?;
|
||||
record_offsets.push(u32::from_be_bytes([rec[0], rec[1], rec[2], rec[3]]));
|
||||
}
|
||||
|
||||
if record_offsets.is_empty() {
|
||||
return Err(anyhow!("No records in MOBI file"));
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Start(record_offsets[0] as u64))?;
|
||||
let mut mobi_header = [0u8; 256];
|
||||
reader.read_exact(&mut mobi_header)?;
|
||||
|
||||
if &mobi_header[16..20] != b"MOBI" {
|
||||
return Err(anyhow!("Invalid MOBI header"));
|
||||
}
|
||||
|
||||
let header_length = u32::from_be_bytes([
|
||||
mobi_header[20],
|
||||
mobi_header[21],
|
||||
mobi_header[22],
|
||||
mobi_header[23],
|
||||
]) as usize;
|
||||
|
||||
let exth_flags = u32::from_be_bytes([
|
||||
mobi_header[128],
|
||||
mobi_header[129],
|
||||
mobi_header[130],
|
||||
mobi_header[131],
|
||||
]);
|
||||
if exth_flags & 0x40 == 0 {
|
||||
return Err(anyhow!("No EXTH header in MOBI file"));
|
||||
}
|
||||
|
||||
let exth_offset = record_offsets[0] as u64 + 16 + header_length as u64;
|
||||
reader.seek(SeekFrom::Start(exth_offset))?;
|
||||
|
||||
let mut exth_magic = [0u8; 4];
|
||||
reader.read_exact(&mut exth_magic)?;
|
||||
if &exth_magic != b"EXTH" {
|
||||
return Err(anyhow!("EXTH header not found"));
|
||||
}
|
||||
|
||||
let mut exth_len_bytes = [0u8; 4];
|
||||
reader.read_exact(&mut exth_len_bytes)?;
|
||||
|
||||
let mut exth_count_bytes = [0u8; 4];
|
||||
reader.read_exact(&mut exth_count_bytes)?;
|
||||
let exth_count = u32::from_be_bytes(exth_count_bytes) as usize;
|
||||
|
||||
let mut cover_offset: Option<u32> = None;
|
||||
let first_img_idx = u32::from_be_bytes([
|
||||
mobi_header[108],
|
||||
mobi_header[109],
|
||||
mobi_header[110],
|
||||
mobi_header[111],
|
||||
]);
|
||||
|
||||
for _ in 0..exth_count {
|
||||
let mut rec_header = [0u8; 8];
|
||||
if reader.read_exact(&mut rec_header).is_err() {
|
||||
break;
|
||||
}
|
||||
let rec_type =
|
||||
u32::from_be_bytes([rec_header[0], rec_header[1], rec_header[2], rec_header[3]]);
|
||||
let rec_len =
|
||||
u32::from_be_bytes([rec_header[4], rec_header[5], rec_header[6], rec_header[7]])
|
||||
as usize;
|
||||
|
||||
let data_len = rec_len.saturating_sub(8);
|
||||
let mut data = vec![0u8; data_len];
|
||||
if reader.read_exact(&mut data).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
if rec_type == 201 && data_len >= 4 {
|
||||
cover_offset = Some(u32::from_be_bytes([data[0], data[1], data[2], data[3]]));
|
||||
}
|
||||
}
|
||||
|
||||
let cover_record_idx = if let Some(offset) = cover_offset {
|
||||
first_img_idx + offset
|
||||
} else {
|
||||
first_img_idx
|
||||
};
|
||||
|
||||
if cover_record_idx as usize >= record_offsets.len() {
|
||||
return Err(anyhow!("Cover record index out of bounds"));
|
||||
}
|
||||
|
||||
let start = record_offsets[cover_record_idx as usize] as u64;
|
||||
let end = if (cover_record_idx as usize + 1) < record_offsets.len() {
|
||||
record_offsets[cover_record_idx as usize + 1] as u64
|
||||
} else {
|
||||
reader.seek(SeekFrom::End(0))?;
|
||||
reader.stream_position()?
|
||||
};
|
||||
|
||||
let len = (end - start) as usize;
|
||||
reader.seek(SeekFrom::Start(start))?;
|
||||
let mut cover_data = vec![0u8; len];
|
||||
reader.read_exact(&mut cover_data)?;
|
||||
|
||||
if cover_data.starts_with(&[0xFF, 0xD8, 0xFF])
|
||||
|| cover_data.starts_with(&[0x89, 0x50, 0x4E, 0x47])
|
||||
|| cover_data.starts_with(b"GIF")
|
||||
{
|
||||
return Ok(cover_data);
|
||||
}
|
||||
|
||||
Err(anyhow!("No valid cover image found in MOBI"))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// CBZ extraction
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Extract cover image from CBZ (comic book ZIP) file.
|
||||
pub fn extract_cbz_cover_bytes<R: Read + Seek>(reader: R) -> Result<Vec<u8>> {
|
||||
let mut archive = ZipArchive::new(reader)?;
|
||||
|
||||
let mut images: Vec<(usize, String)> = Vec::new();
|
||||
for i in 0..archive.len() {
|
||||
let file = archive.by_index(i)?;
|
||||
let name = file.name().to_string();
|
||||
drop(file);
|
||||
|
||||
if is_image_extension(&name.to_lowercase()) {
|
||||
images.push((i, name));
|
||||
}
|
||||
}
|
||||
|
||||
images.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
|
||||
if let Some((idx, _)) = images.first() {
|
||||
let mut file = archive.by_index(*idx)?;
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
return Ok(buf);
|
||||
}
|
||||
|
||||
Err(anyhow!("No images found in CBZ"))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// FB2 extraction
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Extract cover image from FB2 (FictionBook) file.
|
||||
pub fn extract_fb2_cover_bytes<R: Read>(mut reader: R) -> Result<Vec<u8>> {
|
||||
let mut content = String::new();
|
||||
reader.read_to_string(&mut content)?;
|
||||
|
||||
let cover_id = if let Some(start) = content.find("<coverpage>") {
|
||||
let end = content[start..].find("</coverpage>").unwrap_or(500);
|
||||
let coverpage = &content[start..start + end];
|
||||
if let Some(href_pos) = coverpage.find("href=\"#") {
|
||||
let id_start = href_pos + 7;
|
||||
let id_end = coverpage[id_start..].find('"').unwrap_or(50);
|
||||
Some(coverpage[id_start..id_start + id_end].to_string())
|
||||
} else if let Some(href_pos) = coverpage.find("l:href=\"#") {
|
||||
let id_start = href_pos + 9;
|
||||
let id_end = coverpage[id_start..].find('"').unwrap_or(50);
|
||||
Some(coverpage[id_start..id_start + id_end].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let search_pattern = if let Some(ref id) = cover_id {
|
||||
format!("<binary id=\"{}\"", id)
|
||||
} else {
|
||||
"<binary".to_string()
|
||||
};
|
||||
|
||||
if let Some(pos) = content.find(&search_pattern) {
|
||||
if let Some(tag_end) = content[pos..].find('>') {
|
||||
let data_start = pos + tag_end + 1;
|
||||
if let Some(data_end) = content[data_start..].find("</binary>") {
|
||||
let b64_data = content[data_start..data_start + data_end].trim();
|
||||
let b64_clean: String = b64_data.chars().filter(|c| !c.is_whitespace()).collect();
|
||||
let bytes = general_purpose::STANDARD.decode(&b64_clean)?;
|
||||
return Ok(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cover_id.is_some() {
|
||||
if let Some(pos) = content.find("<binary") {
|
||||
if let Some(tag_end) = content[pos..].find('>') {
|
||||
let data_start = pos + tag_end + 1;
|
||||
if let Some(data_end) = content[data_start..].find("</binary>") {
|
||||
let b64_data = content[data_start..data_start + data_end].trim();
|
||||
let b64_clean: String =
|
||||
b64_data.chars().filter(|c| !c.is_whitespace()).collect();
|
||||
let bytes = general_purpose::STANDARD.decode(&b64_clean)?;
|
||||
return Ok(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No cover image found in FB2"))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TXT "cover" (placeholder)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Generate a placeholder thumbnail for TXT files.
|
||||
pub fn extract_txt_cover_bytes<R: Read>(mut reader: R, size: u32) -> Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let _n = reader.read(&mut buf)?;
|
||||
|
||||
let mut img = image::RgbaImage::from_pixel(size, size, Rgba([245, 245, 245, 255]));
|
||||
|
||||
for x in 0..size {
|
||||
img.put_pixel(x, 0, Rgba([200, 200, 200, 255]));
|
||||
img.put_pixel(x, size - 1, Rgba([200, 200, 200, 255]));
|
||||
}
|
||||
for y in 0..size {
|
||||
img.put_pixel(0, y, Rgba([200, 200, 200, 255]));
|
||||
img.put_pixel(size - 1, y, Rgba([200, 200, 200, 255]));
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
DynamicImage::ImageRgba8(img).write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Unified extraction by extension
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Extract cover image bytes based on file extension.
|
||||
pub fn extract_cover_bytes_by_ext(path: &Path, ext: &str) -> Result<Vec<u8>> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
match ext.to_lowercase().as_str() {
|
||||
"epub" => extract_epub_cover_bytes(file),
|
||||
"mobi" | "azw" | "azw3" | "kf8" | "prc" => extract_mobi_cover_bytes(file),
|
||||
"cbz" | "cbr" => extract_cbz_cover_bytes(file),
|
||||
"fb2" => extract_fb2_cover_bytes(file),
|
||||
"txt" => extract_txt_cover_bytes(file, 256),
|
||||
_ => Err(anyhow!("Unsupported format: {}", ext)),
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Thumbnail creation with overlay
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Create a thumbnail from cover image bytes with Readest icon overlay.
|
||||
pub fn create_thumbnail_with_overlay(cover_bytes: &[u8], requested_size: u32) -> Result<Vec<u8>> {
|
||||
let img = image::load_from_memory(cover_bytes)?;
|
||||
let thumbnail = img.thumbnail(requested_size, requested_size);
|
||||
|
||||
let overlay_img = load_overlay_icon();
|
||||
|
||||
let mut base = thumbnail.to_rgba8();
|
||||
let (base_w, base_h) = (base.width(), base.height());
|
||||
|
||||
if let Some(ov) = overlay_img {
|
||||
let overlay_size = (requested_size / 5).clamp(24, 48);
|
||||
let ov_resized = ov.resize(overlay_size, overlay_size, imageops::FilterType::Lanczos3);
|
||||
let ovb = ov_resized.to_rgba8();
|
||||
let (ov_w, ov_h) = (ovb.width(), ovb.height());
|
||||
|
||||
let x = base_w.saturating_sub(ov_w + 4);
|
||||
let y = base_h.saturating_sub(ov_h + 4);
|
||||
|
||||
for oy in 0..ov_h {
|
||||
for ox in 0..ov_w {
|
||||
let dst_x = x + ox;
|
||||
let dst_y = y + oy;
|
||||
|
||||
if dst_x < base_w && dst_y < base_h {
|
||||
let src_pixel = ovb.get_pixel(ox, oy);
|
||||
let alpha = src_pixel.0[3] as f32 / 255.0;
|
||||
|
||||
if alpha > 0.0 {
|
||||
let dst_pixel = base.get_pixel(dst_x, dst_y);
|
||||
let mut result = dst_pixel.0;
|
||||
|
||||
for c in 0..3 {
|
||||
let fg = src_pixel.0[c] as f32;
|
||||
let bg = result[c] as f32;
|
||||
result[c] = (fg * alpha + bg * (1.0 - alpha)) as u8;
|
||||
}
|
||||
result[3] = 255;
|
||||
|
||||
base.put_pixel(dst_x, dst_y, Rgba(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
DynamicImage::ImageRgba8(base).write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Load the Readest overlay icon.
|
||||
fn load_overlay_icon() -> Option<DynamicImage> {
|
||||
// Try embedded icon
|
||||
let icon_bytes = include_bytes!("../../../public/icon.png");
|
||||
if let Ok(img) = image::load_from_memory(icon_bytes) {
|
||||
return Some(img);
|
||||
}
|
||||
|
||||
// Fallback: try loading from filesystem
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
let candidates = [
|
||||
exe.parent().map(|p| p.join("icon.png")),
|
||||
exe.parent().map(|p| p.join("resources").join("icon.png")),
|
||||
exe.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.join("resources").join("icon.png")),
|
||||
];
|
||||
|
||||
for candidate in candidates.into_iter().flatten() {
|
||||
if candidate.exists() {
|
||||
if let Ok(bytes) = std::fs::read(&candidate) {
|
||||
if let Ok(img) = image::load_from_memory(&bytes) {
|
||||
return Some(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Caching
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Generate a thumbnail with disk caching.
|
||||
pub fn cached_thumbnail_for_path(path: &Path, ext: &str, size: u32) -> Result<Vec<u8>> {
|
||||
// Compute cache key by hashing file parts for stability without loading entire file
|
||||
let mut hasher = Context::new();
|
||||
hasher.consume(ext.as_bytes());
|
||||
hasher.consume(&size.to_le_bytes());
|
||||
|
||||
let file = std::fs::File::open(path)?;
|
||||
let metadata = file.metadata()?;
|
||||
let file_len = metadata.len();
|
||||
|
||||
// Read partial chunks like the TypeScript partialMD5 implementation
|
||||
const STEP: u64 = 1024;
|
||||
const SIZE: u64 = 1024;
|
||||
let mut file = file;
|
||||
|
||||
for i in -1i32..=10 {
|
||||
let pos = if i == -1 {
|
||||
256u64
|
||||
} else {
|
||||
STEP << (2 * i as u32)
|
||||
};
|
||||
let start = pos.min(file_len);
|
||||
let end = (start + SIZE).min(file_len);
|
||||
|
||||
if start >= file_len {
|
||||
break;
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(start))?;
|
||||
let mut buf = vec![0u8; (end - start) as usize];
|
||||
file.read_exact(&mut buf)?;
|
||||
hasher.consume(&buf);
|
||||
}
|
||||
|
||||
let digest = hasher.finalize();
|
||||
let key = format!("{:x}.png", digest);
|
||||
|
||||
if let Some(ref dir) = *CACHE_DIR {
|
||||
let cache_path = dir.join(&key);
|
||||
if cache_path.exists() {
|
||||
if let Ok(cached) = std::fs::read(&cache_path) {
|
||||
return Ok(cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cover = extract_cover_bytes_by_ext(path, ext)?;
|
||||
let thumbnail = create_thumbnail_with_overlay(&cover, size)?;
|
||||
|
||||
if let Some(ref dir) = *CACHE_DIR {
|
||||
let cache_path = dir.join(&key);
|
||||
let _ = std::fs::write(&cache_path, &thumbnail);
|
||||
}
|
||||
|
||||
Ok(thumbnail)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helper functions
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn is_image_extension(name: &str) -> bool {
|
||||
name.ends_with(".jpg")
|
||||
|| name.ends_with(".jpeg")
|
||||
|| name.ends_with(".png")
|
||||
|| name.ends_with(".gif")
|
||||
|| name.ends_with(".webp")
|
||||
|| name.ends_with(".bmp")
|
||||
}
|
||||
|
||||
fn read_zip_file_to_string<R: Read + Seek>(
|
||||
archive: &mut ZipArchive<R>,
|
||||
name: &str,
|
||||
) -> Result<String> {
|
||||
let mut file = archive.by_name(name)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn read_zip_file_to_bytes<R: Read + Seek>(
|
||||
archive: &mut ZipArchive<R>,
|
||||
name: &str,
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut file = archive.by_name(name)?;
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn extract_attribute(xml: &str, tag: &str, attr: &str) -> Option<String> {
|
||||
let pattern = format!("<{}", tag);
|
||||
if let Some(tag_pos) = xml.find(&pattern) {
|
||||
let tag_end = xml[tag_pos..].find('>').unwrap_or(500) + tag_pos;
|
||||
let tag_content = &xml[tag_pos..tag_end];
|
||||
|
||||
let attr_pattern = format!("{}=\"", attr);
|
||||
if let Some(attr_pos) = tag_content.find(&attr_pattern) {
|
||||
let value_start = attr_pos + attr_pattern.len();
|
||||
if let Some(value_end) = tag_content[value_start..].find('"') {
|
||||
return Some(tag_content[value_start..value_start + value_end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_cover_id_in_opf(opf: &str) -> Option<String> {
|
||||
if let Some(pos) = opf.find("name=\"cover\"") {
|
||||
let window_start = pos.saturating_sub(50);
|
||||
let window_end = (pos + 100).min(opf.len());
|
||||
let window = &opf[window_start..window_end];
|
||||
|
||||
if let Some(content_pos) = window.find("content=\"") {
|
||||
let start = content_pos + 9;
|
||||
if let Some(end) = window[start..].find('"') {
|
||||
return Some(window[start..start + end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pos) = opf.find("properties=\"cover-image\"") {
|
||||
let window_start = pos.saturating_sub(200);
|
||||
let window_end = pos;
|
||||
let window = &opf[window_start..window_end];
|
||||
|
||||
if let Some(id_pos) = window.rfind("id=\"") {
|
||||
let start = id_pos + 4;
|
||||
if let Some(end) = window[start..].find('"') {
|
||||
return Some(window[start..start + end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn find_href_by_id_in_opf(opf: &str, id: &str) -> Option<String> {
|
||||
let pattern = format!("id=\"{}\"", id);
|
||||
if let Some(pos) = opf.find(&pattern) {
|
||||
let window_start = pos.saturating_sub(10);
|
||||
let window_end = (pos + 200).min(opf.len());
|
||||
let window = &opf[window_start..window_end];
|
||||
|
||||
if let Some(href_pos) = window.find("href=\"") {
|
||||
let start = href_pos + 6;
|
||||
if let Some(end) = window[start..].find('"') {
|
||||
return Some(window[start..start + end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_first_image_in_manifest(opf: &str) -> Option<String> {
|
||||
let manifest_start = opf.find("<manifest")?;
|
||||
let manifest_end = opf[manifest_start..]
|
||||
.find("</manifest>")
|
||||
.map(|e| manifest_start + e)?;
|
||||
let manifest = &opf[manifest_start..manifest_end];
|
||||
|
||||
for media_type in ["image/jpeg", "image/png", "image/gif", "image/webp"] {
|
||||
let pattern = format!("media-type=\"{}\"", media_type);
|
||||
if let Some(pos) = manifest.find(&pattern) {
|
||||
let window_start = pos.saturating_sub(200);
|
||||
let window = &manifest[window_start..pos];
|
||||
|
||||
if let Some(href_pos) = window.rfind("href=\"") {
|
||||
let start = href_pos + 6;
|
||||
if let Some(end) = window[start..].find('"') {
|
||||
return Some(window[start..start + end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Windows Thumbnail Provider for Readest
|
||||
//!
|
||||
//! This module provides Windows Explorer thumbnail support for eBook files.
|
||||
//! Thumbnails are only shown when Readest is set as the default application.
|
||||
//!
|
||||
//! Supported formats: EPUB, MOBI, AZW, AZW3, KF8, FB2, CBZ, CBR
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod com_provider;
|
||||
mod extraction;
|
||||
|
||||
pub use extraction::*;
|
||||
@@ -0,0 +1,63 @@
|
||||
const options = {
|
||||
debug: false,
|
||||
sort: false,
|
||||
func: {
|
||||
list: ['_'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
lngs: [
|
||||
'de',
|
||||
'ja',
|
||||
'es',
|
||||
'fa',
|
||||
'fr',
|
||||
'it',
|
||||
'el',
|
||||
'ko',
|
||||
'uk',
|
||||
'nl',
|
||||
'sl',
|
||||
'sv',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'tr',
|
||||
'hi',
|
||||
'id',
|
||||
'vi',
|
||||
'ms',
|
||||
'he',
|
||||
'ar',
|
||||
'th',
|
||||
'bo',
|
||||
'bn',
|
||||
'ta',
|
||||
'si',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
],
|
||||
ns: ['translation'],
|
||||
defaultNs: 'translation',
|
||||
defaultValue: '__STRING_NOT_TRANSLATED__',
|
||||
resource: {
|
||||
loadPath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
savePath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
jsonIndent: 2,
|
||||
lineEnding: '\n',
|
||||
},
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
interpolation: {
|
||||
prefix: '{{',
|
||||
suffix: '}}',
|
||||
},
|
||||
metadata: {},
|
||||
allowDynamicKeys: true,
|
||||
removeUnusedKeys: true,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
input: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.test.{js,jsx,ts,tsx}'],
|
||||
output: '.',
|
||||
options,
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
module.exports = {
|
||||
input: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.test.{js,jsx,ts,tsx}'],
|
||||
output: '.',
|
||||
options: {
|
||||
debug: false,
|
||||
sort: false,
|
||||
func: {
|
||||
list: ['_'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
lngs: [
|
||||
'de',
|
||||
'ja',
|
||||
'es',
|
||||
'fr',
|
||||
'it',
|
||||
'el',
|
||||
'ko',
|
||||
'uk',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'tr',
|
||||
'hi',
|
||||
'id',
|
||||
'vi',
|
||||
'ar',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
],
|
||||
ns: ['translation'],
|
||||
defaultNs: 'translation',
|
||||
defaultValue: '__STRING_NOT_TRANSLATED__',
|
||||
resource: {
|
||||
loadPath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
savePath: './public/locales/{{lng}}/{{ns}}.json',
|
||||
jsonIndent: 2,
|
||||
lineEnding: '\n',
|
||||
},
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
interpolation: {
|
||||
prefix: '{{',
|
||||
suffix: '}}',
|
||||
},
|
||||
metadata: {},
|
||||
allowDynamicKeys: true,
|
||||
removeUnusedKeys: true,
|
||||
},
|
||||
};
|
||||
@@ -1,18 +1,22 @@
|
||||
import withPWAInit from '@ducanh2912/next-pwa';
|
||||
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
import withSerwistInit from '@serwist/next';
|
||||
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,47 @@ const nextConfig = {
|
||||
// Configure assetPrefix or else the server won't properly resolve your assets.
|
||||
assetPrefix: '',
|
||||
reactStrictMode: true,
|
||||
serverExternalPackages: ['isows'],
|
||||
webpack: (config) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
nunjucks: 'nunjucks/browser/nunjucks.js',
|
||||
};
|
||||
return config;
|
||||
},
|
||||
turbopack: {
|
||||
resolveAlias: {
|
||||
nunjucks: 'nunjucks/browser/nunjucks.js',
|
||||
},
|
||||
},
|
||||
transpilePackages: [
|
||||
'ai',
|
||||
'ai-sdk-ollama',
|
||||
'@ai-sdk/react',
|
||||
'@assistant-ui/react',
|
||||
'@assistant-ui/react-ai-sdk',
|
||||
'@assistant-ui/react-markdown',
|
||||
'streamdown',
|
||||
...(isDev
|
||||
? []
|
||||
: [
|
||||
'i18next-browser-languagedetector',
|
||||
'react-i18next',
|
||||
'i18next',
|
||||
'@tauri-apps',
|
||||
'highlight.js',
|
||||
'foliate-js',
|
||||
'marked',
|
||||
]),
|
||||
],
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/reader/:ids',
|
||||
destination: '/reader?ids=:ids',
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -33,23 +78,37 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/_next/static/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: isDev
|
||||
? 'public, max-age=0, must-revalidate'
|
||||
: 'public, max-age=31536000, immutable',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const withPWA = withPWAInit({
|
||||
dest: 'public',
|
||||
disable: isDev || appPlatform !== 'web',
|
||||
cacheOnFrontEndNav: true,
|
||||
aggressiveFrontEndNavCaching: true,
|
||||
reloadOnOnline: true,
|
||||
swcMinify: true,
|
||||
fallbacks: {
|
||||
document: '/offline',
|
||||
},
|
||||
workboxOptions: {
|
||||
disableDevLogs: true,
|
||||
},
|
||||
const pwaDisabled = isDev || appPlatform !== 'web';
|
||||
|
||||
const withPWA = pwaDisabled
|
||||
? (config) => config
|
||||
: withSerwistInit({
|
||||
swSrc: 'src/sw.ts',
|
||||
swDest: 'public/sw.js',
|
||||
cacheOnNavigation: true,
|
||||
reloadOnOnline: true,
|
||||
disable: false,
|
||||
register: true,
|
||||
scope: '/',
|
||||
});
|
||||
|
||||
const withAnalyzer = withBundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
export default withPWA(nextConfig);
|
||||
export default withPWA(withAnalyzer(nextConfig));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.58",
|
||||
"version": "0.9.101",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
"build": "dotenv -e .env.tauri -- next build",
|
||||
@@ -9,17 +10,38 @@
|
||||
"dev-web": "dotenv -e .env.web -- next dev",
|
||||
"build-web": "dotenv -e .env.web -- next build",
|
||||
"start-web": "dotenv -e .env.web -- next start",
|
||||
"i18n:extract": "i18next-scanner",
|
||||
"lint": "next lint",
|
||||
"dev-web:vinext": "dotenv -e .env.web -- vinext dev",
|
||||
"build-web:vinext": "dotenv -e .env.web -- vinext build",
|
||||
"start-web:vinext": "dotenv -e .env.web -- vinext start",
|
||||
"build-tauri": "dotenv -e .env.tauri -- next build",
|
||||
"i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs",
|
||||
"lint": "tsgo --noEmit && eslint --cache .",
|
||||
"test": "dotenv -e .env -e .env.test.local vitest",
|
||||
"test:browser": "vitest --config vitest.browser.config.mts --watch=false",
|
||||
"test:tauri": "bash scripts/test-tauri.sh",
|
||||
"test:pr:web": "pnpm test -- --watch=false && pnpm test:browser",
|
||||
"test:pr:tauri": "bash scripts/test-tauri.sh",
|
||||
"test:all": "pnpm test -- --watch=false && pnpm test:browser && bash scripts/test-tauri.sh",
|
||||
"test:e2e": "wdio run wdio.conf.ts",
|
||||
"tauri:dev:test": "dotenv -e .env.tauri -- tauri dev --features webdriver",
|
||||
"tauri:build:test": "dotenv -e .env.tauri -- tauri build --debug --features webdriver",
|
||||
"tauri": "tauri",
|
||||
"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",
|
||||
"fmt:check": "cargo fmt -p Readest --check",
|
||||
"clippy:check": "cargo clippy -p Readest --no-deps -- -D warnings",
|
||||
"format": "pnpm -w format",
|
||||
"format:check": "pnpm -w format:check",
|
||||
"prepare-public-vendor": "mkdirp ./public/vendor/pdfjs ./public/vendor/simplecc",
|
||||
"copy-pdfjs-js": "cpx \"../../packages/foliate-js/node_modules/pdfjs-dist/legacy/build/{pdf.worker.min.mjs,pdf.min.mjs,pdf.d.mts}\" ./public/vendor/pdfjs",
|
||||
"copy-pdfjs-wasm": "cpx \"../../packages/foliate-js/node_modules/pdfjs-dist/wasm/{openjpeg.wasm,qcms_bg.wasm}\" ./public/vendor/pdfjs",
|
||||
"copy-pdfjs-fonts": "cpx \"../../packages/foliate-js/node_modules/pdfjs-dist/{cmaps,standard_fonts}/*\" ./public/vendor/pdfjs",
|
||||
"copy-flatten-pdfjs-annotation-layer-css": "npx postcss \"../../packages/foliate-js/vendor/pdfjs/annotation_layer_builder.css\" --no-map -u postcss-nested > ./public/vendor/pdfjs/annotation_layer_builder.css",
|
||||
"copy-flatten-pdfjs-text-layer-css": "npx postcss \"../../packages/foliate-js/vendor/pdfjs/text_layer_builder.css\" --no-map -u postcss-nested > ./public/vendor/pdfjs/text_layer_builder.css",
|
||||
"copy-flatten-pdfjs-css": "pnpm copy-flatten-pdfjs-annotation-layer-css && pnpm copy-flatten-pdfjs-text-layer-css",
|
||||
"copy-pdfjs": "pnpm copy-pdfjs-js && pnpm copy-pdfjs-fonts && pnpm copy-flatten-pdfjs-css",
|
||||
"copy-pdfjs": "pnpm copy-pdfjs-js && pnpm copy-pdfjs-wasm && pnpm copy-pdfjs-fonts && pnpm copy-flatten-pdfjs-css",
|
||||
"copy-simplecc": "cpx \"../../packages/simplecc-wasm/dist/web/*\" ./public/vendor/simplecc",
|
||||
"setup-pdfjs": "pnpm prepare-public-vendor && pnpm copy-pdfjs",
|
||||
"setup-simplecc": "pnpm prepare-public-vendor && pnpm copy-simplecc",
|
||||
"setup-vendors": "pnpm setup-pdfjs && pnpm setup-simplecc",
|
||||
"build-win-x64": "dotenv -e .env.tauri.local -- tauri build --target i686-pc-windows-msvc --bundles nsis",
|
||||
"build-win-arm64": "dotenv -e .env.tauri.local -- tauri build --target aarch64-pc-windows-msvc --bundles nsis",
|
||||
"build-linux-x64": "dotenv -e .env.tauri.local -- tauri build --target x86_64-unknown-linux-gnu --bundles appimage",
|
||||
@@ -30,88 +52,185 @@
|
||||
"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",
|
||||
"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"
|
||||
"preview": "pnpm patch-build-webpack && NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && pnpm restore-build-original && opennextjs-cloudflare preview --ip 0.0.0.0 --port 3001",
|
||||
"deploy": "pnpm patch-build-webpack && NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && pnpm restore-build-original && opennextjs-cloudflare deploy",
|
||||
"upload": "pnpm patch-build-webpack && NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && pnpm restore-build-original && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
"patch-build-webpack": "if [ \"$(uname)\" = \"Darwin\" ]; then sed -i '' 's/next build\"/next build --webpack\"/' package.json; else sed -i 's/next build\"/next build --webpack\"/' package.json; fi",
|
||||
"restore-build-original": "if [ \"$(uname)\" = \"Darwin\" ]; then sed -i '' 's/next build --webpack\"/next build\"/' package.json; else sed -i 's/next build --webpack\"/next build\"/' package.json; fi",
|
||||
"update-metadata": "bash ./scripts/sync-release-notes.sh release-notes.json ../../data/metainfo/appdata.xml",
|
||||
"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:translations": "count=$(grep -rno '__STRING_NOT_TRANSLATED__' public/locales/* | wc -l); if [ \"$count\" -gt 0 ]; then echo '❌ Untranslated strings found!'; exit 1; else echo '✅ All strings translated.'; fi",
|
||||
"check:lookbehind-regex": "count=$(grep -rnoE '\\(\\?<[!=]' .next/static/chunks/* out/_next/static/chunks/* | wc -l); if [ \"$count\" -gt 0 ]; then echo '❌ Lookbehind regex found in output!'; exit 1; else echo '✅ No lookbehind regex found.'; fi",
|
||||
"check:all": "pnpm check:translations && pnpm check:lookbehind-regex",
|
||||
"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",
|
||||
"@ai-sdk/react": "^3.0.49",
|
||||
"@assistant-ui/react": "0.11.56",
|
||||
"@assistant-ui/react-ai-sdk": "1.1.21",
|
||||
"@assistant-ui/react-markdown": "0.11.9",
|
||||
"@aws-sdk/client-s3": "^3.1000.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1000.0",
|
||||
"@choochmeque/tauri-plugin-sharekit-api": "^0.3.0",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@fabianlars/tauri-plugin-oauth": "2",
|
||||
"@opennextjs/cloudflare": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.1",
|
||||
"@opennextjs/cloudflare": "^1.17.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@serwist/next": "^9.5.6",
|
||||
"@serwist/webpack-plugin": "^9.5.6",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.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",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"@supabase/supabase-js": "^2.76.1",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-cli": "^2.4.1",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.7",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
"@tauri-apps/plugin-haptics": "^2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.7",
|
||||
"@tauri-apps/plugin-log": "^2.8.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "~2.3.5",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@tauri-apps/plugin-websocket": "~2.4.2",
|
||||
"@tursodatabase/database-common": "^0.5.0",
|
||||
"@tursodatabase/database-wasm": "^0.5.0",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"@zip.js/zip.js": "^2.8.16",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"ai": "^6.0.47",
|
||||
"ai-sdk-ollama": "^3.2.0",
|
||||
"app-store-server-api": "^0.17.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"buffer": "^6.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"cssbeautify": "^0.3.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.0",
|
||||
"foliate-js": "workspace:*",
|
||||
"franc-min": "^6.2.0",
|
||||
"fzf": "^0.5.2",
|
||||
"google-auth-library": "^10.5.0",
|
||||
"googleapis": "^164.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"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",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"js-md5": "^0.8.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^15.0.12",
|
||||
"next": "15.3.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "16.1.7",
|
||||
"next-view-transitions": "^0.3.5",
|
||||
"nunjucks": "^3.2.4",
|
||||
"overlayscrollbars": "^2.11.4",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"posthog-js": "^1.246.0",
|
||||
"react": "19.0.0",
|
||||
"react": "19.2.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dom": "19.2.4",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-virtuoso": "^4.17.0",
|
||||
"react-window": "^1.8.11",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"semver": "^7.7.1",
|
||||
"streamdown": "^1.6.10",
|
||||
"stripe": "^18.2.1",
|
||||
"styled-jsx": "^5.1.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tauri-plugin-device-info-api": "^1.0.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^4.0.8",
|
||||
"zustand": "5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^15.4.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tauri-apps/cli": "2.5.0",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@tursodatabase/database": "^0.5.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/lunr": "^2.3.7",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/nunjucks": "^3.2.6",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260312.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitejs/plugin-rsc": "^0.5.21",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/browser-webdriverio": "^4.0.18",
|
||||
"@wdio/cli": "^9.25.0",
|
||||
"@wdio/globals": "^9.23.0",
|
||||
"@wdio/local-runner": "^9.24.0",
|
||||
"@wdio/mocha-framework": "^9.24.0",
|
||||
"@wdio/spec-reporter": "^9.24.0",
|
||||
"@wdio/types": "^9.24.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"cpx2": "^8.0.0",
|
||||
"daisyui": "^4.12.24",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"i18next-scanner": "^4.6.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"node-env-run": "^4.0.2",
|
||||
"playwright": "^1.58.2",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"react-server-dom-webpack": "^19.2.4",
|
||||
"serwist": "^9.3.0",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"typescript": "^5.7.2",
|
||||
"wrangler": "^4.19.1"
|
||||
"vinext": "^0.0.21",
|
||||
"vite": "^7.3.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.18",
|
||||
"wrangler": "^4.73.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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ed533042-5626-4704-b5f2-fa3bbd1136ed
|
||||
@@ -1,2 +0,0 @@
|
||||
/_next/static/*
|
||||
Cache-Control: public,max-age=31536000,immutable
|
||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 318 KiB |
@@ -1,8 +1,27 @@
|
||||
{
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
"KingHwa_OldSong": "KingHwa Song",
|
||||
"Are you sure to delete {{count}} selected book(s)?_one": "Are you sure to delete {{count}} selected book?",
|
||||
"Are you sure to delete {{count}} selected book(s)?_other": "Are you sure to delete {{count}} selected books?",
|
||||
"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": "<0>{{count}}</0><1> page left in chapter</1>",
|
||||
"{{count}} pages left in chapter_other": "<0>{{count}}</0><1> pages left in chapter</1>",
|
||||
"Deleted {{count}} file(s)_one": "Deleted {{count}} file",
|
||||
"Deleted {{count}} file(s)_other": "Deleted {{count}} files",
|
||||
"Failed to delete {{count}} file(s)_one": "Failed to delete {{count}} file",
|
||||
"Failed to delete {{count}} file(s)_other": "Failed to delete {{count}} files",
|
||||
"{{count}} selected_one": "{{count}} selected",
|
||||
"{{count}} selected_other": "{{count}} selected",
|
||||
"Are you sure to delete {{count}} selected file(s)?_one": "Are you sure to delete {{count}} selected file?",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "Are you sure to delete {{count}} selected files?",
|
||||
"Successfully imported {{count}} book(s)_one": "Successfully imported {{count}} book",
|
||||
"Successfully imported {{count}} book(s)_other": "Successfully imported {{count}} books",
|
||||
"Set status for {{count}} book(s)_one": "Set status for {{count}} book",
|
||||
"Set status for {{count}} book(s)_other": "Set status for {{count}} books",
|
||||
"{{count}} book(s) synced_one": "{{count}} book synced",
|
||||
"{{count}} book(s) synced_other": "{{count}} books synced"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"Auto Mode": "Mode Otomatis",
|
||||
"Behavior": "Perilaku",
|
||||
"Book": "Buku",
|
||||
"Book Cover": "Sampul Buku",
|
||||
"Bookmark": "Penanda",
|
||||
"Cancel": "Batal",
|
||||
"Chapter": "Bab",
|
||||
@@ -18,7 +17,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 +26,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 +45,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 +66,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",
|
||||
@@ -85,7 +81,6 @@
|
||||
"Search...": "Cari...",
|
||||
"Select Book": "Pilih Buku",
|
||||
"Select Books": "Pilih buku",
|
||||
"Select Multiple Books": "Pilih beberapa buku",
|
||||
"Sepia": "Sepia",
|
||||
"Serif Font": "Font Serif",
|
||||
"Show Book Details": "Tampilkan Detail Buku",
|
||||
@@ -96,23 +91,21 @@
|
||||
"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",
|
||||
"Theme Mode": "Mode Tema",
|
||||
"Translate": "Terjemahkan",
|
||||
"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.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Mode Menulis",
|
||||
"Your Library": "Perpustakaan Anda",
|
||||
"TTS not supported for PDF": "TTS tidak didukung untuk PDF",
|
||||
"Override Book Font": "Ganti Font Buku",
|
||||
"Apply to All Books": "Terapkan ke semua buku",
|
||||
"Apply to This Book": "Terapkan ke buku ini",
|
||||
@@ -122,14 +115,13 @@
|
||||
"Book Details": "Detail Buku",
|
||||
"From Local File": "Dari file lokal",
|
||||
"TOC": "Daftar Isi",
|
||||
"Table of Contents": "Daftar Isi",
|
||||
"Book uploaded: {{title}}": "Buku diunggah: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Gagal mengunggah buku: {{title}}",
|
||||
"Book downloaded: {{title}}": "Buku diunduh: {{title}}",
|
||||
"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 +153,7 @@
|
||||
"Create a Password": "Buat kata sandi",
|
||||
"Sign up": "Daftar",
|
||||
"Signing up...": "Sedang mendaftar...",
|
||||
"Don’t 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",
|
||||
@@ -180,40 +172,15 @@
|
||||
"Token": "Token",
|
||||
"Your OTP token": "Token OTP Anda",
|
||||
"Verify token": "Verifikasi token",
|
||||
"Sign in with Google": "Masuk dengan Google",
|
||||
"Sign in with Apple": "Masuk dengan Apple",
|
||||
"Sign in with GitHub": "Masuk dengan GitHub",
|
||||
"Account": "Akun",
|
||||
"Failed to delete user. Please try again later.": "Gagal menghapus pengguna. Silakan coba lagi nanti.",
|
||||
"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",
|
||||
@@ -222,7 +189,7 @@
|
||||
"No supported files found. Supported formats: {{formats}}": "Tidak ada file yang didukung ditemukan. Format yang didukung: {{formats}}",
|
||||
"Drop to Import Books": "Jatuhkan untuk Mengimpor Buku",
|
||||
"Custom": "Kustom",
|
||||
"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": "Seluruh dunia adalah panggung,\nDan semua pria dan wanita hanyalah pemain;\nMereka memiliki keluar dan masuk mereka,\nDan seorang pria dalam hidupnya memainkan banyak peran,\nTindakannya ada tujuh zaman.\n\n— William Shakespeare",
|
||||
"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": "Seluruh dunia adalah panggung,\nDan semua pria dan wanita hanyalah pemain;\nMereka memiliki keluar dan masuk mereka,\nDan seorang pria dalam hidupnya memainkan banyak peran,\nTindakannya ada tujuh zaman.\n\n— William Shakespeare",
|
||||
"Custom Theme": "Tema Kustom",
|
||||
"Theme Name": "Nama Tema",
|
||||
"Text Color": "Warna Teks",
|
||||
@@ -249,9 +216,9 @@
|
||||
"Auto Import on File Open": "Impor Otomatis saat File Dibuka",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Tidak ada bab yang terdeteksi.",
|
||||
"Failed to parse the EPUB file.": "Gagal mengurai file EPUB.",
|
||||
"This book format is not supported.": "Format buku ini tidak didukung.",
|
||||
"No chapters detected": "Tidak ada bab yang terdeteksi",
|
||||
"Failed to parse the EPUB file": "Gagal mengurai file EPUB",
|
||||
"This book format is not supported": "Format buku ini tidak didukung",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Tidak dapat mengambil terjemahan. Silakan masuk terlebih dahulu dan coba lagi.",
|
||||
"Group": "Grup",
|
||||
"Always on Top": "Selalu di Atas",
|
||||
@@ -264,13 +231,13 @@
|
||||
"Clear Search": "Hapus Pencarian",
|
||||
"Header & Footer": "Header & Footer",
|
||||
"Apply also in Scrolled Mode": "Terapkan juga di Mode Gulir",
|
||||
"A new version of Readest is Available!": "Versi baru Readest Tersedia!",
|
||||
"A new version of Readest is available!": "Versi baru Readest Tersedia!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} tersedia (versi terinstal {{currentVersion}}).",
|
||||
"Download and install now?": "Unduh dan instal sekarang?",
|
||||
"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,18 +246,15 @@
|
||||
"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",
|
||||
"(from 'As You Like It', Act II)": "(dari 'Seperti yang Anda Inginkan', Bab II)",
|
||||
"Link Color": "Warna Tautan",
|
||||
"Volume Keys for Page Flip": "Volume Keys untuk Pembalikan Halaman",
|
||||
"Clicks for Page Flip": "Klik untuk Pembalikan Halaman",
|
||||
"Swap Clicks Area": "Area Klik Tukar",
|
||||
"Screen": "Screen",
|
||||
"Orientation": "Orientasi",
|
||||
"Portrait": "Potret",
|
||||
@@ -299,7 +263,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",
|
||||
@@ -325,7 +289,6 @@
|
||||
"Disable Translation": "Nonaktifkan Terjemahan",
|
||||
"Scroll": "Scroll",
|
||||
"Overlap Pixels": "Overlap Piksel",
|
||||
"Click": "Klik",
|
||||
"System Language": "Bahasa Sistem",
|
||||
"Security": "Keamanan",
|
||||
"Allow JavaScript": "Izinkan JavaScript",
|
||||
@@ -338,14 +301,12 @@
|
||||
"Synced at {{time}}": "Disinkronkan pada {{time}}",
|
||||
"Never synced": "Belum pernah disinkronkan",
|
||||
"Show Remaining Time": "Perlihatkan Waktu Tersisa",
|
||||
"Show Page Number": "Perlihatkan Nomor Halaman",
|
||||
"{{time}} min left in chapter": "{{time}} menit tersisa di bab",
|
||||
"Override Book Color": "Ubah Warna Buku",
|
||||
"Login Required": "Perlu masuk",
|
||||
"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 +321,773 @@
|
||||
"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",
|
||||
"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": "<0>{{count}}</0><1> halaman tersisa di bab ini</1>",
|
||||
"Show Remaining Pages": "Tampilkan halaman tersisa",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
"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:",
|
||||
"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}}",
|
||||
"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",
|
||||
"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",
|
||||
"TTS not supported for this document": "TTS tidak didukung untuk dokumen ini",
|
||||
"Reset Password": "Reset Kata Sandi",
|
||||
"Show Reading Progress": "Perlihatkan Progres Membaca",
|
||||
"Reading Progress Style": "Gaya Progres Membaca",
|
||||
"Page Number": "Nomor Halaman",
|
||||
"Percentage": "Persentase",
|
||||
"Deleted local copy of the book: {{title}}": "Salinan lokal buku dihapus: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "Gagal menghapus cadangan cloud buku: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "Gagal menghapus salinan lokal buku: {{title}}",
|
||||
"Are you sure to delete the local copy of the selected book?": "Apakah Anda yakin ingin menghapus salinan lokal buku yang dipilih?",
|
||||
"Remove from Cloud & Device": "Hapus dari Cloud & Perangkat",
|
||||
"Remove from Cloud Only": "Hapus dari Cloud Saja",
|
||||
"Remove from Device Only": "Hapus dari Perangkat Saja",
|
||||
"Disconnected": "Terputus",
|
||||
"KOReader Sync Settings": "Pengaturan Sinkronisasi KOReader",
|
||||
"Sync Strategy": "Strategi Sinkronisasi",
|
||||
"Ask on conflict": "Tanya saat terjadi konflik",
|
||||
"Always use latest": "Selalu gunakan yang terbaru",
|
||||
"Send changes only": "Kirim perubahan saja",
|
||||
"Receive changes only": "Terima perubahan saja",
|
||||
"Checksum Method": "Metode Checksum",
|
||||
"File Content (recommended)": "Konten File (disarankan)",
|
||||
"File Name": "Nama File",
|
||||
"Device Name": "Nama Perangkat",
|
||||
"Connect to your KOReader Sync server.": "Hubungkan ke server Sinkronisasi KOReader Anda.",
|
||||
"Server URL": "URL Server",
|
||||
"Username": "Nama Pengguna",
|
||||
"Your Username": "Nama Pengguna Anda",
|
||||
"Password": "Kata Sandi",
|
||||
"Connect": "Hubungkan",
|
||||
"KOReader Sync": "Sinkronisasi KOReader",
|
||||
"Sync Conflict": "Konflik Sinkronisasi",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "Sinkronkan progres membaca dari \"{{deviceName}}\"?",
|
||||
"another device": "perangkat lain",
|
||||
"Local Progress": "Progres Lokal",
|
||||
"Remote Progress": "Progres Jarak Jauh",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "Halaman {{page}} dari {{total}} ({{percentage}}%)",
|
||||
"Current position": "Posisi Saat Ini",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "Sekitar halaman {{page}} dari {{total}} ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "Sekitar {{percentage}}%",
|
||||
"Failed to connect": "Gagal terhubung",
|
||||
"Sync Server Connected": "Server sinkronisasi terhubung",
|
||||
"Sync as {{userDisplayName}}": "Sinkronkan sebagai {{userDisplayName}}",
|
||||
"Custom Fonts": "Font Kustom",
|
||||
"Cancel Delete": "Batal Hapus",
|
||||
"Import Font": "Impor Font",
|
||||
"Delete Font": "Hapus Font",
|
||||
"Tips": "Tips",
|
||||
"Custom fonts can be selected from the Font Face menu": "Font kustom dapat dipilih dari menu Font Face",
|
||||
"Manage Custom Fonts": "Kelola Font Kustom",
|
||||
"Select Files": "Pilih File",
|
||||
"Select Image": "Pilih Gambar",
|
||||
"Select Video": "Pilih Video",
|
||||
"Select Audio": "Pilih Audio",
|
||||
"Select Fonts": "Pilih Font",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Format font yang didukung: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Dorong Progres",
|
||||
"Pull Progress": "Tarik Progres",
|
||||
"Previous Paragraph": "Paragraf Sebelumnya",
|
||||
"Previous Sentence": "Kalimat Sebelumnya",
|
||||
"Pause": "Jeda",
|
||||
"Play": "Putar",
|
||||
"Next Sentence": "Kalimat Berikutnya",
|
||||
"Next Paragraph": "Paragraf Berikutnya",
|
||||
"Separate Cover Page": "Halaman Sampul Terpisah",
|
||||
"Resize Notebook": "Ubah Ukuran Buku Catatan",
|
||||
"Resize Sidebar": "Ubah Ukuran Bilah Samping",
|
||||
"Get Help from the Readest Community": "Dapatkan Bantuan dari Komunitas Readest",
|
||||
"Remove cover image": "Hapus gambar sampul",
|
||||
"Bookshelf": "Rak Buku",
|
||||
"View Menu": "Lihat Menu",
|
||||
"Settings Menu": "Menu Pengaturan",
|
||||
"View account details and quota": "Lihat detail akun dan kuota",
|
||||
"Library Header": "Header Perpustakaan",
|
||||
"Book Content": "Isi Buku",
|
||||
"Footer Bar": "Bilah Footer",
|
||||
"Header Bar": "Bilah Header",
|
||||
"View Options": "Opsi Tampilan",
|
||||
"Book Menu": "Menu Buku",
|
||||
"Search Options": "Opsi Pencarian",
|
||||
"Close": "Tutup",
|
||||
"Delete Book Options": "Opsi Hapus Buku",
|
||||
"ON": "AKTIF",
|
||||
"OFF": "TIDAK AKTIF",
|
||||
"Reading Progress": "Progres Membaca",
|
||||
"Page Margin": "Margin Halaman",
|
||||
"Remove Bookmark": "Hapus Penanda",
|
||||
"Add Bookmark": "Tambahkan Penanda",
|
||||
"Books Content": "Isi Buku",
|
||||
"Jump to Location": "Lompat ke Lokasi",
|
||||
"Unpin Notebook": "Batal Pin Notebook",
|
||||
"Pin Notebook": "Pin Notebook",
|
||||
"Hide Search Bar": "Sembunyikan Bilah Pencarian",
|
||||
"Show Search Bar": "Tampilkan Bilah Pencarian",
|
||||
"On {{current}} of {{total}} page": "Pada {{current}} dari {{total}} halaman",
|
||||
"Section Title": "Judul Bagian",
|
||||
"Decrease": "Kecilkan",
|
||||
"Increase": "Besarakan",
|
||||
"Settings Panels": "Panel Pengaturan",
|
||||
"Settings": "Pengaturan",
|
||||
"Unpin Sidebar": "Batal Pin Bilah Samping",
|
||||
"Pin Sidebar": "Pin Bilah Samping",
|
||||
"Toggle Sidebar": "Toggel Bilah Samping",
|
||||
"Toggle Translation": "Toggel Terjemahan",
|
||||
"Translation Disabled": "Terjemahan Dinonaktifkan",
|
||||
"Minimize": "Minimalkan",
|
||||
"Maximize or Restore": "Maksimalkan atau Pulihkan",
|
||||
"Exit Parallel Read": "Keluar dari Baca Paralel",
|
||||
"Enter Parallel Read": "Masuk ke Baca Paralel",
|
||||
"Zoom Level": "Level Zoom",
|
||||
"Zoom Out": "Perbesar Zoom",
|
||||
"Reset Zoom": "Reset Zoom",
|
||||
"Zoom In": "Perkecil Zoom",
|
||||
"Zoom Mode": "Mode Zoom",
|
||||
"Single Page": "Halaman Tunggal",
|
||||
"Auto Spread": "Penyebaran Otomatis",
|
||||
"Fit Page": "Sesuaikan Halaman",
|
||||
"Fit Width": "Sesuaikan Lebar",
|
||||
"Failed to select directory": "Gagal memilih direktori",
|
||||
"The new data directory must be different from the current one.": "Direktori data baru harus berbeda dari yang saat ini.",
|
||||
"Migration failed: {{error}}": "Migrasi gagal: {{error}}",
|
||||
"Change Data Location": "Ubah Lokasi Data",
|
||||
"Current Data Location": "Lokasi Data Saat Ini",
|
||||
"Total size: {{size}}": "Ukuran total: {{size}}",
|
||||
"Calculating file info...": "Menghitung informasi file...",
|
||||
"New Data Location": "Lokasi Data Baru",
|
||||
"Choose Different Folder": "Pilih Folder Berbeda",
|
||||
"Choose New Folder": "Pilih Folder Baru",
|
||||
"Migrating data...": "Migrasi data...",
|
||||
"Copying: {{file}}": "Menyalin: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} dari {{total}} file",
|
||||
"Migration completed successfully!": "Migrasi selesai dengan sukses!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Data Anda telah dipindahkan ke lokasi baru. Silakan restart aplikasi untuk menyelesaikan proses.",
|
||||
"Migration failed": "Migrasi gagal",
|
||||
"Important Notice": "Pemberitahuan Penting",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Ini akan memindahkan semua data aplikasi Anda ke lokasi baru. Pastikan tujuan memiliki cukup ruang kosong.",
|
||||
"Restart App": "Restart Aplikasi",
|
||||
"Start Migration": "Mulai Migrasi",
|
||||
"Advanced Settings": "Pengaturan Lanjutan",
|
||||
"File count: {{size}}": "Jumlah file: {{size}}",
|
||||
"Background Read Aloud": "Bacakan Latar Belakang",
|
||||
"Ready to read aloud": "Siap untuk dibacakan",
|
||||
"Read Aloud": "Bacakan",
|
||||
"Screen Brightness": "Kecerahan Layar",
|
||||
"Background Image": "Gambar Latar Belakang",
|
||||
"Import Image": "Impor Gambar",
|
||||
"Opacity": "Keburaman",
|
||||
"Size": "Ukuran",
|
||||
"Cover": "Sampul",
|
||||
"Contain": "Mengandung",
|
||||
"{{number}} pages left in chapter": "<0>{{number}}</0><1> halaman tersisa di bab</1>",
|
||||
"Device": "Perangkat",
|
||||
"E-Ink Mode": "Mode E-Ink",
|
||||
"Highlight Colors": "Warna Sorotan",
|
||||
"Pagination": "Paginasi",
|
||||
"Disable Double Tap": "Nonaktifkan Ketuk Ganda",
|
||||
"Tap to Paginate": "Ketuk untuk Paginasi",
|
||||
"Click to Paginate": "Klik untuk Paginasi",
|
||||
"Tap Both Sides": "Ketuk Kedua Sisi",
|
||||
"Click Both Sides": "Klik Kedua Sisi",
|
||||
"Swap Tap Sides": "Tukar Sisi Ketuk",
|
||||
"Swap Click Sides": "Tukar Sisi Klik",
|
||||
"Source and Translated": "Sumber dan Terjemahan",
|
||||
"Translated Only": "Hanya Terjemahan",
|
||||
"Source Only": "Hanya Sumber",
|
||||
"TTS Text": "Teks TTS",
|
||||
"The book file is corrupted": "File buku rusak",
|
||||
"The book file is empty": "File buku kosong",
|
||||
"Failed to open the book file": "Gagal membuka file buku",
|
||||
"On-Demand Purchase": "Pembelian Sesuai Permintaan",
|
||||
"Full Customization": "Kustomisasi Penuh",
|
||||
"Lifetime Plan": "Rencana Seumur Hidup",
|
||||
"One-Time Payment": "Pembayaran Sekali",
|
||||
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Lakukan pembayaran sekali untuk menikmati akses seumur hidup ke fitur tertentu di semua perangkat. Beli fitur atau layanan tertentu hanya saat Anda membutuhkannya.",
|
||||
"Expand Cloud Sync Storage": "Perluas Penyimpanan Sinkronisasi Cloud",
|
||||
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Perluas penyimpanan cloud Anda selamanya dengan pembelian sekali. Setiap pembelian tambahan menambah lebih banyak ruang.",
|
||||
"Unlock All Customization Options": "Buka Semua Opsi Kustomisasi",
|
||||
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Buka tema tambahan, font, opsi tata letak, dan baca nyaring, penerjemah, layanan penyimpanan cloud.",
|
||||
"Purchase Successful!": "Pembelian Berhasil!",
|
||||
"lifetime": "seumur hidup",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Terima kasih atas pembelian Anda! Pembayaran Anda telah berhasil diproses.",
|
||||
"Order ID:": "ID Pesanan:",
|
||||
"TTS Highlighting": "TTS Menyorot",
|
||||
"Style": "Gaya",
|
||||
"Underline": "Garisan Bawah",
|
||||
"Strikethrough": "Garis Tengah",
|
||||
"Squiggly": "Bergelombang",
|
||||
"Outline": "Garis Besar",
|
||||
"Save Current Color": "Simpan Warna Saat Ini",
|
||||
"Quick Colors": "Warna Cepat",
|
||||
"Highlighter": "Penyorot",
|
||||
"Save Book Cover": "Simpan Sampul Buku",
|
||||
"Auto-save last book cover": "Simpan otomatis sampul buku terakhir",
|
||||
"Back": "Kembali",
|
||||
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Email konfirmasi telah dikirim! Silakan periksa alamat email lama dan baru Anda untuk mengonfirmasi perubahan.",
|
||||
"Failed to update email": "Gagal memperbarui email",
|
||||
"New Email": "Email baru",
|
||||
"Your new email": "Email baru Anda",
|
||||
"Updating email ...": "Memperbarui email ...",
|
||||
"Update email": "Perbarui email",
|
||||
"Current email": "Email saat ini",
|
||||
"Update Email": "Perbarui email",
|
||||
"All": "Semua",
|
||||
"Unable to open book": "Tidak dapat membuka buku",
|
||||
"Punctuation": "Tanda Baca",
|
||||
"Replace Quotation Marks": "Ganti Tanda Kutip",
|
||||
"Enabled only in vertical layout.": "Hanya diaktifkan dalam tata letak vertikal.",
|
||||
"No Conversion": "Tanpa konversi",
|
||||
"Simplified to Traditional": "Sederhana → Tradisional",
|
||||
"Traditional to Simplified": "Tradisional → Sederhana",
|
||||
"Simplified to Traditional (Taiwan)": "Sederhana → Tradisional (Taiwan)",
|
||||
"Simplified to Traditional (Hong Kong)": "Sederhana → Tradisional (Hong Kong)",
|
||||
"Simplified to Traditional (Taiwan), with phrases": "Sederhana → Tradisional (Taiwan • frasa)",
|
||||
"Traditional (Taiwan) to Simplified": "Tradisional (Taiwan) → Sederhana",
|
||||
"Traditional (Hong Kong) to Simplified": "Tradisional (Hong Kong) → Sederhana",
|
||||
"Traditional (Taiwan) to Simplified, with phrases": "Tradisional (Taiwan • frasa) → Sederhana",
|
||||
"Convert Simplified and Traditional Chinese": "Konversi Tionghoa Sederhana/Tradisional",
|
||||
"Convert Mode": "Mode Konversi",
|
||||
"Failed to auto-save book cover for lock screen: {{error}}": "Gagal menyimpan otomatis sampul buku untuk layar kunci: {{error}}",
|
||||
"Download from Cloud": "Unduh dari Cloud",
|
||||
"Upload to Cloud": "Unggah ke Cloud",
|
||||
"Clear Custom Fonts": "Bersihkan Font Kustom",
|
||||
"Columns": "Kolom",
|
||||
"OPDS Catalogs": "Katalog OPDS",
|
||||
"Adding LAN addresses is not supported in the web app version.": "Menambahkan alamat LAN tidak didukung di versi web.",
|
||||
"Invalid OPDS catalog. Please check the URL.": "Katalog OPDS tidak valid. Silakan periksa URL-nya.",
|
||||
"Browse and download books from online catalogs": "Jelajahi dan unduh buku dari katalog online",
|
||||
"My Catalogs": "Katalog Saya",
|
||||
"Add Catalog": "Tambah Katalog",
|
||||
"No catalogs yet": "Belum ada katalog",
|
||||
"Add your first OPDS catalog to start browsing books": "Tambahkan katalog OPDS pertama Anda untuk mulai menjelajahi buku",
|
||||
"Add Your First Catalog": "Tambah Katalog Pertama Anda",
|
||||
"Browse": "Jelajahi",
|
||||
"Popular Catalogs": "Katalog Populer",
|
||||
"Add": "Tambah",
|
||||
"Add OPDS Catalog": "Tambah Katalog OPDS",
|
||||
"Catalog Name": "Nama Katalog",
|
||||
"My Calibre Library": "Perpustakaan Calibre Saya",
|
||||
"OPDS URL": "URL OPDS",
|
||||
"Username (optional)": "Username (opsional)",
|
||||
"Password (optional)": "Password (opsional)",
|
||||
"Description (optional)": "Deskripsi (opsional)",
|
||||
"A brief description of this catalog": "Deskripsi singkat untuk katalog ini",
|
||||
"Validating...": "Memvalidasi...",
|
||||
"View All": "Lihat Semua",
|
||||
"Forward": "Maju",
|
||||
"Home": "Beranda",
|
||||
"{{count}} items_other": "{{count}} item",
|
||||
"Download completed": "Unduhan selesai",
|
||||
"Download failed": "Unduhan gagal",
|
||||
"Open Access": "Akses Terbuka",
|
||||
"Borrow": "Pinjam",
|
||||
"Buy": "Beli",
|
||||
"Subscribe": "Berlangganan",
|
||||
"Sample": "Contoh",
|
||||
"Download": "Unduh",
|
||||
"Open & Read": "Buka & Baca",
|
||||
"Tags": "Tag",
|
||||
"Tag": "Tag",
|
||||
"First": "Pertama",
|
||||
"Previous": "Sebelumnya",
|
||||
"Next": "Berikutnya",
|
||||
"Last": "Terakhir",
|
||||
"Cannot Load Page": "Tidak dapat memuat halaman",
|
||||
"An error occurred": "Terjadi kesalahan",
|
||||
"Online Library": "Perpustakaan Online",
|
||||
"URL must start with http:// or https://": "URL harus diawali dengan http:// atau https://",
|
||||
"Title, Author, Tag, etc...": "Judul, Penulis, Tag, dll...",
|
||||
"Query": "Kueri",
|
||||
"Subject": "Subjek",
|
||||
"Enter {{terms}}": "Masukkan {{terms}}",
|
||||
"No search results found": "Tidak ada hasil pencarian ditemukan",
|
||||
"Failed to load OPDS feed: {{status}} {{statusText}}": "Gagal memuat feed OPDS: {{status}} {{statusText}}",
|
||||
"Search in {{title}}": "Cari di {{title}}",
|
||||
"Manage Storage": "Kelola Penyimpanan",
|
||||
"Failed to load files": "Gagal memuat file",
|
||||
"Deleted {{count}} file(s)_other": "{{count}} file dihapus",
|
||||
"Failed to delete {{count}} file(s)_other": "Gagal menghapus {{count}} file",
|
||||
"Failed to delete files": "Gagal menghapus file",
|
||||
"Total Files": "Total File",
|
||||
"Total Size": "Total Ukuran",
|
||||
"Quota": "Kuota",
|
||||
"Used": "Digunakan",
|
||||
"Files": "File",
|
||||
"Search files...": "Cari file...",
|
||||
"Newest First": "Terbaru",
|
||||
"Oldest First": "Terlama",
|
||||
"Largest First": "Terbesar",
|
||||
"Smallest First": "Terkecil",
|
||||
"Name A-Z": "Nama A-Z",
|
||||
"Name Z-A": "Nama Z-A",
|
||||
"{{count}} selected_other": "{{count}} dipilih",
|
||||
"Delete Selected": "Hapus yang Dipilih",
|
||||
"Created": "Dibuat",
|
||||
"No files found": "Tidak ada file ditemukan",
|
||||
"No files uploaded yet": "Belum ada file diunggah",
|
||||
"files": "file",
|
||||
"Page {{current}} of {{total}}": "Halaman {{current}} dari {{total}}",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "Yakin ingin menghapus {{count}} file yang dipilih?",
|
||||
"Cloud Storage Usage": "Penggunaan Penyimpanan Cloud",
|
||||
"Rename Group": "Ganti Nama Grup",
|
||||
"From Directory": "Dari Direktori",
|
||||
"Successfully imported {{count}} book(s)_other": "Berhasil mengimpor {{count}} buku",
|
||||
"Count": "Jumlah",
|
||||
"Start Page": "Halaman Awal",
|
||||
"Search in OPDS Catalog...": "Cari di Katalog OPDS...",
|
||||
"Please log in to use advanced TTS features": "Silakan masuk untuk menggunakan fitur TTS lanjutan",
|
||||
"Word limit of 30 words exceeded.": "Batas 30 kata telah terlampaui.",
|
||||
"Proofread": "Koreksi",
|
||||
"Current selection": "Pilihan saat ini",
|
||||
"All occurrences in this book": "Semua kemunculan di buku ini",
|
||||
"All occurrences in your library": "Semua kemunculan di perpustakaan Anda",
|
||||
"Selected text:": "Teks terpilih:",
|
||||
"Replace with:": "Ganti dengan:",
|
||||
"Enter text...": "Masukkan teks…",
|
||||
"Case sensitive:": "Peka huruf besar dan kecil:",
|
||||
"Scope:": "Cakupan:",
|
||||
"Selection": "Pilihan",
|
||||
"Library": "Perpustakaan",
|
||||
"Yes": "Ya",
|
||||
"No": "Tidak",
|
||||
"Proofread Replacement Rules": "Aturan penggantian koreksi",
|
||||
"Selected Text Rules": "Aturan teks terpilih",
|
||||
"No selected text replacement rules": "Tidak ada aturan penggantian untuk teks terpilih",
|
||||
"Book Specific Rules": "Aturan khusus buku",
|
||||
"No book-level replacement rules": "Tidak ada aturan penggantian tingkat buku",
|
||||
"Disable Quick Action": "Nonaktifkan Aksi Cepat",
|
||||
"Enable Quick Action on Selection": "Aktifkan Aksi Cepat saat memilih",
|
||||
"None": "Tidak ada",
|
||||
"Annotation Tools": "Alat Anotasi",
|
||||
"Enable Quick Actions": "Aktifkan Aksi Cepat",
|
||||
"Quick Action": "Aksi Cepat",
|
||||
"Copy to Notebook": "Salin ke Buku Catatan",
|
||||
"Copy text after selection": "Salin teks setelah pemilihan",
|
||||
"Highlight text after selection": "Sorot teks setelah pemilihan",
|
||||
"Annotate text after selection": "Anotasi teks setelah pemilihan",
|
||||
"Search text after selection": "Cari teks setelah pemilihan",
|
||||
"Look up text in dictionary after selection": "Cari teks di kamus setelah pemilihan",
|
||||
"Look up text in Wikipedia after selection": "Cari teks di Wikipedia setelah pemilihan",
|
||||
"Translate text after selection": "Terjemahkan teks setelah pemilihan",
|
||||
"Read text aloud after selection": "Bacakan teks setelah pemilihan",
|
||||
"Proofread text after selection": "Periksa teks setelah pemilihan",
|
||||
"{{activeCount}} active, {{pendingCount}} pending": "{{activeCount}} aktif, {{pendingCount}} menunggu",
|
||||
"{{failedCount}} failed": "{{failedCount}} gagal",
|
||||
"Waiting...": "Menunggu...",
|
||||
"Failed": "Gagal",
|
||||
"Completed": "Selesai",
|
||||
"Cancelled": "Dibatalkan",
|
||||
"Retry": "Coba lagi",
|
||||
"Active": "Aktif",
|
||||
"Transfer Queue": "Antrian Transfer",
|
||||
"Upload All": "Unggah Semua",
|
||||
"Download All": "Unduh Semua",
|
||||
"Resume Transfers": "Lanjutkan Transfer",
|
||||
"Pause Transfers": "Jeda Transfer",
|
||||
"Pending": "Menunggu",
|
||||
"No transfers": "Tidak ada transfer",
|
||||
"Retry All": "Coba Semua Lagi",
|
||||
"Clear Completed": "Hapus Selesai",
|
||||
"Clear Failed": "Hapus Gagal",
|
||||
"Upload queued: {{title}}": "Unggahan dalam antrian: {{title}}",
|
||||
"Download queued: {{title}}": "Unduhan dalam antrian: {{title}}",
|
||||
"Book not found in library": "Buku tidak ditemukan di perpustakaan",
|
||||
"Unknown error": "Kesalahan tidak diketahui",
|
||||
"Please log in to continue": "Silakan masuk untuk melanjutkan",
|
||||
"Cloud File Transfers": "Transfer File Cloud",
|
||||
"Show Search Results": "Tampilkan hasil pencarian",
|
||||
"Search results for '{{term}}'": "Hasil untuk '{{term}}'",
|
||||
"Close Search": "Tutup pencarian",
|
||||
"Previous Result": "Hasil sebelumnya",
|
||||
"Next Result": "Hasil berikutnya",
|
||||
"Bookmarks": "Penanda",
|
||||
"Annotations": "Anotasi",
|
||||
"Show Results": "Tampilkan Hasil",
|
||||
"Clear search": "Hapus pencarian",
|
||||
"Clear search history": "Hapus riwayat pencarian",
|
||||
"Tap to Toggle Footer": "Ketuk untuk beralih footer",
|
||||
"Show Current Time": "Tampilkan Waktu Sekarang",
|
||||
"Use 24 Hour Clock": "Gunakan Format 24 Jam",
|
||||
"Show Current Battery Status": "Tampilkan Status Baterai Saat Ini",
|
||||
"Exported successfully": "Berhasil diekspor",
|
||||
"Book exported successfully.": "Buku berhasil diekspor.",
|
||||
"Failed to export the book.": "Gagal mengekspor buku.",
|
||||
"Export Book": "Ekspor Buku",
|
||||
"Whole word:": "Kata utuh:",
|
||||
"Error": "Kesalahan",
|
||||
"Unable to load the article. Try searching directly on {{link}}.": "Tidak dapat memuat artikel. Coba cari langsung di {{link}}.",
|
||||
"Unable to load the word. Try searching directly on {{link}}.": "Tidak dapat memuat kata. Coba cari langsung di {{link}}.",
|
||||
"Date Published": "Tanggal terbit",
|
||||
"Only for TTS:": "Hanya untuk TTS:",
|
||||
"Uploaded": "Diunggah",
|
||||
"Downloaded": "Diunduh",
|
||||
"Deleted": "Dihapus",
|
||||
"Note:": "Catatan:",
|
||||
"Time:": "Waktu:",
|
||||
"Format Options": "Opsi Format",
|
||||
"Export Date": "Tanggal Ekspor",
|
||||
"Chapter Titles": "Judul Bab",
|
||||
"Chapter Separator": "Pemisah Bab",
|
||||
"Highlights": "Sorotan",
|
||||
"Note Date": "Tanggal Catatan",
|
||||
"Advanced": "Lanjutan",
|
||||
"Hide": "Sembunyikan",
|
||||
"Show": "Tampilkan",
|
||||
"Use Custom Template": "Gunakan Template Kustom",
|
||||
"Export Template": "Template Ekspor",
|
||||
"Template Syntax:": "Sintaks Template:",
|
||||
"Insert value": "Masukkan nilai",
|
||||
"Format date (locale)": "Format tanggal (lokal)",
|
||||
"Format date (custom)": "Format tanggal (kustom)",
|
||||
"Conditional": "Kondisional",
|
||||
"Loop": "Loop",
|
||||
"Available Variables:": "Variabel yang Tersedia:",
|
||||
"Book title": "Judul buku",
|
||||
"Book author": "Penulis buku",
|
||||
"Export date": "Tanggal ekspor",
|
||||
"Array of chapters": "Daftar bab",
|
||||
"Chapter title": "Judul bab",
|
||||
"Array of annotations": "Daftar anotasi",
|
||||
"Highlighted text": "Teks yang disorot",
|
||||
"Annotation note": "Catatan anotasi",
|
||||
"Date Format Tokens:": "Token Format Tanggal:",
|
||||
"Year (4 digits)": "Tahun (4 digit)",
|
||||
"Month (01-12)": "Bulan (01-12)",
|
||||
"Day (01-31)": "Hari (01-31)",
|
||||
"Hour (00-23)": "Jam (00-23)",
|
||||
"Minute (00-59)": "Menit (00-59)",
|
||||
"Second (00-59)": "Detik (00-59)",
|
||||
"Show Source": "Tampilkan Sumber",
|
||||
"No content to preview": "Tidak ada konten untuk dipratinjau",
|
||||
"Export": "Ekspor",
|
||||
"Set Timeout": "Atur batas waktu",
|
||||
"Select Voice": "Pilih suara",
|
||||
"Toggle Sticky Bottom TTS Bar": "Alihkan bilah TTS tetap",
|
||||
"Display what I'm reading on Discord": "Tampilkan buku yang sedang dibaca di Discord",
|
||||
"Show on Discord": "Tampilkan di Discord",
|
||||
"Instant {{action}}": "{{action}} Instan",
|
||||
"Instant {{action}} Disabled": "{{action}} Instan Dinonaktifkan",
|
||||
"Annotation": "Anotasi",
|
||||
"Reset Template": "Atur Ulang Template",
|
||||
"Annotation style": "Gaya anotasi",
|
||||
"Annotation color": "Warna anotasi",
|
||||
"Annotation time": "Waktu anotasi",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Apakah Anda yakin ingin mengindeks ulang buku ini?",
|
||||
"Enable AI in Settings": "Aktifkan AI di Pengaturan",
|
||||
"Index This Book": "Indeks Buku Ini",
|
||||
"Enable AI search and chat for this book": "Aktifkan pencarian AI dan obrolan untuk buku ini",
|
||||
"Start Indexing": "Mulai Mengindeks",
|
||||
"Indexing book...": "Mengindeks buku...",
|
||||
"Preparing...": "Mempersiapkan...",
|
||||
"Delete this conversation?": "Hapus percakapan ini?",
|
||||
"No conversations yet": "Belum ada percakapan",
|
||||
"Start a new chat to ask questions about this book": "Mulai obrolan baru untuk mengajukan pertanyaan tentang buku ini",
|
||||
"Rename": "Ubah nama",
|
||||
"New Chat": "Obrolan Baru",
|
||||
"Chat": "Obrolan",
|
||||
"Please enter a model ID": "Silakan masukkan ID model",
|
||||
"Model not available or invalid": "Model tidak tersedia atau tidak valid",
|
||||
"Failed to validate model": "Gagal memvalidasi model",
|
||||
"Couldn't connect to Ollama. Is it running?": "Tidak dapat terhubung ke Ollama. Apakah sedang berjalan?",
|
||||
"Invalid API key or connection failed": "Kunci API tidak valid atau koneksi gagal",
|
||||
"Connection failed": "Koneksi gagal",
|
||||
"AI Assistant": "Asisten AI",
|
||||
"Enable AI Assistant": "Aktifkan Asisten AI",
|
||||
"Provider": "Penyedia",
|
||||
"Ollama (Local)": "Ollama (Lokal)",
|
||||
"AI Gateway (Cloud)": "Gateway AI (Cloud)",
|
||||
"Ollama Configuration": "Konfigurasi Ollama",
|
||||
"Refresh Models": "Segarkan Model",
|
||||
"AI Model": "Model AI",
|
||||
"No models detected": "Tidak ada model terdeteksi",
|
||||
"AI Gateway Configuration": "Konfigurasi Gateway AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Pilih dari pilihan model AI berkualitas tinggi dan ekonomis. Anda juga dapat menggunakan model Anda sendiri dengan memilih \"Model Kustom\" di bawah.",
|
||||
"API Key": "Kunci API",
|
||||
"Get Key": "Dapatkan Kunci",
|
||||
"Model": "Model",
|
||||
"Custom Model...": "Model Kustom...",
|
||||
"Custom Model ID": "ID Model Kustom",
|
||||
"Validate": "Validasi",
|
||||
"Model available": "Model tersedia",
|
||||
"Connection": "Koneksi",
|
||||
"Test Connection": "Uji Koneksi",
|
||||
"Connected": "Terhubung",
|
||||
"Custom Colors": "Warna Kustom",
|
||||
"Color E-Ink Mode": "Mode E-Ink Berwarna",
|
||||
"Reading Ruler": "Penggaris Membaca",
|
||||
"Enable Reading Ruler": "Aktifkan Penggaris Membaca",
|
||||
"Lines to Highlight": "Baris untuk Disorot",
|
||||
"Ruler Color": "Warna Penggaris",
|
||||
"Command Palette": "Palet Perintah",
|
||||
"Search settings and actions...": "Cari pengaturan dan tindakan...",
|
||||
"No results found for": "Tidak ada hasil ditemukan untuk",
|
||||
"Type to search settings and actions": "Ketik untuk mencari pengaturan dan tindakan",
|
||||
"Recent": "Terbaru",
|
||||
"navigate": "navigasi",
|
||||
"select": "pilih",
|
||||
"close": "tutup",
|
||||
"Search Settings": "Cari Pengaturan",
|
||||
"Page Margins": "Margin Halaman",
|
||||
"AI Provider": "Penyedia AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi",
|
||||
"Set status for {{count}} book(s)_other": "Atur status untuk {{count}} buku",
|
||||
"Mark as Unread": "Tandai sebagai Belum Dibaca",
|
||||
"Mark as Finished": "Tandai sebagai Selesai",
|
||||
"Finished": "Selesai",
|
||||
"Unread": "Belum Dibaca",
|
||||
"Clear Status": "Hapus Status",
|
||||
"Status": "Status",
|
||||
"Loading": "Memuat...",
|
||||
"Exit Paragraph Mode": "Keluar dari Mode Paragraf",
|
||||
"Paragraph Mode": "Mode Paragraf",
|
||||
"Embedding Model": "Model Penyematan",
|
||||
"{{count}} book(s) synced_other": "{{count}} buku telah disinkronkan",
|
||||
"Unable to start RSVP": "Tidak dapat memulai RSVP",
|
||||
"RSVP not supported for PDF": "RSVP tidak didukung untuk PDF",
|
||||
"Select Chapter": "Pilih Bab",
|
||||
"Context": "Konteks",
|
||||
"Ready": "Siap",
|
||||
"Chapter Progress": "Kemajuan Bab",
|
||||
"words": "kata",
|
||||
"{{time}} left": "{{time}} tersisa",
|
||||
"Reading progress": "Kemajuan membaca",
|
||||
"Click to seek": "Klik untuk mencari",
|
||||
"Skip back 15 words": "Lompat mundur 15 kata",
|
||||
"Back 15 words (Shift+Left)": "Mundur 15 kata (Shift+Kiri)",
|
||||
"Pause (Space)": "Jeda (Spasi)",
|
||||
"Play (Space)": "Putar (Spasi)",
|
||||
"Skip forward 15 words": "Lompat maju 15 kata",
|
||||
"Forward 15 words (Shift+Right)": "Maju 15 kata (Shift+Kanan)",
|
||||
"Decrease speed": "Kurangi kecepatan",
|
||||
"Slower (Left/Down)": "Lebih lambat (Kiri/Bawah)",
|
||||
"Increase speed": "Tambah kecepatan",
|
||||
"Faster (Right/Up)": "Lebih cepat (Kanan/Atas)",
|
||||
"Start RSVP Reading": "Mulai Membaca RSVP",
|
||||
"Choose where to start reading": "Pilih tempat untuk mulai membaca",
|
||||
"From Chapter Start": "Dari Awal Bab",
|
||||
"Start reading from the beginning of the chapter": "Mulai membaca dari awal bab",
|
||||
"Resume": "Lanjutkan",
|
||||
"Continue from where you left off": "Lanjutkan dari tempat Anda berhenti",
|
||||
"From Current Page": "Dari Halaman Saat Ini",
|
||||
"Start from where you are currently reading": "Mulai dari tempat Anda sedang membaca",
|
||||
"From Selection": "Dari Pilihan",
|
||||
"Speed Reading Mode": "Mode Baca Cepat",
|
||||
"Scroll left": "Gulir ke kiri",
|
||||
"Scroll right": "Gulir ke kanan",
|
||||
"Library Sync Progress": "Kemajuan Sinkronisasi Pustaka",
|
||||
"Back to library": "Kembali ke perpustakaan",
|
||||
"Group by...": "Kelompokkan menurut...",
|
||||
"Export as Plain Text": "Ekspor sebagai Teks Biasa",
|
||||
"Export as Markdown": "Ekspor sebagai Markdown",
|
||||
"Show Page Navigation Buttons": "Tombol navigasi",
|
||||
"Page {{number}}": "Halaman {{number}}",
|
||||
"highlight": "sorotan",
|
||||
"underline": "garis bawah",
|
||||
"squiggly": "bergelombang",
|
||||
"red": "merah",
|
||||
"violet": "ungu",
|
||||
"blue": "biru",
|
||||
"green": "hijau",
|
||||
"yellow": "kuning",
|
||||
"Select {{style}} style": "Pilih gaya {{style}}",
|
||||
"Select {{color}} color": "Pilih warna {{color}}",
|
||||
"Close Book": "Tutup Buku",
|
||||
"Speed Reading": "Membaca Cepat",
|
||||
"Close Speed Reading": "Tutup Membaca Cepat",
|
||||
"Authors": "Penulis",
|
||||
"Books": "Buku",
|
||||
"Groups": "Grup",
|
||||
"Back to TTS Location": "Kembali ke Lokasi TTS",
|
||||
"Metadata": "Metadata",
|
||||
"Image viewer": "Penampil gambar",
|
||||
"Previous Image": "Gambar Sebelumnya",
|
||||
"Next Image": "Gambar Berikutnya",
|
||||
"Zoomed": "Diperbesar",
|
||||
"Zoom level": "Tingkat zoom",
|
||||
"Table viewer": "Penampil tabel",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Tidak dapat terhubung ke Readwise. Silakan periksa koneksi jaringan Anda.",
|
||||
"Invalid Readwise access token": "Token akses Readwise tidak valid",
|
||||
"Disconnected from Readwise": "Terputus dari Readwise",
|
||||
"Never": "Tidak pernah",
|
||||
"Readwise Settings": "Pengaturan Readwise",
|
||||
"Connected to Readwise": "Terhubung ke Readwise",
|
||||
"Last synced: {{time}}": "Terakhir disinkronkan: {{time}}",
|
||||
"Sync Enabled": "Sinkronisasi Diaktifkan",
|
||||
"Disconnect": "Putuskan sambungan",
|
||||
"Connect your Readwise account to sync highlights.": "Hubungkan akun Readwise Anda untuk menyinkronkan sorotan.",
|
||||
"Get your access token at": "Dapatkan token akses Anda di",
|
||||
"Access Token": "Token Akses",
|
||||
"Paste your Readwise access token": "Tempelkan token akses Readwise Anda",
|
||||
"Config": "Konfigurasi",
|
||||
"Readwise Sync": "Sinkronisasi Readwise",
|
||||
"Push Highlights": "Kirim Sorotan",
|
||||
"Highlights synced to Readwise": "Sorotan disinkronkan ke Readwise",
|
||||
"Readwise sync failed: no internet connection": "Sinkronisasi Readwise gagal: tidak ada koneksi internet",
|
||||
"Readwise sync failed: {{error}}": "Sinkronisasi Readwise gagal: {{error}}",
|
||||
"System Screen Brightness": "Kecerahan Layar Sistem",
|
||||
"Page:": "Halaman:",
|
||||
"Page: {{number}}": "Halaman: {{number}}",
|
||||
"Annotation page number": "Nomor halaman anotasi",
|
||||
"Translating...": "Menerjemahkan...",
|
||||
"Show Battery Percentage": "Tampilkan Persentase Baterai",
|
||||
"Hide Scrollbar": "Sembunyikan Bilah Gulir",
|
||||
"Skip to last reading position": "Lompat ke posisi baca terakhir",
|
||||
"Show context": "Tampilkan konteks",
|
||||
"Hide context": "Sembunyikan konteks",
|
||||
"Decrease font size": "Perkecil ukuran font",
|
||||
"Increase font size": "Perbesar ukuran font",
|
||||
"Focus": "Fokus",
|
||||
"Theme color": "Warna tema",
|
||||
"Import failed": "Impor gagal",
|
||||
"Available Formatters:": "Formatter Tersedia:",
|
||||
"Format date": "Format tanggal",
|
||||
"Markdown block quote (> per line)": "Kutipan blok Markdown (> per baris)",
|
||||
"Newlines to <br>": "Baris baru ke <br>",
|
||||
"Change case": "Ubah huruf besar/kecil",
|
||||
"Trim whitespace": "Pangkas spasi",
|
||||
"Truncate to n characters": "Potong ke n karakter",
|
||||
"Replace text": "Ganti teks",
|
||||
"Fallback value": "Nilai cadangan",
|
||||
"Get length": "Dapatkan panjang",
|
||||
"First/last element": "Elemen pertama/terakhir",
|
||||
"Join array": "Gabungkan array",
|
||||
"Backup failed: {{error}}": "Pencadangan gagal: {{error}}",
|
||||
"Select Backup": "Pilih Cadangan",
|
||||
"Restore failed: {{error}}": "Pemulihan gagal: {{error}}",
|
||||
"Backup & Restore": "Cadangkan & Pulihkan",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Buat cadangan perpustakaan Anda atau pulihkan dari cadangan sebelumnya. Pemulihan akan digabungkan dengan perpustakaan Anda saat ini.",
|
||||
"Backup Library": "Cadangkan Perpustakaan",
|
||||
"Restore Library": "Pulihkan Perpustakaan",
|
||||
"Creating backup...": "Membuat cadangan...",
|
||||
"Restoring library...": "Memulihkan perpustakaan...",
|
||||
"{{current}} of {{total}} items": "{{current}} dari {{total}} item",
|
||||
"Backup completed successfully!": "Pencadangan berhasil!",
|
||||
"Restore completed successfully!": "Pemulihan berhasil!",
|
||||
"Your library has been saved to the selected location.": "Perpustakaan Anda telah disimpan di lokasi yang dipilih.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} buku ditambahkan, {{updated}} buku diperbarui.",
|
||||
"Operation failed": "Operasi gagal",
|
||||
"Loading library...": "Memuat perpustakaan...",
|
||||
"{{count}} books refreshed_other": "{{count}} buku diperbarui",
|
||||
"Failed to refresh metadata": "Gagal memperbarui metadata",
|
||||
"Refresh Metadata": "Perbarui Metadata"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"Auto Mode": "自動モード",
|
||||
"Behavior": "動作",
|
||||
"Book": "書籍",
|
||||
"Book Cover": "表紙",
|
||||
"Bookmark": "ブックマーク",
|
||||
"Cancel": "キャンセル",
|
||||
"Chapter": "章",
|
||||
@@ -18,7 +17,6 @@
|
||||
"Confirm Deletion": "削除の確認",
|
||||
"Copied to notebook": "ノートブックにコピー",
|
||||
"Copy": "コピー",
|
||||
"Custom CSS": "カスタムCSS",
|
||||
"Dark Mode": "ダークモード",
|
||||
"Default": "デフォルト",
|
||||
"Default Font": "デフォルトフォント",
|
||||
@@ -28,7 +26,6 @@
|
||||
"Dictionary": "辞書",
|
||||
"Download Readest": "Readestをダウンロード",
|
||||
"Edit": "編集",
|
||||
"Enter your custom CSS here...": "ここにカスタムCSSを入力...",
|
||||
"Excerpts": "抜粋",
|
||||
"Failed to import book(s): {{filenames}}": "書籍のインポートに失敗しました:{{filenames}}",
|
||||
"Fast": "高速",
|
||||
@@ -48,7 +45,6 @@
|
||||
"Horizontal Direction": "横書",
|
||||
"Hyphenation": "ハイフネーション",
|
||||
"Import Books": "書籍をインポート",
|
||||
"Language:": "言語:",
|
||||
"Layout": "レイアウト",
|
||||
"Light Mode": "ライトモード",
|
||||
"Loading...": "読み込み中...",
|
||||
@@ -70,8 +66,8 @@
|
||||
"Paging Animation": "ページめくりアニメーション",
|
||||
"Paragraph": "段落",
|
||||
"Parallel Read": "並列読書",
|
||||
"Published:": "出版日:",
|
||||
"Publisher:": "出版社:",
|
||||
"Published": "出版日",
|
||||
"Publisher": "出版社",
|
||||
"Reading Progress Synced": "読書進捗が同期されました",
|
||||
"Reload Page": "ページを再読み込み",
|
||||
"Reveal in File Explorer": "エクスプローラーで表示",
|
||||
@@ -85,7 +81,6 @@
|
||||
"Search...": "検索...",
|
||||
"Select Book": "書籍を選択",
|
||||
"Select Books": "書籍を選択",
|
||||
"Select Multiple Books": "複数の書籍を選択",
|
||||
"Sepia": "セピア",
|
||||
"Serif Font": "セリフ体",
|
||||
"Show Book Details": "書籍の詳細を表示",
|
||||
@@ -96,23 +91,21 @@
|
||||
"Slow": "低速",
|
||||
"Solarized": "ソーラライズド",
|
||||
"Speak": "読み上げ",
|
||||
"Subjects:": "主題:",
|
||||
"Subjects": "主題",
|
||||
"System Fonts": "システムフォント",
|
||||
"TTS not supported in this device": "このデバイスではTTSがサポートされていません",
|
||||
"Theme Color": "テーマカラー",
|
||||
"Theme Mode": "テーマモード",
|
||||
"Translate": "翻訳",
|
||||
"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.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
|
||||
"Wikipedia": "ウィキペディア",
|
||||
"Writing Mode": "書き込みモード",
|
||||
"Your Library": "ライブラリー",
|
||||
"TTS not supported for PDF": "PDFではTTSがサポートされていません",
|
||||
"Override Book Font": "書籍フォントを上書き",
|
||||
"Apply to All Books": "すべての書籍に適用",
|
||||
"Apply to This Book": "この書籍に適用",
|
||||
@@ -122,14 +115,13 @@
|
||||
"Book Details": "書籍の詳細",
|
||||
"From Local File": "ローカルファイルから",
|
||||
"TOC": "目次",
|
||||
"Table of Contents": "目次",
|
||||
"Book uploaded: {{title}}": "書籍がアップロードされました:{{title}}",
|
||||
"Failed to upload book: {{title}}": "書籍のアップロードに失敗しました:{{title}}",
|
||||
"Book downloaded: {{title}}": "書籍がダウンロードされました:{{title}}",
|
||||
"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 +153,7 @@
|
||||
"Create a Password": "パスワードを作成",
|
||||
"Sign up": "サインアップ",
|
||||
"Signing up...": "サインアップ中...",
|
||||
"Don’t 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": "マジックリンクメールを送信",
|
||||
@@ -180,40 +172,15 @@
|
||||
"Token": "トークン",
|
||||
"Your OTP token": "あなたのOTPトークン",
|
||||
"Verify token": "トークンを確認",
|
||||
"Sign in with Google": "Googleでサインイン",
|
||||
"Sign in with Apple": "Appleでサインイン",
|
||||
"Sign in with GitHub": "GitHubでサインイン",
|
||||
"Account": "アカウント",
|
||||
"Failed to delete user. Please try again later.": "ユーザーの削除に失敗しました。後ほど再試行してください。",
|
||||
"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": "最大列幅",
|
||||
@@ -222,7 +189,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": "テキストカラー",
|
||||
@@ -249,9 +216,9 @@
|
||||
"Auto Import on File Open": "ファイルを開くと自動インポート",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "章が検出されません。",
|
||||
"Failed to parse the EPUB file.": "EPUBファイルの解析に失敗しました。",
|
||||
"This book format is not supported.": "この書籍形式はサポートされていません。",
|
||||
"No chapters detected": "章が検出されません",
|
||||
"Failed to parse the EPUB file": "EPUBファイルの解析に失敗しました",
|
||||
"This book format is not supported": "この書籍形式はサポートされていません",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "翻訳を取得できません。まずログインして再試行してください。",
|
||||
"Group": "グループ",
|
||||
"Always on Top": "常に最前面",
|
||||
@@ -264,13 +231,13 @@
|
||||
"Clear Search": "検索をクリア",
|
||||
"Header & Footer": "ヘッダーとフッター",
|
||||
"Apply also in Scrolled Mode": "スクロールモードでも適用",
|
||||
"A new version of Readest is Available!": "新しいバージョンのReadestが利用可能です!",
|
||||
"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:": "変更ログ:",
|
||||
"Changelog": "変更ログ",
|
||||
"Software Update": "ソフトウェアの更新",
|
||||
"Title": "タイトル",
|
||||
"Date Read": "読書日",
|
||||
@@ -279,18 +246,15 @@
|
||||
"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)": "(「お気に召すまま」第2幕)",
|
||||
"Link Color": "リンクカラー",
|
||||
"Volume Keys for Page Flip": "音量キーでページめくり",
|
||||
"Clicks for Page Flip": "タップでページめくり",
|
||||
"Swap Clicks Area": "タップ領域を入れ替え",
|
||||
"Screen": "画面",
|
||||
"Orientation": "向き",
|
||||
"Portrait": "縦",
|
||||
@@ -299,7 +263,7 @@
|
||||
"Checking for updates...": "更新を確認中...",
|
||||
"Error checking for updates": "更新の確認中にエラーが発生しました",
|
||||
"Details": "詳細",
|
||||
"File Size:": "ファイルサイズ:",
|
||||
"File Size": "ファイルサイズ",
|
||||
"Auto Detect": "自動検出",
|
||||
"Next Section": "次のセクション",
|
||||
"Previous Section": "前のセクション",
|
||||
@@ -325,7 +289,6 @@
|
||||
"Disable Translation": "翻訳を無効にする",
|
||||
"Scroll": "スクロール",
|
||||
"Overlap Pixels": "重なりピクセル",
|
||||
"Click": "クリック",
|
||||
"System Language": "システム言語",
|
||||
"Security": "セキュリティ",
|
||||
"Allow JavaScript": "JavaScriptを許可",
|
||||
@@ -338,14 +301,12 @@
|
||||
"Synced at {{time}}": "{{time}} に同期済み",
|
||||
"Never synced": "まだ同期されていません",
|
||||
"Show Remaining Time": "残り時間を表示",
|
||||
"Show Page Number": "ページ番号を表示",
|
||||
"{{time}} min left in chapter": "{{time}}分残り",
|
||||
"Override Book Color": "書籍の色を上書き",
|
||||
"Login Required": "ログイン必須",
|
||||
"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 +321,773 @@
|
||||
"Right Margin (px)": "右マージン (px)",
|
||||
"Left Margin (px)": "左マージン (px)",
|
||||
"Column Gap (%)": "列間の間隔 (%)",
|
||||
"Always Show Status Bar": "ステータスバーを常に表示"
|
||||
"Always Show Status Bar": "ステータスバーを常に表示",
|
||||
"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": "<0>{{count}}</0><1> ページ残り</1>",
|
||||
"Show Remaining Pages": "残りを表示",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "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:": "金額:",
|
||||
"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}}",
|
||||
"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": "小説、科学、歴史",
|
||||
"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": "ダブルクリック無効",
|
||||
"TTS not supported for this document": "TTSはこのドキュメントではサポートされていません",
|
||||
"Reset Password": "パスワードをリセット",
|
||||
"Show Reading Progress": "読書進捗を表示",
|
||||
"Reading Progress Style": "読書進捗スタイル",
|
||||
"Page Number": "ページ番号",
|
||||
"Percentage": "パーセンテージ",
|
||||
"Deleted local copy of the book: {{title}}": "書籍のローカルコピーを削除しました:{{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "書籍のクラウドバックアップの削除に失敗しました:{{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "書籍のローカルコピーの削除に失敗しました:{{title}}",
|
||||
"Are you sure to delete the local copy of the selected book?": "選択した書籍のローカルコピーを削除してもよろしいですか?",
|
||||
"Remove from Cloud & Device": "クラウドとデバイスから削除",
|
||||
"Remove from Cloud Only": "クラウドからのみ削除",
|
||||
"Remove from Device Only": "デバイスからのみ削除",
|
||||
"Disconnected": "切断されました",
|
||||
"KOReader Sync Settings": "KOReader Sync設定",
|
||||
"Sync Strategy": "同期戦略",
|
||||
"Ask on conflict": "競合時に確認",
|
||||
"Always use latest": "最新を常に使用",
|
||||
"Send changes only": "変更のみを送信",
|
||||
"Receive changes only": "変更のみを受信",
|
||||
"Checksum Method": "チェックサム方式",
|
||||
"File Content (recommended)": "ファイル内容(推奨)",
|
||||
"File Name": "ファイル名",
|
||||
"Device Name": "デバイス名",
|
||||
"Connect to your KOReader Sync server.": "KOReader Syncサーバーに接続します。",
|
||||
"Server URL": "サーバーURL",
|
||||
"Username": "ユーザー名",
|
||||
"Your Username": "あなたのユーザー名",
|
||||
"Password": "パスワード",
|
||||
"Connect": "接続",
|
||||
"KOReader Sync": "KOReader Sync",
|
||||
"Sync Conflict": "同期競合",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "{{deviceName}}から読書進捗を同期しますか?",
|
||||
"another device": "別のデバイス",
|
||||
"Local Progress": "ローカル進捗",
|
||||
"Remote Progress": "リモート進捗",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "ページ {{page}} / {{total}} ({{percentage}}%)",
|
||||
"Current position": "現在の位置",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "おおよそページ {{page}} / {{total}} ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "おおよそ {{percentage}}%",
|
||||
"Failed to connect": "接続に失敗しました",
|
||||
"Sync Server Connected": "同期サーバーに接続されました",
|
||||
"Sync as {{userDisplayName}}": "{{userDisplayName}} として同期",
|
||||
"Custom Fonts": "カスタムフォント",
|
||||
"Cancel Delete": "削除をキャンセル",
|
||||
"Import Font": "フォントをインポート",
|
||||
"Delete Font": "フォントを削除",
|
||||
"Tips": "ヒント",
|
||||
"Custom fonts can be selected from the Font Face menu": "カスタムフォントは書体メニューから選択できます",
|
||||
"Manage Custom Fonts": "カスタムフォント管理",
|
||||
"Select Files": "ファイルを選択",
|
||||
"Select Image": "画像を選択",
|
||||
"Select Video": "動画を選択",
|
||||
"Select Audio": "音声を選択",
|
||||
"Select Fonts": "フォントを選択",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "サポートされているフォント形式:.ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "進捗をプッシュ",
|
||||
"Pull Progress": "進捗をプル",
|
||||
"Previous Paragraph": "前の段落",
|
||||
"Previous Sentence": "前の文",
|
||||
"Pause": "一時停止",
|
||||
"Play": "再生",
|
||||
"Next Sentence": "次の文",
|
||||
"Next Paragraph": "次の段落",
|
||||
"Separate Cover Page": "別の表紙ページ",
|
||||
"Resize Notebook": "ノートのサイズ変更",
|
||||
"Resize Sidebar": "サイドバーのサイズ変更",
|
||||
"Get Help from the Readest Community": "Readestコミュニティからヘルプを得る",
|
||||
"Remove cover image": "表紙画像を削除",
|
||||
"Bookshelf": "本棚",
|
||||
"View Menu": "メニューを表示",
|
||||
"Settings Menu": "設定メニュー",
|
||||
"View account details and quota": "アカウントの詳細とクォータを表示",
|
||||
"Library Header": "ライブラリヘッダー",
|
||||
"Book Content": "本の内容",
|
||||
"Footer Bar": "フッターバー",
|
||||
"Header Bar": "ヘッダーバー",
|
||||
"View Options": "表示オプション",
|
||||
"Book Menu": "本のメニュー",
|
||||
"Search Options": "検索オプション",
|
||||
"Close": "閉じる",
|
||||
"Delete Book Options": "本の削除オプション",
|
||||
"ON": "オン",
|
||||
"OFF": "オフ",
|
||||
"Reading Progress": "読書進捗",
|
||||
"Page Margin": "ページマージン",
|
||||
"Remove Bookmark": "ブックマークを削除",
|
||||
"Add Bookmark": "ブックマークを追加",
|
||||
"Books Content": "本の内容",
|
||||
"Jump to Location": "位置にジャンプ",
|
||||
"Unpin Notebook": "ノートブックのピン留めを解除",
|
||||
"Pin Notebook": "ノートブックをピン留め",
|
||||
"Hide Search Bar": "検索バーを隠す",
|
||||
"Show Search Bar": "検索バーを表示",
|
||||
"On {{current}} of {{total}} page": "ページ {{current}} / {{total}}",
|
||||
"Section Title": "セクションタイトル",
|
||||
"Decrease": "減少",
|
||||
"Increase": "増加",
|
||||
"Settings Panels": "設定パネル",
|
||||
"Settings": "設定",
|
||||
"Unpin Sidebar": "サイドバーのピン留めを解除",
|
||||
"Pin Sidebar": "サイドバーをピン留め",
|
||||
"Toggle Sidebar": "サイドバーの切り替え",
|
||||
"Toggle Translation": "翻訳の切り替え",
|
||||
"Translation Disabled": "翻訳が無効化されました",
|
||||
"Minimize": "最小化",
|
||||
"Maximize or Restore": "最大化または復元",
|
||||
"Exit Parallel Read": "並行読書を終了",
|
||||
"Enter Parallel Read": "並行読書に入る",
|
||||
"Zoom Level": "ズームレベル",
|
||||
"Zoom Out": "ズームアウト",
|
||||
"Reset Zoom": "ズームリセット",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Mode": "ズームモード",
|
||||
"Single Page": "シングルページ",
|
||||
"Auto Spread": "自動スプレッド",
|
||||
"Fit Page": "ページに合わせる",
|
||||
"Fit Width": "幅に合わせる",
|
||||
"Failed to select directory": "ディレクトリの選択に失敗しました",
|
||||
"The new data directory must be different from the current one.": "新しいデータディレクトリは現在のものと異なる必要があります。",
|
||||
"Migration failed: {{error}}": "移行に失敗しました: {{error}}",
|
||||
"Change Data Location": "データの場所を変更",
|
||||
"Current Data Location": "現在のデータの場所",
|
||||
"Total size: {{size}}": "合計サイズ: {{size}}",
|
||||
"Calculating file info...": "ファイル情報を計算中...",
|
||||
"New Data Location": "新しいデータの場所",
|
||||
"Choose Different Folder": "別のフォルダーを選択",
|
||||
"Choose New Folder": "新しいフォルダーを選択",
|
||||
"Migrating data...": "データを移行中...",
|
||||
"Copying: {{file}}": "コピー中: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} / {{total}} ファイル",
|
||||
"Migration completed successfully!": "移行が成功しました!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "データは新しい場所に移動されました。プロセスを完了するにはアプリケーションを再起動してください。",
|
||||
"Migration failed": "移行に失敗しました",
|
||||
"Important Notice": "重要なお知らせ",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "これにより、すべてのアプリデータが新しい場所に移動されます。宛先に十分な空き容量があることを確認してください。",
|
||||
"Restart App": "アプリを再起動",
|
||||
"Start Migration": "移行を開始",
|
||||
"Advanced Settings": "詳細設定",
|
||||
"File count: {{size}}": "ファイル数: {{size}}",
|
||||
"Background Read Aloud": "バックグラウンド読み上げ",
|
||||
"Ready to read aloud": "読み上げの準備ができました",
|
||||
"Read Aloud": "読み上げ",
|
||||
"Screen Brightness": "画面の明るさ",
|
||||
"Background Image": "背景画像",
|
||||
"Import Image": "画像をインポート",
|
||||
"Opacity": "不透明度",
|
||||
"Size": "サイズ",
|
||||
"Cover": "カバー",
|
||||
"Contain": "含む",
|
||||
"{{number}} pages left in chapter": "<1>章に</1><0>{{number}}</0><1>ページ残り</1>",
|
||||
"Device": "デバイス",
|
||||
"E-Ink Mode": "E-Inkモード",
|
||||
"Highlight Colors": "ハイライトカラー",
|
||||
"Pagination": "ページネーション",
|
||||
"Disable Double Tap": "ダブルタップを無効にする",
|
||||
"Tap to Paginate": "タップしてページを切り替える",
|
||||
"Click to Paginate": "クリックしてページを切り替える",
|
||||
"Tap Both Sides": "両側をタップ",
|
||||
"Click Both Sides": "両側をクリック",
|
||||
"Swap Tap Sides": "タップサイドを入れ替える",
|
||||
"Swap Click Sides": "クリックサイドを入れ替える",
|
||||
"Source and Translated": "原文と翻訳",
|
||||
"Translated Only": "翻訳のみ",
|
||||
"Source Only": "原文のみ",
|
||||
"TTS Text": "TTSテキスト",
|
||||
"The book file is corrupted": "本のファイルが破損しています",
|
||||
"The book file is empty": "本のファイルが空です",
|
||||
"Failed to open the book file": "本のファイルを開くことができませんでした",
|
||||
"On-Demand Purchase": "オンデマンド購入",
|
||||
"Full Customization": "フルカスタマイズ",
|
||||
"Lifetime Plan": "ライフタイムプラン",
|
||||
"One-Time Payment": "ワンタイムペイメント",
|
||||
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "すべてのデバイスで特定の機能にライフタイムアクセスを楽しむために、1回の支払いを行います。必要なときにのみ特定の機能やサービスを購入します。",
|
||||
"Expand Cloud Sync Storage": "クラウド同期ストレージを拡張",
|
||||
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "1回の購入でクラウドストレージを永遠に拡張します。追加の購入ごとに、さらに多くのスペースが追加されます。",
|
||||
"Unlock All Customization Options": "すべてのカスタマイズオプションを解除",
|
||||
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "追加のテーマ、フォント、レイアウトオプション、読み上げ、翻訳者、クラウドストレージサービスを解除します。",
|
||||
"Purchase Successful!": "購入成功!",
|
||||
"lifetime": "ライフタイム",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "ご購入ありがとうございます!お支払いが正常に処理されました。",
|
||||
"Order ID:": "購入ID:",
|
||||
"TTS Highlighting": "TTSハイライト",
|
||||
"Style": "スタイル",
|
||||
"Underline": "下線",
|
||||
"Strikethrough": "取り消し線",
|
||||
"Squiggly": "波線",
|
||||
"Outline": "アウトライン",
|
||||
"Save Current Color": "現在の色を保存",
|
||||
"Quick Colors": "クイックカラー",
|
||||
"Highlighter": "蛍光ペン",
|
||||
"Save Book Cover": "本のカバーを保存",
|
||||
"Auto-save last book cover": "最後の本のカバーを自動保存",
|
||||
"Back": "戻る",
|
||||
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "確認メールを送信しました。変更を確認するために、古いメールアドレスと新しいメールアドレスを確認してください。",
|
||||
"Failed to update email": "メールの更新に失敗しました",
|
||||
"New Email": "新しいメールアドレス",
|
||||
"Your new email": "あなたの新しいメールアドレス",
|
||||
"Updating email ...": "メールを更新しています ...",
|
||||
"Update email": "メールを更新",
|
||||
"Current email": "現在のメールアドレス",
|
||||
"Update Email": "メールを更新",
|
||||
"All": "すべて",
|
||||
"Unable to open book": "本を開くことができません",
|
||||
"Punctuation": "句読点",
|
||||
"Replace Quotation Marks": "引用符を置き換える",
|
||||
"Enabled only in vertical layout.": "縦書きレイアウトでのみ有効です。",
|
||||
"No Conversion": "変換なし",
|
||||
"Simplified to Traditional": "簡体字 → 繁体字",
|
||||
"Traditional to Simplified": "繁体字 → 簡体字",
|
||||
"Simplified to Traditional (Taiwan)": "簡体字 → 繁体字(台湾)",
|
||||
"Simplified to Traditional (Hong Kong)": "簡体字 → 繁体字(香港)",
|
||||
"Simplified to Traditional (Taiwan), with phrases": "簡体字 → 繁体字(台湾)• フレーズ",
|
||||
"Traditional (Taiwan) to Simplified": "繁体字(台湾) → 簡体字",
|
||||
"Traditional (Hong Kong) to Simplified": "繁体字(香港) → 簡体字",
|
||||
"Traditional (Taiwan) to Simplified, with phrases": "繁体字(台湾) → 簡体字 • フレーズ",
|
||||
"Convert Simplified and Traditional Chinese": "簡体字/繁体字変換",
|
||||
"Convert Mode": "変換モード",
|
||||
"Failed to auto-save book cover for lock screen: {{error}}": "ロック画面の本のカバーの自動保存に失敗しました:{{error}}",
|
||||
"Download from Cloud": "クラウドからダウンロード",
|
||||
"Upload to Cloud": "クラウドにアップロード",
|
||||
"Clear Custom Fonts": "カスタムフォントをクリア",
|
||||
"Columns": "列",
|
||||
"OPDS Catalogs": "OPDSカタログ",
|
||||
"Adding LAN addresses is not supported in the web app version.": "Webアプリ版ではLANアドレスの追加はサポートされていません。",
|
||||
"Invalid OPDS catalog. Please check the URL.": "無効なOPDSカタログです。URLを確認してください。",
|
||||
"Browse and download books from online catalogs": "オンラインカタログから本を閲覧・ダウンロード",
|
||||
"My Catalogs": "マイカタログ",
|
||||
"Add Catalog": "カタログを追加",
|
||||
"No catalogs yet": "まだカタログはありません",
|
||||
"Add your first OPDS catalog to start browsing books": "本を閲覧するには、最初のOPDSカタログを追加してください。",
|
||||
"Add Your First Catalog": "最初のカタログを追加",
|
||||
"Browse": "閲覧",
|
||||
"Popular Catalogs": "人気カタログ",
|
||||
"Add": "追加",
|
||||
"Add OPDS Catalog": "OPDSカタログを追加",
|
||||
"Catalog Name": "カタログ名",
|
||||
"My Calibre Library": "私のCalibreライブラリ",
|
||||
"OPDS URL": "OPDS URL",
|
||||
"Username (optional)": "ユーザー名(任意)",
|
||||
"Password (optional)": "パスワード(任意)",
|
||||
"Description (optional)": "説明(任意)",
|
||||
"A brief description of this catalog": "このカタログの簡単な説明",
|
||||
"Validating...": "検証中...",
|
||||
"View All": "すべて表示",
|
||||
"Forward": "進む",
|
||||
"Home": "ホーム",
|
||||
"{{count}} items_other": "{{count}} 件",
|
||||
"Download completed": "ダウンロード完了",
|
||||
"Download failed": "ダウンロード失敗",
|
||||
"Open Access": "オープンアクセス",
|
||||
"Borrow": "借りる",
|
||||
"Buy": "購入",
|
||||
"Subscribe": "購読",
|
||||
"Sample": "サンプル",
|
||||
"Download": "ダウンロード",
|
||||
"Open & Read": "開いて読む",
|
||||
"Tags": "タグ",
|
||||
"Tag": "タグ",
|
||||
"First": "最初",
|
||||
"Previous": "前へ",
|
||||
"Next": "次へ",
|
||||
"Last": "最後",
|
||||
"Cannot Load Page": "ページを読み込めません",
|
||||
"An error occurred": "エラーが発生しました",
|
||||
"Online Library": "オンラインライブラリ",
|
||||
"URL must start with http:// or https://": "URLはhttp://またはhttps://で始まる必要があります",
|
||||
"Title, Author, Tag, etc...": "タイトル、著者、タグなど...",
|
||||
"Query": "クエリ",
|
||||
"Subject": "件名",
|
||||
"Enter {{terms}}": "{{terms}}を入力してください",
|
||||
"No search results found": "検索結果が見つかりません",
|
||||
"Failed to load OPDS feed: {{status}} {{statusText}}": "OPDSフィードの読み込みに失敗しました: {{status}} {{statusText}}",
|
||||
"Search in {{title}}": "{{title}}内を検索",
|
||||
"Manage Storage": "ストレージ管理",
|
||||
"Failed to load files": "ファイルの読み込みに失敗しました",
|
||||
"Deleted {{count}} file(s)_other": "{{count}} 件のファイルを削除しました",
|
||||
"Failed to delete {{count}} file(s)_other": "{{count}} 件のファイルの削除に失敗しました",
|
||||
"Failed to delete files": "ファイルの削除に失敗しました",
|
||||
"Total Files": "ファイル合計",
|
||||
"Total Size": "合計サイズ",
|
||||
"Quota": "容量制限",
|
||||
"Used": "使用済み",
|
||||
"Files": "ファイル",
|
||||
"Search files...": "ファイルを検索...",
|
||||
"Newest First": "新しい順",
|
||||
"Oldest First": "古い順",
|
||||
"Largest First": "大きい順",
|
||||
"Smallest First": "小さい順",
|
||||
"Name A-Z": "名前 A-Z",
|
||||
"Name Z-A": "名前 Z-A",
|
||||
"{{count}} selected_other": "{{count}} 件選択済み",
|
||||
"Delete Selected": "選択した項目を削除",
|
||||
"Created": "作成日",
|
||||
"No files found": "ファイルが見つかりません",
|
||||
"No files uploaded yet": "まだファイルがアップロードされていません",
|
||||
"files": "ファイル",
|
||||
"Page {{current}} of {{total}}": "ページ {{current}} / {{total}}",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "選択した {{count}} 件のファイルを削除してもよろしいですか?",
|
||||
"Cloud Storage Usage": "クラウドストレージ使用量",
|
||||
"Rename Group": "グループ名を変更",
|
||||
"From Directory": "ディレクトリから",
|
||||
"Successfully imported {{count}} book(s)_other": "成功裏に{{count}}冊の本をインポートしました",
|
||||
"Count": "件数",
|
||||
"Start Page": "開始ページ",
|
||||
"Search in OPDS Catalog...": "OPDSカタログ内を検索...",
|
||||
"Please log in to use advanced TTS features": "高度なTTS機能を使用するにはログインしてください",
|
||||
"Word limit of 30 words exceeded.": "30語の上限を超えています。",
|
||||
"Proofread": "校正",
|
||||
"Current selection": "現在の選択",
|
||||
"All occurrences in this book": "この本内のすべての出現箇所",
|
||||
"All occurrences in your library": "ライブラリ内のすべての出現箇所",
|
||||
"Selected text:": "選択されたテキスト:",
|
||||
"Replace with:": "置換後:",
|
||||
"Enter text...": "テキストを入力…",
|
||||
"Case sensitive:": "大文字と小文字を区別:",
|
||||
"Scope:": "適用範囲:",
|
||||
"Selection": "選択",
|
||||
"Library": "ライブラリ",
|
||||
"Yes": "はい",
|
||||
"No": "いいえ",
|
||||
"Proofread Replacement Rules": "校正置換ルール",
|
||||
"Selected Text Rules": "選択テキストのルール",
|
||||
"No selected text replacement rules": "選択されたテキストの置換ルールはありません",
|
||||
"Book Specific Rules": "書籍別ルール",
|
||||
"No book-level replacement rules": "書籍レベルの置換ルールはありません",
|
||||
"Disable Quick Action": "クイックアクションを無効にする",
|
||||
"Enable Quick Action on Selection": "選択時にクイックアクションを有効にする",
|
||||
"None": "なし",
|
||||
"Annotation Tools": "注釈ツール",
|
||||
"Enable Quick Actions": "クイックアクションを有効にする",
|
||||
"Quick Action": "クイックアクション",
|
||||
"Copy to Notebook": "ノートにコピー",
|
||||
"Copy text after selection": "選択後のテキストをコピー",
|
||||
"Highlight text after selection": "選択後のテキストをハイライト",
|
||||
"Annotate text after selection": "選択後のテキストに注釈を付ける",
|
||||
"Search text after selection": "選択後のテキストを検索",
|
||||
"Look up text in dictionary after selection": "選択後のテキストを辞書で調べる",
|
||||
"Look up text in Wikipedia after selection": "選択後のテキストをWikipediaで調べる",
|
||||
"Translate text after selection": "選択後のテキストを翻訳する",
|
||||
"Read text aloud after selection": "選択後のテキストを音読する",
|
||||
"Proofread text after selection": "選択後のテキストを校正する",
|
||||
"{{activeCount}} active, {{pendingCount}} pending": "{{activeCount}} 進行中、{{pendingCount}} 待機中",
|
||||
"{{failedCount}} failed": "{{failedCount}} 失敗",
|
||||
"Waiting...": "待機中...",
|
||||
"Failed": "失敗",
|
||||
"Completed": "完了",
|
||||
"Cancelled": "キャンセル",
|
||||
"Retry": "再試行",
|
||||
"Active": "進行中",
|
||||
"Transfer Queue": "転送キュー",
|
||||
"Upload All": "すべてアップロード",
|
||||
"Download All": "すべてダウンロード",
|
||||
"Resume Transfers": "転送を再開",
|
||||
"Pause Transfers": "転送を一時停止",
|
||||
"Pending": "待機中",
|
||||
"No transfers": "転送なし",
|
||||
"Retry All": "すべて再試行",
|
||||
"Clear Completed": "完了を消去",
|
||||
"Clear Failed": "失敗を消去",
|
||||
"Upload queued: {{title}}": "アップロードをキューに追加: {{title}}",
|
||||
"Download queued: {{title}}": "ダウンロードをキューに追加: {{title}}",
|
||||
"Book not found in library": "ライブラリに本が見つかりません",
|
||||
"Unknown error": "不明なエラー",
|
||||
"Please log in to continue": "続行するにはログインしてください",
|
||||
"Cloud File Transfers": "クラウドファイル転送",
|
||||
"Show Search Results": "検索結果を表示",
|
||||
"Search results for '{{term}}'": "「{{term}}」を含む結果",
|
||||
"Close Search": "検索を閉じる",
|
||||
"Previous Result": "前の結果",
|
||||
"Next Result": "次の結果",
|
||||
"Bookmarks": "ブックマーク",
|
||||
"Annotations": "注釈",
|
||||
"Show Results": "結果を表示",
|
||||
"Clear search": "検索をクリア",
|
||||
"Clear search history": "検索履歴をクリア",
|
||||
"Tap to Toggle Footer": "タップでフッターを切り替え",
|
||||
"Show Current Time": "現在時刻を表示",
|
||||
"Use 24 Hour Clock": "24時間表示を使用",
|
||||
"Show Current Battery Status": "バッテリー残量を表示",
|
||||
"Exported successfully": "エクスポート成功",
|
||||
"Book exported successfully.": "書籍のエクスポートに成功しました。",
|
||||
"Failed to export the book.": "書籍のエクスポートに失敗しました。",
|
||||
"Export Book": "書籍をエクスポート",
|
||||
"Whole word:": "単語全体:",
|
||||
"Error": "エラー",
|
||||
"Unable to load the article. Try searching directly on {{link}}.": "記事を読み込めません。{{link}}で直接検索してみてください。",
|
||||
"Unable to load the word. Try searching directly on {{link}}.": "単語を読み込めません。{{link}}で直接検索してみてください。",
|
||||
"Date Published": "出版日",
|
||||
"Only for TTS:": "TTSのみ:",
|
||||
"Uploaded": "アップロード済み",
|
||||
"Downloaded": "ダウンロード済み",
|
||||
"Deleted": "削除済み",
|
||||
"Note:": "ノート:",
|
||||
"Time:": "時刻:",
|
||||
"Format Options": "形式オプション",
|
||||
"Export Date": "エクスポート日",
|
||||
"Chapter Titles": "章タイトル",
|
||||
"Chapter Separator": "章区切り",
|
||||
"Highlights": "ハイライト",
|
||||
"Note Date": "ノート日付",
|
||||
"Advanced": "詳細設定",
|
||||
"Hide": "非表示",
|
||||
"Show": "表示",
|
||||
"Use Custom Template": "カスタムテンプレート使用",
|
||||
"Export Template": "エクスポートテンプレート",
|
||||
"Template Syntax:": "テンプレート構文:",
|
||||
"Insert value": "値を挿入",
|
||||
"Format date (locale)": "日付形式(ロケール)",
|
||||
"Format date (custom)": "日付形式(カスタム)",
|
||||
"Conditional": "条件分岐",
|
||||
"Loop": "ループ",
|
||||
"Available Variables:": "利用可能な変数:",
|
||||
"Book title": "本のタイトル",
|
||||
"Book author": "著者",
|
||||
"Export date": "エクスポート日",
|
||||
"Array of chapters": "章の配列",
|
||||
"Chapter title": "章タイトル",
|
||||
"Array of annotations": "注釈の配列",
|
||||
"Highlighted text": "ハイライトテキスト",
|
||||
"Annotation note": "注釈ノート",
|
||||
"Date Format Tokens:": "日付形式トークン:",
|
||||
"Year (4 digits)": "年(4桁)",
|
||||
"Month (01-12)": "月(01-12)",
|
||||
"Day (01-31)": "日(01-31)",
|
||||
"Hour (00-23)": "時(00-23)",
|
||||
"Minute (00-59)": "分(00-59)",
|
||||
"Second (00-59)": "秒(00-59)",
|
||||
"Show Source": "ソース表示",
|
||||
"No content to preview": "プレビューするコンテンツがありません",
|
||||
"Export": "エクスポート",
|
||||
"Set Timeout": "タイムアウト設定",
|
||||
"Select Voice": "音声選択",
|
||||
"Toggle Sticky Bottom TTS Bar": "TTSバー固定切替",
|
||||
"Display what I'm reading on Discord": "読書中の本をDiscordに表示",
|
||||
"Show on Discord": "Discordに表示",
|
||||
"Instant {{action}}": "インスタント{{action}}",
|
||||
"Instant {{action}} Disabled": "インスタント{{action}}を無効化",
|
||||
"Annotation": "注釈",
|
||||
"Reset Template": "テンプレートをリセット",
|
||||
"Annotation style": "注釈スタイル",
|
||||
"Annotation color": "注釈の色",
|
||||
"Annotation time": "注釈時間",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "この本を再インデックスしてもよろしいですか?",
|
||||
"Enable AI in Settings": "設定でAIを有効にする",
|
||||
"Index This Book": "この本をインデックス",
|
||||
"Enable AI search and chat for this book": "この本のAI検索とチャットを有効にする",
|
||||
"Start Indexing": "インデックス開始",
|
||||
"Indexing book...": "本をインデックス中...",
|
||||
"Preparing...": "準備中...",
|
||||
"Delete this conversation?": "この会話を削除しますか?",
|
||||
"No conversations yet": "まだ会話がありません",
|
||||
"Start a new chat to ask questions about this book": "この本について質問するには新しいチャットを開始してください",
|
||||
"Rename": "名前を変更",
|
||||
"New Chat": "新しいチャット",
|
||||
"Chat": "チャット",
|
||||
"Please enter a model ID": "モデルIDを入力してください",
|
||||
"Model not available or invalid": "モデルが利用できないか無効です",
|
||||
"Failed to validate model": "モデルの検証に失敗しました",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollamaに接続できませんでした。実行中ですか?",
|
||||
"Invalid API key or connection failed": "APIキーが無効か接続に失敗しました",
|
||||
"Connection failed": "接続に失敗しました",
|
||||
"AI Assistant": "AIアシスタント",
|
||||
"Enable AI Assistant": "AIアシスタントを有効にする",
|
||||
"Provider": "プロバイダー",
|
||||
"Ollama (Local)": "Ollama(ローカル)",
|
||||
"AI Gateway (Cloud)": "AIゲートウェイ(クラウド)",
|
||||
"Ollama Configuration": "Ollama設定",
|
||||
"Refresh Models": "モデルを更新",
|
||||
"AI Model": "AIモデル",
|
||||
"No models detected": "モデルが検出されませんでした",
|
||||
"AI Gateway Configuration": "AIゲートウェイ設定",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "高品質で経済的なAIモデルからお選びください。下の「カスタムモデル」を選択して独自のモデルを使用することもできます。",
|
||||
"API Key": "APIキー",
|
||||
"Get Key": "キーを取得",
|
||||
"Model": "モデル",
|
||||
"Custom Model...": "カスタムモデル...",
|
||||
"Custom Model ID": "カスタムモデルID",
|
||||
"Validate": "検証",
|
||||
"Model available": "モデルが利用可能",
|
||||
"Connection": "接続",
|
||||
"Test Connection": "接続をテスト",
|
||||
"Connected": "接続済み",
|
||||
"Custom Colors": "カスタムカラー",
|
||||
"Color E-Ink Mode": "カラー電子ペーパーモード",
|
||||
"Reading Ruler": "リーディングルーラー",
|
||||
"Enable Reading Ruler": "リーディングルーラーを有効にする",
|
||||
"Lines to Highlight": "ハイライトする行数",
|
||||
"Ruler Color": "ルーラーの色",
|
||||
"Command Palette": "コマンドパレット",
|
||||
"Search settings and actions...": "設定とアクションを検索...",
|
||||
"No results found for": "の結果が見つかりませんでした",
|
||||
"Type to search settings and actions": "入力して設定とアクションを検索",
|
||||
"Recent": "最近使った項目",
|
||||
"navigate": "ナビゲート",
|
||||
"select": "選択",
|
||||
"close": "閉じる",
|
||||
"Search Settings": "設定を検索",
|
||||
"Page Margins": "ページ余白",
|
||||
"AI Provider": "AIプロバイダー",
|
||||
"Ollama URL": "OllamaのURL",
|
||||
"Ollama Model": "Ollamaモデル",
|
||||
"AI Gateway Model": "AI Gatewayモデル",
|
||||
"Actions": "アクション",
|
||||
"Navigation": "ナビゲーション",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} 冊の本のステータスを設定",
|
||||
"Mark as Unread": "未読にする",
|
||||
"Mark as Finished": "読了にする",
|
||||
"Finished": "読了",
|
||||
"Unread": "未読",
|
||||
"Clear Status": "ステータスをクリア",
|
||||
"Status": "ステータス",
|
||||
"Loading": "読み込み中",
|
||||
"Exit Paragraph Mode": "段落モードを終了",
|
||||
"Paragraph Mode": "段落モード",
|
||||
"Embedding Model": "埋め込みモデル",
|
||||
"{{count}} book(s) synced_other": "{{count}} 冊の本が同期されました",
|
||||
"Unable to start RSVP": "RSVPを起動できません",
|
||||
"RSVP not supported for PDF": "PDFはRSVPに対応していません",
|
||||
"Select Chapter": "章を選択",
|
||||
"Context": "コンテキスト",
|
||||
"Ready": "準備完了",
|
||||
"Chapter Progress": "章の進捗",
|
||||
"words": "単語",
|
||||
"{{time}} left": "残り {{time}}",
|
||||
"Reading progress": "読書の進捗",
|
||||
"Click to seek": "クリックしてシーク",
|
||||
"Skip back 15 words": "15単語戻る",
|
||||
"Back 15 words (Shift+Left)": "15単語戻る (Shift+左)",
|
||||
"Pause (Space)": "一時停止 (Space)",
|
||||
"Play (Space)": "再生 (Space)",
|
||||
"Skip forward 15 words": "15単語進む",
|
||||
"Forward 15 words (Shift+Right)": "15単語進む (Shift+右)",
|
||||
"Decrease speed": "速度を下げる",
|
||||
"Slower (Left/Down)": "低速 (左/下)",
|
||||
"Increase speed": "速度を上げる",
|
||||
"Faster (Right/Up)": "高速 (右/上)",
|
||||
"Start RSVP Reading": "RSVP読書を開始",
|
||||
"Choose where to start reading": "読書を開始する場所を選択",
|
||||
"From Chapter Start": "章の最初から",
|
||||
"Start reading from the beginning of the chapter": "この章の最初から読み直す",
|
||||
"Resume": "再開",
|
||||
"Continue from where you left off": "中断した場所から再開する",
|
||||
"From Current Page": "現在のページから",
|
||||
"Start from where you are currently reading": "現在読んでいる場所から開始する",
|
||||
"From Selection": "選択範囲から",
|
||||
"Speed Reading Mode": "速読モード",
|
||||
"Scroll left": "左にスクロール",
|
||||
"Scroll right": "右にスクロール",
|
||||
"Library Sync Progress": "ライブラリ同期の進捗",
|
||||
"Back to library": "ライブラリに戻る",
|
||||
"Group by...": "グループ化...",
|
||||
"Export as Plain Text": "プレーンテキストとして書き出し",
|
||||
"Export as Markdown": "Markdownとして書き出し",
|
||||
"Show Page Navigation Buttons": "ナビゲーションボタンを表示",
|
||||
"Page {{number}}": "{{number}} ページ",
|
||||
"highlight": "ハイライト",
|
||||
"underline": "下線",
|
||||
"squiggly": "波線",
|
||||
"red": "赤",
|
||||
"violet": "すみれ色",
|
||||
"blue": "青",
|
||||
"green": "緑",
|
||||
"yellow": "黄色",
|
||||
"Select {{style}} style": "スタイル {{style}} を選択",
|
||||
"Select {{color}} color": "色 {{color}} を選択",
|
||||
"Close Book": "本を閉じる",
|
||||
"Speed Reading": "速読",
|
||||
"Close Speed Reading": "速読を閉じる",
|
||||
"Authors": "著者",
|
||||
"Books": "書籍",
|
||||
"Groups": "グループ",
|
||||
"Back to TTS Location": "TTSの位置に戻る",
|
||||
"Metadata": "メタデータ",
|
||||
"Image viewer": "画像ビューア",
|
||||
"Previous Image": "前の画像",
|
||||
"Next Image": "次の画像",
|
||||
"Zoomed": "ズーム済み",
|
||||
"Zoom level": "ズームレベル",
|
||||
"Table viewer": "テーブルビューア",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise に接続できません。ネットワーク接続を確認してください。",
|
||||
"Invalid Readwise access token": "無効な Readwise アクセストークン",
|
||||
"Disconnected from Readwise": "Readwise から切断されました",
|
||||
"Never": "一度もなし",
|
||||
"Readwise Settings": "Readwise 設定",
|
||||
"Connected to Readwise": "Readwise に接続済み",
|
||||
"Last synced: {{time}}": "最終同期:{{time}}",
|
||||
"Sync Enabled": "同期有効",
|
||||
"Disconnect": "切断",
|
||||
"Connect your Readwise account to sync highlights.": "ハイライトを同期するには Readwise アカウントを接続してください。",
|
||||
"Get your access token at": "アクセストークンの取得先:",
|
||||
"Access Token": "アクセストークン",
|
||||
"Paste your Readwise access token": "Readwise アクセストークンを貼り付けてください",
|
||||
"Config": "設定",
|
||||
"Readwise Sync": "Readwise 同期",
|
||||
"Push Highlights": "ハイライトをプッシュ",
|
||||
"Highlights synced to Readwise": "ハイライトが Readwise に同期されました",
|
||||
"Readwise sync failed: no internet connection": "Readwise 同期失敗:インターネット接続がありません",
|
||||
"Readwise sync failed: {{error}}": "Readwise 同期失敗:{{error}}",
|
||||
"System Screen Brightness": "システムの画面の明るさ",
|
||||
"Page:": "ページ:",
|
||||
"Page: {{number}}": "ページ: {{number}}",
|
||||
"Annotation page number": "注釈のページ番号",
|
||||
"Translating...": "翻訳中...",
|
||||
"Show Battery Percentage": "バッテリー残量を表示",
|
||||
"Hide Scrollbar": "スクロールバーを非表示",
|
||||
"Skip to last reading position": "最後の読書位置へ移動",
|
||||
"Show context": "コンテキストを表示",
|
||||
"Hide context": "コンテキストを非表示",
|
||||
"Decrease font size": "文字サイズを縮小",
|
||||
"Increase font size": "文字サイズを拡大",
|
||||
"Focus": "フォーカス",
|
||||
"Theme color": "テーマカラー",
|
||||
"Import failed": "インポートに失敗しました",
|
||||
"Available Formatters:": "利用可能なフォーマッター:",
|
||||
"Format date": "日付のフォーマット",
|
||||
"Markdown block quote (> per line)": "Markdown引用ブロック (行ごとに >)",
|
||||
"Newlines to <br>": "改行を <br> に変換",
|
||||
"Change case": "大文字/小文字の変更",
|
||||
"Trim whitespace": "空白を除去",
|
||||
"Truncate to n characters": "n文字に切り詰め",
|
||||
"Replace text": "テキストを置換",
|
||||
"Fallback value": "フォールバック値",
|
||||
"Get length": "長さを取得",
|
||||
"First/last element": "最初/最後の要素",
|
||||
"Join array": "配列を結合",
|
||||
"Backup failed: {{error}}": "バックアップに失敗しました: {{error}}",
|
||||
"Select Backup": "バックアップを選択",
|
||||
"Restore failed: {{error}}": "復元に失敗しました: {{error}}",
|
||||
"Backup & Restore": "バックアップと復元",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "ライブラリのバックアップを作成するか、以前のバックアップから復元します。復元は現在のライブラリとマージされます。",
|
||||
"Backup Library": "ライブラリをバックアップ",
|
||||
"Restore Library": "ライブラリを復元",
|
||||
"Creating backup...": "バックアップを作成中...",
|
||||
"Restoring library...": "ライブラリを復元中...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 件",
|
||||
"Backup completed successfully!": "バックアップが完了しました!",
|
||||
"Restore completed successfully!": "復元が完了しました!",
|
||||
"Your library has been saved to the selected location.": "ライブラリが選択した場所に保存されました。",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}冊追加、{{updated}}冊更新されました。",
|
||||
"Operation failed": "操作に失敗しました",
|
||||
"Loading library...": "ライブラリを読み込み中...",
|
||||
"{{count}} books refreshed_other": "{{count}}冊のメタデータを更新しました",
|
||||
"Failed to refresh metadata": "メタデータの更新に失敗しました",
|
||||
"Refresh Metadata": "メタデータを更新"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"Auto Mode": "자동 모드",
|
||||
"Behavior": "동작",
|
||||
"Book": "책",
|
||||
"Book Cover": "책 표지",
|
||||
"Bookmark": "북마크",
|
||||
"Cancel": "취소",
|
||||
"Chapter": "챕터",
|
||||
@@ -18,7 +17,6 @@
|
||||
"Confirm Deletion": "삭제 확인",
|
||||
"Copied to notebook": "노트북에 복사됨",
|
||||
"Copy": "복사",
|
||||
"Custom CSS": "사용자 정의 CSS",
|
||||
"Dark Mode": "다크 모드",
|
||||
"Default": "기본값",
|
||||
"Default Font": "기본 글꼴",
|
||||
@@ -28,7 +26,6 @@
|
||||
"Dictionary": "사전",
|
||||
"Download Readest": "Readest 다운로드",
|
||||
"Edit": "편집",
|
||||
"Enter your custom CSS here...": "여기에 사용자 정의 CSS를 입력하세요...",
|
||||
"Excerpts": "발췌",
|
||||
"Failed to import book(s): {{filenames}}": "책 가져오기 실패: {{filenames}}",
|
||||
"Fast": "빠름",
|
||||
@@ -48,7 +45,6 @@
|
||||
"Horizontal Direction": "수평 방향",
|
||||
"Hyphenation": "하이픈 넣기",
|
||||
"Import Books": "책 가져오기",
|
||||
"Language:": "언어:",
|
||||
"Layout": "레이아웃",
|
||||
"Light Mode": "라이트 모드",
|
||||
"Loading...": "로딩 중...",
|
||||
@@ -70,8 +66,8 @@
|
||||
"Paging Animation": "페이지 넘김 애니메이션",
|
||||
"Paragraph": "단락",
|
||||
"Parallel Read": "병렬 읽기",
|
||||
"Published:": "출판일:",
|
||||
"Publisher:": "출판사:",
|
||||
"Published": "출판일",
|
||||
"Publisher": "출판사",
|
||||
"Reading Progress Synced": "읽기 진행 상황 동기화됨",
|
||||
"Reload Page": "페이지 새로고침",
|
||||
"Reveal in File Explorer": "파일 탐색기에서 표시",
|
||||
@@ -85,7 +81,6 @@
|
||||
"Search...": "검색...",
|
||||
"Select Book": "책 선택",
|
||||
"Select Books": "책 선택",
|
||||
"Select Multiple Books": "여러 책 선택",
|
||||
"Sepia": "세피아",
|
||||
"Serif Font": "세리프체",
|
||||
"Show Book Details": "책 세부 정보 표시",
|
||||
@@ -96,23 +91,21 @@
|
||||
"Slow": "느림",
|
||||
"Solarized": "솔라라이즈드",
|
||||
"Speak": "말하기",
|
||||
"Subjects:": "주제:",
|
||||
"Subjects": "주제",
|
||||
"System Fonts": "시스템 글꼴",
|
||||
"TTS not supported in this device": "이 장치에서 TTS가 지원되지 않음",
|
||||
"Theme Color": "테마 색상",
|
||||
"Theme Mode": "테마 모드",
|
||||
"Translate": "번역",
|
||||
"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.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
|
||||
"Wikipedia": "위키피디아",
|
||||
"Writing Mode": "글쓰기 모드",
|
||||
"Your Library": "내 서재",
|
||||
"TTS not supported for PDF": "PDF에서 TTS가 지원되지 않음",
|
||||
"Override Book Font": "책 글꼴 덮어쓰기",
|
||||
"Apply to All Books": "모든 책에 적용",
|
||||
"Apply to This Book": "이 책에 적용",
|
||||
@@ -122,14 +115,13 @@
|
||||
"Book Details": "책 세부 정보",
|
||||
"From Local File": "로컬 파일에서",
|
||||
"TOC": "목차",
|
||||
"Table of Contents": "목차",
|
||||
"Book uploaded: {{title}}": "책 업로드됨: {{title}}",
|
||||
"Failed to upload book: {{title}}": "책 업로드 실패: {{title}}",
|
||||
"Book downloaded: {{title}}": "책 다운로드됨: {{title}}",
|
||||
"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 +153,7 @@
|
||||
"Create a Password": "비밀번호 만들기",
|
||||
"Sign up": "회원가입",
|
||||
"Signing up...": "회원가입 중...",
|
||||
"Don’t 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": "마법의 링크 이메일 보내기",
|
||||
@@ -180,40 +172,15 @@
|
||||
"Token": "토큰",
|
||||
"Your OTP token": "당신의 OTP 토큰",
|
||||
"Verify token": "토큰 확인",
|
||||
"Sign in with Google": "Google로 로그인",
|
||||
"Sign in with Apple": "Apple로 로그인",
|
||||
"Sign in with GitHub": "GitHub로 로그인",
|
||||
"Account": "계정",
|
||||
"Failed to delete user. Please try again later.": "사용자 삭제에 실패했습니다. 나중에 다시 시도해 주세요.",
|
||||
"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": "최대 열 너비",
|
||||
@@ -222,7 +189,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": "텍스트 색상",
|
||||
@@ -249,9 +216,9 @@
|
||||
"Auto Import on File Open": "파일 열 때 자동 가져오기",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "장 발견되지 않음.",
|
||||
"Failed to parse the EPUB file.": "EPUB 파일 구문 분석 실패.",
|
||||
"This book format is not supported.": "이 책 형식은 지원되지 않습니다.",
|
||||
"No chapters detected": "장 발견되지 않음",
|
||||
"Failed to parse the EPUB file": "EPUB 파일 구문 분석 실패",
|
||||
"This book format is not supported": "이 책 형식은 지원되지 않습니다",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "번역을 가져올 수 없습니다. 먼저 로그인하고 다시 시도하세요.",
|
||||
"Group": "그룹",
|
||||
"Always on Top": "항상 위에",
|
||||
@@ -264,13 +231,13 @@
|
||||
"Clear Search": "검색 지우기",
|
||||
"Header & Footer": "헤더 및 푸터",
|
||||
"Apply also in Scrolled Mode": "스크롤 모드에서도 적용",
|
||||
"A new version of Readest is Available!": "Readest의 새 버전이 있습니다!",
|
||||
"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:": "변경 로그:",
|
||||
"Changelog": "변경 로그",
|
||||
"Software Update": "소프트웨어 업데이트",
|
||||
"Title": "제목",
|
||||
"Date Read": "읽은 날짜",
|
||||
@@ -279,18 +246,15 @@
|
||||
"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)": "(당신이 좋아하는 대로, 2막)",
|
||||
"Link Color": "링크 색상",
|
||||
"Volume Keys for Page Flip": "페이지 넘김을 위한 볼륨 키",
|
||||
"Clicks for Page Flip": "페이지 넘김을 위한 클릭",
|
||||
"Swap Clicks Area": "클릭 영역 교환",
|
||||
"Screen": "화면",
|
||||
"Orientation": "방향",
|
||||
"Portrait": "세로",
|
||||
@@ -299,7 +263,7 @@
|
||||
"Checking for updates...": "업데이트 확인 중...",
|
||||
"Error checking for updates": "업데이트 확인 중 오류 발생",
|
||||
"Details": "세부정보",
|
||||
"File Size:": "파일 크기:",
|
||||
"File Size": "파일 크기",
|
||||
"Auto Detect": "자동 감지",
|
||||
"Next Section": "다음 섹션",
|
||||
"Previous Section": "이전 섹션",
|
||||
@@ -325,7 +289,6 @@
|
||||
"Disable Translation": "번역 비활성화",
|
||||
"Scroll": "스크롤",
|
||||
"Overlap Pixels": "겹치는 픽셀",
|
||||
"Click": "클릭",
|
||||
"System Language": "시스템 언어",
|
||||
"Security": "보안",
|
||||
"Allow JavaScript": "JavaScript 허용",
|
||||
@@ -338,14 +301,12 @@
|
||||
"Synced at {{time}}": "{{time}}에 동기화됨",
|
||||
"Never synced": "동기화된 적 없음",
|
||||
"Show Remaining Time": "남은 시간 표시",
|
||||
"Show Page Number": "페이지 번호 표시",
|
||||
"{{time}} min left in chapter": "{{time}}분 남음",
|
||||
"Override Book Color": "책 색상 덮어쓰기",
|
||||
"Login Required": "로그인 필요",
|
||||
"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 +321,773 @@
|
||||
"Right Margin (px)": "오른쪽 여백 (px)",
|
||||
"Left Margin (px)": "왼쪽 여백 (px)",
|
||||
"Column Gap (%)": "열 간격 (%)",
|
||||
"Always Show Status Bar": "상태 표시줄 항상 표시"
|
||||
"Always Show Status Bar": "상태 표시줄 항상 표시",
|
||||
"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": "<1>남은 페이지 </1><0>{{count}}</0><1>장</1>",
|
||||
"Show Remaining Pages": "남은 페이지 보기",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "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:": "금액:",
|
||||
"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}}",
|
||||
"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": "소설, 과학, 역사",
|
||||
"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": "더블 클릭 비활성화",
|
||||
"TTS not supported for this document": "TTS가 이 문서에서 지원되지 않습니다",
|
||||
"Reset Password": "비밀번호 재설정",
|
||||
"Show Reading Progress": "읽기 진행 상황 표시",
|
||||
"Reading Progress Style": "읽기 진행 상황 스타일",
|
||||
"Page Number": "페이지 번호",
|
||||
"Percentage": "백분율",
|
||||
"Deleted local copy of the book: {{title}}": "책의 로컬 사본이 삭제됨: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "책의 클라우드 백업 삭제에 실패했습니다: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "책의 로컬 사본 삭제에 실패했습니다: {{title}}",
|
||||
"Are you sure to delete the local copy of the selected book?": "선택한 책의 로컬 사본을 삭제하시겠습니까?",
|
||||
"Remove from Cloud & Device": "클라우드 및 장치에서 제거",
|
||||
"Remove from Cloud Only": "클라우드에서만 제거",
|
||||
"Remove from Device Only": "장치에서만 제거",
|
||||
"Disconnected": "연결 끊김",
|
||||
"KOReader Sync Settings": "KOReader Sync 설정",
|
||||
"Sync Strategy": "동기화 전략",
|
||||
"Ask on conflict": "충돌 시 묻기",
|
||||
"Always use latest": "항상 최신 버전 사용",
|
||||
"Send changes only": "변경 사항만 전송",
|
||||
"Receive changes only": "변경 사항만 수신",
|
||||
"Checksum Method": "체크섬 방법",
|
||||
"File Content (recommended)": "파일 내용 (권장)",
|
||||
"File Name": "파일 이름",
|
||||
"Device Name": "장치 이름",
|
||||
"Connect to your KOReader Sync server.": "KOReader Sync 서버에 연결합니다.",
|
||||
"Server URL": "서버 URL",
|
||||
"Username": "사용자 이름",
|
||||
"Your Username": "당신의 사용자 이름",
|
||||
"Password": "비밀번호",
|
||||
"Connect": "연결",
|
||||
"KOReader Sync": "KOReader Sync",
|
||||
"Sync Conflict": "동기화 충돌",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": " \"{{deviceName}}\"에서 읽기 진행 상황을 동기화하시겠습니까?",
|
||||
"another device": "다른 장치",
|
||||
"Local Progress": "로컬 진행 상황",
|
||||
"Remote Progress": "원격 진행 상황",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "페이지 {{page}} / {{total}} ({{percentage}}%)",
|
||||
"Current position": "현재 위치",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "대략 페이지 {{page}} / {{total}} ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "대략 {{percentage}}%",
|
||||
"Failed to connect": "연결 실패",
|
||||
"Sync Server Connected": "동기화 서버에 연결됨",
|
||||
"Sync as {{userDisplayName}}": "{{userDisplayName}}로 동기화",
|
||||
"Custom Fonts": "사용자 정의 글꼴",
|
||||
"Cancel Delete": "삭제 취소",
|
||||
"Import Font": "글꼴 가져오기",
|
||||
"Delete Font": "글꼴 삭제",
|
||||
"Tips": "팁",
|
||||
"Custom fonts can be selected from the Font Face menu": "사용자 정의 글꼴은 글꼴 메뉴에서 선택할 수 있습니다",
|
||||
"Manage Custom Fonts": "사용자 정의 글꼴 관리",
|
||||
"Select Files": "파일 선택",
|
||||
"Select Image": "이미지 선택",
|
||||
"Select Video": "동영상 선택",
|
||||
"Select Audio": "오디오 선택",
|
||||
"Select Fonts": "글꼴 선택",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "지원되는 글꼴 형식: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "진행 상황 푸시",
|
||||
"Pull Progress": "진행 상황 끌어오기",
|
||||
"Previous Paragraph": "이전 단락",
|
||||
"Previous Sentence": "이전 문장",
|
||||
"Pause": "일시정지",
|
||||
"Play": "재생",
|
||||
"Next Sentence": "다음 문장",
|
||||
"Next Paragraph": "다음 단락",
|
||||
"Separate Cover Page": "별도의 표지 페이지",
|
||||
"Resize Notebook": "노트북 크기 조정",
|
||||
"Resize Sidebar": "사이드바 크기 조정",
|
||||
"Get Help from the Readest Community": "Readest 커뮤니티에서 도움 받기",
|
||||
"Remove cover image": "표지 이미지 제거",
|
||||
"Bookshelf": "책장",
|
||||
"View Menu": "메뉴 보기",
|
||||
"Settings Menu": "설정 메뉴",
|
||||
"View account details and quota": "계정 세부정보 및 할당량 보기",
|
||||
"Library Header": "라이브러리 헤더",
|
||||
"Book Content": "도서 내용",
|
||||
"Footer Bar": "푸터 바",
|
||||
"Header Bar": "헤더 바",
|
||||
"View Options": "보기 옵션",
|
||||
"Book Menu": "도서 메뉴",
|
||||
"Search Options": "검색 옵션",
|
||||
"Close": "닫기",
|
||||
"Delete Book Options": "도서 삭제 옵션",
|
||||
"ON": "켜기",
|
||||
"OFF": "끄기",
|
||||
"Reading Progress": "읽기 진행 상황",
|
||||
"Page Margin": "페이지 여백",
|
||||
"Remove Bookmark": "북마크 제거",
|
||||
"Add Bookmark": "북마크 추가",
|
||||
"Books Content": "책 내용",
|
||||
"Jump to Location": "위치로 이동",
|
||||
"Unpin Notebook": "노트북 고정 해제",
|
||||
"Pin Notebook": "노트북 고정",
|
||||
"Hide Search Bar": "검색 바 숨기기",
|
||||
"Show Search Bar": "검색 바 표시",
|
||||
"On {{current}} of {{total}} page": "페이지 {{current}} / {{total}}",
|
||||
"Section Title": "섹션 제목",
|
||||
"Decrease": "감소",
|
||||
"Increase": "증가",
|
||||
"Settings Panels": "설정 패널",
|
||||
"Settings": "설정",
|
||||
"Unpin Sidebar": "사이드바 고정 해제",
|
||||
"Pin Sidebar": "사이드바 고정",
|
||||
"Toggle Sidebar": "사이드바 전환",
|
||||
"Toggle Translation": "번역 전환",
|
||||
"Translation Disabled": "번역 비활성화",
|
||||
"Minimize": "최소화",
|
||||
"Maximize or Restore": "최대화 또는 복원",
|
||||
"Exit Parallel Read": "병렬 읽기 종료",
|
||||
"Enter Parallel Read": "병렬 읽기 시작",
|
||||
"Zoom Level": "확대/축소 수준",
|
||||
"Zoom Out": "축소",
|
||||
"Reset Zoom": "줌 재설정",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Mode": "줌 모드",
|
||||
"Single Page": "단일 페이지",
|
||||
"Auto Spread": "자동 스프레드",
|
||||
"Fit Page": "페이지에 맞춤",
|
||||
"Fit Width": "너비에 맞춤",
|
||||
"Failed to select directory": "디렉토리 선택 실패",
|
||||
"The new data directory must be different from the current one.": "새 데이터 디렉토리는 현재 디렉토리와 달라야 합니다.",
|
||||
"Migration failed: {{error}}": "마이그레이션 실패: {{error}}",
|
||||
"Change Data Location": "데이터 위치 변경",
|
||||
"Current Data Location": "현재 데이터 위치",
|
||||
"Total size: {{size}}": "총 크기: {{size}}",
|
||||
"Calculating file info...": "파일 정보 계산 중...",
|
||||
"New Data Location": "새 데이터 위치",
|
||||
"Choose Different Folder": "다른 폴더 선택",
|
||||
"Choose New Folder": "새 폴더 선택",
|
||||
"Migrating data...": "데이터 마이그레이션 중...",
|
||||
"Copying: {{file}}": "복사 중: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} / {{total}} 파일",
|
||||
"Migration completed successfully!": "마이그레이션이 성공적으로 완료되었습니다!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "데이터가 새 위치로 이동되었습니다. 프로세스를 완료하려면 애플리케이션을 재시작하십시오.",
|
||||
"Migration failed": "마이그레이션 실패",
|
||||
"Important Notice": "중요한 공지",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "이 작업은 모든 앱 데이터를 새 위치로 이동합니다. 대상에 충분한 여유 공간이 있는지 확인하십시오.",
|
||||
"Restart App": "앱 재시작",
|
||||
"Start Migration": "마이그레이션 시작",
|
||||
"Advanced Settings": "고급 설정",
|
||||
"File count: {{size}}": "파일 수: {{size}}",
|
||||
"Background Read Aloud": "백그라운드 음성 읽기",
|
||||
"Ready to read aloud": "읽기 준비 완료",
|
||||
"Read Aloud": "음성 읽기",
|
||||
"Screen Brightness": "화면 밝기",
|
||||
"Background Image": "배경 이미지",
|
||||
"Import Image": "이미지 가져오기",
|
||||
"Opacity": "불투명도",
|
||||
"Size": "크기",
|
||||
"Cover": "표지",
|
||||
"Contain": "포함",
|
||||
"{{number}} pages left in chapter": "<1>남은 페이지 </1><0>{{number}}</0><1>장</1>",
|
||||
"Device": "장치",
|
||||
"E-Ink Mode": "E-Ink 모드",
|
||||
"Highlight Colors": "하이라이트 색상",
|
||||
"Pagination": "페이지 매김",
|
||||
"Disable Double Tap": "더블 탭 비활성화",
|
||||
"Tap to Paginate": "탭하여 페이지 매김",
|
||||
"Click to Paginate": "클릭하여 페이지 매김",
|
||||
"Tap Both Sides": "양쪽을 탭",
|
||||
"Click Both Sides": "양쪽을 클릭",
|
||||
"Swap Tap Sides": "탭 사이드 전환",
|
||||
"Swap Click Sides": "클릭 사이드 전환",
|
||||
"Source and Translated": "원본 및 번역",
|
||||
"Translated Only": "번역만",
|
||||
"Source Only": "원본만",
|
||||
"TTS Text": "TTS 텍스트",
|
||||
"The book file is corrupted": "책 파일이 손상되었습니다",
|
||||
"The book file is empty": "책 파일이 비어 있습니다",
|
||||
"Failed to open the book file": "책 파일을 열지 못했습니다",
|
||||
"On-Demand Purchase": "온디맨드 구매",
|
||||
"Full Customization": "풀 커스터마이징",
|
||||
"Lifetime Plan": "평생 플랜",
|
||||
"One-Time Payment": "일회성 결제",
|
||||
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "모든 장치에서 특정 기능에 대한 평생 액세스를 즐기기 위해 단일 결제를 수행합니다. 필요할 때만 특정 기능이나 서비스를 구매합니다.",
|
||||
"Expand Cloud Sync Storage": "클라우드 동기화 저장소 확장",
|
||||
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "1회 구매로 클라우드 저장소를 영구적으로 확장합니다. 추가 구매 시 더 많은 공간이 추가됩니다.",
|
||||
"Unlock All Customization Options": "모든 커스터마이징 옵션 잠금 해제",
|
||||
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "추가 테마, 글꼴, 레이아웃 옵션 및 읽기, 번역기, 클라우드 스토리지 서비스를 잠금 해제합니다.",
|
||||
"Purchase Successful!": "구매 성공!",
|
||||
"lifetime": "평생",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "구매해주셔서 감사합니다! 결제가 성공적으로 처리되었습니다.",
|
||||
"Order ID:": "주문 ID:",
|
||||
"TTS Highlighting": "TTS 하이라이팅",
|
||||
"Style": "스타일",
|
||||
"Underline": "밑줄",
|
||||
"Strikethrough": "취소선",
|
||||
"Squiggly": "물결선",
|
||||
"Outline": "윤곽선",
|
||||
"Save Current Color": "현재 색상 저장",
|
||||
"Quick Colors": "빠른 색상",
|
||||
"Highlighter": "형광펜",
|
||||
"Save Book Cover": "책 표지 저장",
|
||||
"Auto-save last book cover": "마지막 책 표지 자동 저장",
|
||||
"Back": "뒤로",
|
||||
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "확인 이메일이 전송되었습니다! 변경을 확인하려면 이전 이메일 주소와 새 이메일 주소를 확인하세요.",
|
||||
"Failed to update email": "이메일 업데이트 실패",
|
||||
"New Email": "새 이메일",
|
||||
"Your new email": "새 이메일 주소",
|
||||
"Updating email ...": "이메일 업데이트 중...",
|
||||
"Update email": "이메일 업데이트",
|
||||
"Current email": "현재 이메일",
|
||||
"Update Email": "이메일 업데이트",
|
||||
"All": "전체",
|
||||
"Unable to open book": "책을 열 수 없습니다",
|
||||
"Punctuation": "구두점",
|
||||
"Replace Quotation Marks": "인용 부호 바꾸기",
|
||||
"Enabled only in vertical layout.": "세로 레이아웃에서만 활성화됩니다.",
|
||||
"No Conversion": "변환 없음",
|
||||
"Simplified to Traditional": "간체 → 번체",
|
||||
"Traditional to Simplified": "번체 → 간체",
|
||||
"Simplified to Traditional (Taiwan)": "간체 → 번체 (대만)",
|
||||
"Simplified to Traditional (Hong Kong)": "간체 → 번체 (홍콩)",
|
||||
"Simplified to Traditional (Taiwan), with phrases": "간체 → 번체 (대만) • 문구",
|
||||
"Traditional (Taiwan) to Simplified": "번체 (대만) → 간체",
|
||||
"Traditional (Hong Kong) to Simplified": "번체 (홍콩) → 간체",
|
||||
"Traditional (Taiwan) to Simplified, with phrases": "번체 (대만) → 간체 • 문구",
|
||||
"Convert Simplified and Traditional Chinese": "간체/번체 변환",
|
||||
"Convert Mode": "변환 모드",
|
||||
"Failed to auto-save book cover for lock screen: {{error}}": "잠금 화면용 책 표지 자동 저장 실패: {{error}}",
|
||||
"Download from Cloud": "클라우드에서 다운로드",
|
||||
"Upload to Cloud": "클라우드에 업로드",
|
||||
"Clear Custom Fonts": "사용자 정의 글꼴 지우기",
|
||||
"Columns": "열",
|
||||
"OPDS Catalogs": "OPDS 카탈로그",
|
||||
"Adding LAN addresses is not supported in the web app version.": "웹 앱 버전에서는 LAN 주소 추가를 지원하지 않습니다.",
|
||||
"Invalid OPDS catalog. Please check the URL.": "잘못된 OPDS 카탈로그입니다. URL을 확인해주세요.",
|
||||
"Browse and download books from online catalogs": "온라인 카탈로그에서 책을 탐색하고 다운로드",
|
||||
"My Catalogs": "내 카탈로그",
|
||||
"Add Catalog": "카탈로그 추가",
|
||||
"No catalogs yet": "아직 카탈로그가 없습니다",
|
||||
"Add your first OPDS catalog to start browsing books": "책을 탐색하려면 첫 번째 OPDS 카탈로그를 추가하세요.",
|
||||
"Add Your First Catalog": "첫 번째 카탈로그 추가",
|
||||
"Browse": "탐색",
|
||||
"Popular Catalogs": "인기 카탈로그",
|
||||
"Add": "추가",
|
||||
"Add OPDS Catalog": "OPDS 카탈로그 추가",
|
||||
"Catalog Name": "카탈로그 이름",
|
||||
"My Calibre Library": "내 Calibre 라이브러리",
|
||||
"OPDS URL": "OPDS URL",
|
||||
"Username (optional)": "사용자 이름 (선택 사항)",
|
||||
"Password (optional)": "비밀번호 (선택 사항)",
|
||||
"Description (optional)": "설명 (선택 사항)",
|
||||
"A brief description of this catalog": "이 카탈로그의 간단한 설명",
|
||||
"Validating...": "검증 중...",
|
||||
"View All": "모두 보기",
|
||||
"Forward": "다음",
|
||||
"Home": "홈",
|
||||
"{{count}} items_other": "{{count}}개 항목",
|
||||
"Download completed": "다운로드 완료",
|
||||
"Download failed": "다운로드 실패",
|
||||
"Open Access": "오픈 액세스",
|
||||
"Borrow": "대출",
|
||||
"Buy": "구매",
|
||||
"Subscribe": "구독",
|
||||
"Sample": "샘플",
|
||||
"Download": "다운로드",
|
||||
"Open & Read": "열기 및 읽기",
|
||||
"Tags": "태그",
|
||||
"Tag": "태그",
|
||||
"First": "처음",
|
||||
"Previous": "이전",
|
||||
"Next": "다음",
|
||||
"Last": "마지막",
|
||||
"Cannot Load Page": "페이지를 불러올 수 없습니다",
|
||||
"An error occurred": "오류가 발생했습니다",
|
||||
"Online Library": "온라인 라이브러리",
|
||||
"URL must start with http:// or https://": "URL은 http:// 또는 https://로 시작해야 합니다",
|
||||
"Title, Author, Tag, etc...": "제목, 저자, 태그 등...",
|
||||
"Query": "쿼리",
|
||||
"Subject": "주제",
|
||||
"Enter {{terms}}": "{{terms}} 입력",
|
||||
"No search results found": "검색 결과가 없습니다",
|
||||
"Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS 피드를 불러오지 못했습니다: {{status}} {{statusText}}",
|
||||
"Search in {{title}}": "{{title}}에서 검색",
|
||||
"Manage Storage": "저장소 관리",
|
||||
"Failed to load files": "파일 로드 실패",
|
||||
"Deleted {{count}} file(s)_other": "{{count}}개의 파일이 삭제되었습니다",
|
||||
"Failed to delete {{count}} file(s)_other": "{{count}}개의 파일 삭제 실패",
|
||||
"Failed to delete files": "파일 삭제 실패",
|
||||
"Total Files": "총 파일",
|
||||
"Total Size": "총 용량",
|
||||
"Quota": "쿼터",
|
||||
"Used": "사용됨",
|
||||
"Files": "파일",
|
||||
"Search files...": "파일 검색...",
|
||||
"Newest First": "최신순",
|
||||
"Oldest First": "오래된순",
|
||||
"Largest First": "큰 파일순",
|
||||
"Smallest First": "작은 파일순",
|
||||
"Name A-Z": "이름 A-Z",
|
||||
"Name Z-A": "이름 Z-A",
|
||||
"{{count}} selected_other": "{{count}}개 선택됨",
|
||||
"Delete Selected": "선택 삭제",
|
||||
"Created": "생성됨",
|
||||
"No files found": "파일이 없습니다",
|
||||
"No files uploaded yet": "아직 업로드된 파일이 없습니다",
|
||||
"files": "파일",
|
||||
"Page {{current}} of {{total}}": "페이지 {{current}} / {{total}}",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "선택한 {{count}}개의 파일을 삭제하시겠습니까?",
|
||||
"Cloud Storage Usage": "클라우드 저장소 사용량",
|
||||
"Rename Group": "그룹 이름 바꾸기",
|
||||
"From Directory": "디렉토리에서",
|
||||
"Successfully imported {{count}} book(s)_other": "성공적으로 {{count}} 권의 책이 가져와졌습니다",
|
||||
"Count": "개수",
|
||||
"Start Page": "시작 페이지",
|
||||
"Search in OPDS Catalog...": "OPDS 카탈로그에서 검색...",
|
||||
"Please log in to use advanced TTS features": "고급 TTS 기능을 사용하려면 로그인하세요",
|
||||
"Word limit of 30 words exceeded.": "30단어 제한을 초과했습니다.",
|
||||
"Proofread": "교정",
|
||||
"Current selection": "현재 선택",
|
||||
"All occurrences in this book": "이 책의 모든 위치",
|
||||
"All occurrences in your library": "라이브러리의 모든 위치",
|
||||
"Selected text:": "선택한 텍스트:",
|
||||
"Replace with:": "다음으로 바꾸기:",
|
||||
"Enter text...": "텍스트 입력…",
|
||||
"Case sensitive:": "대소문자 구분:",
|
||||
"Scope:": "적용 범위:",
|
||||
"Selection": "선택",
|
||||
"Library": "라이브러리",
|
||||
"Yes": "예",
|
||||
"No": "아니요",
|
||||
"Proofread Replacement Rules": "교정 대체 규칙",
|
||||
"Selected Text Rules": "선택한 텍스트 규칙",
|
||||
"No selected text replacement rules": "선택한 텍스트에 대한 대체 규칙이 없습니다",
|
||||
"Book Specific Rules": "책별 규칙",
|
||||
"No book-level replacement rules": "책 수준의 대체 규칙이 없습니다",
|
||||
"Disable Quick Action": "빠른 작업 비활성화",
|
||||
"Enable Quick Action on Selection": "선택 시 빠른 작업 활성화",
|
||||
"None": "없음",
|
||||
"Annotation Tools": "주석 도구",
|
||||
"Enable Quick Actions": "빠른 작업 활성화",
|
||||
"Quick Action": "빠른 작업",
|
||||
"Copy to Notebook": "노트에 복사",
|
||||
"Copy text after selection": "선택 후 텍스트 복사",
|
||||
"Highlight text after selection": "선택 후 텍스트 강조",
|
||||
"Annotate text after selection": "선택 후 텍스트 주석 달기",
|
||||
"Search text after selection": "선택 후 텍스트 검색",
|
||||
"Look up text in dictionary after selection": "선택 후 텍스트를 사전에서 찾기",
|
||||
"Look up text in Wikipedia after selection": "선택 후 텍스트를 위키피디아에서 찾기",
|
||||
"Translate text after selection": "선택 후 텍스트 번역",
|
||||
"Read text aloud after selection": "선택 후 텍스트 음성 읽기",
|
||||
"Proofread text after selection": "선택 후 텍스트 교정",
|
||||
"{{activeCount}} active, {{pendingCount}} pending": "{{activeCount}} 진행 중, {{pendingCount}} 대기 중",
|
||||
"{{failedCount}} failed": "{{failedCount}} 실패",
|
||||
"Waiting...": "대기 중...",
|
||||
"Failed": "실패",
|
||||
"Completed": "완료됨",
|
||||
"Cancelled": "취소됨",
|
||||
"Retry": "재시도",
|
||||
"Active": "진행 중",
|
||||
"Transfer Queue": "전송 대기열",
|
||||
"Upload All": "모두 업로드",
|
||||
"Download All": "모두 다운로드",
|
||||
"Resume Transfers": "전송 재개",
|
||||
"Pause Transfers": "전송 일시정지",
|
||||
"Pending": "대기 중",
|
||||
"No transfers": "전송 없음",
|
||||
"Retry All": "모두 재시도",
|
||||
"Clear Completed": "완료된 항목 지우기",
|
||||
"Clear Failed": "실패한 항목 지우기",
|
||||
"Upload queued: {{title}}": "업로드 대기열에 추가됨: {{title}}",
|
||||
"Download queued: {{title}}": "다운로드 대기열에 추가됨: {{title}}",
|
||||
"Book not found in library": "라이브러리에서 책을 찾을 수 없음",
|
||||
"Unknown error": "알 수 없는 오류",
|
||||
"Please log in to continue": "계속하려면 로그인하세요",
|
||||
"Cloud File Transfers": "클라우드 파일 전송",
|
||||
"Show Search Results": "검색 결과 보기",
|
||||
"Search results for '{{term}}'": "'{{term}}' 검색 결과",
|
||||
"Close Search": "검색 닫기",
|
||||
"Previous Result": "이전 결과",
|
||||
"Next Result": "다음 결과",
|
||||
"Bookmarks": "북마크",
|
||||
"Annotations": "주석",
|
||||
"Show Results": "결과 표시",
|
||||
"Clear search": "검색 지우기",
|
||||
"Clear search history": "검색 기록 지우기",
|
||||
"Tap to Toggle Footer": "탭하여 바닥글 전환",
|
||||
"Show Current Time": "현재 시간 표시",
|
||||
"Use 24 Hour Clock": "24시간제 사용",
|
||||
"Show Current Battery Status": "현재 배터리 상태 표시",
|
||||
"Exported successfully": "내보내기 성공",
|
||||
"Book exported successfully.": "책을 성공적으로 내보냈습니다.",
|
||||
"Failed to export the book.": "책 내보내기에 실패했습니다.",
|
||||
"Export Book": "책 내보내기",
|
||||
"Whole word:": "전체 단어:",
|
||||
"Error": "오류",
|
||||
"Unable to load the article. Try searching directly on {{link}}.": "문서를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요.",
|
||||
"Unable to load the word. Try searching directly on {{link}}.": "단어를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요.",
|
||||
"Date Published": "출판일",
|
||||
"Only for TTS:": "TTS 전용:",
|
||||
"Uploaded": "업로드됨",
|
||||
"Downloaded": "다운로드됨",
|
||||
"Deleted": "삭제됨",
|
||||
"Note:": "노트:",
|
||||
"Time:": "시간:",
|
||||
"Format Options": "형식 옵션",
|
||||
"Export Date": "내보내기 날짜",
|
||||
"Chapter Titles": "챕터 제목",
|
||||
"Chapter Separator": "챕터 구분 기호",
|
||||
"Highlights": "하이라이트",
|
||||
"Note Date": "노트 날짜",
|
||||
"Advanced": "고급",
|
||||
"Hide": "숨기기",
|
||||
"Show": "표시",
|
||||
"Use Custom Template": "사용자 지정 템플릿 사용",
|
||||
"Export Template": "내보내기 템플릿",
|
||||
"Template Syntax:": "템플릿 구문:",
|
||||
"Insert value": "값 삽입",
|
||||
"Format date (locale)": "날짜 형식 (로케일)",
|
||||
"Format date (custom)": "날짜 형식 (사용자 지정)",
|
||||
"Conditional": "조건부",
|
||||
"Loop": "반복",
|
||||
"Available Variables:": "사용 가능한 변수:",
|
||||
"Book title": "책 제목",
|
||||
"Book author": "책 저자",
|
||||
"Export date": "내보내기 날짜",
|
||||
"Array of chapters": "챕터 배열",
|
||||
"Chapter title": "챕터 제목",
|
||||
"Array of annotations": "주석 배열",
|
||||
"Highlighted text": "하이라이트된 텍스트",
|
||||
"Annotation note": "주석 노트",
|
||||
"Date Format Tokens:": "날짜 형식 토큰:",
|
||||
"Year (4 digits)": "연도 (4자리)",
|
||||
"Month (01-12)": "월 (01-12)",
|
||||
"Day (01-31)": "일 (01-31)",
|
||||
"Hour (00-23)": "시 (00-23)",
|
||||
"Minute (00-59)": "분 (00-59)",
|
||||
"Second (00-59)": "초 (00-59)",
|
||||
"Show Source": "소스 표시",
|
||||
"No content to preview": "미리 볼 내용 없음",
|
||||
"Export": "내보내기",
|
||||
"Set Timeout": "시간 제한 설정",
|
||||
"Select Voice": "음성 선택",
|
||||
"Toggle Sticky Bottom TTS Bar": "고정 TTS 바 전환",
|
||||
"Display what I'm reading on Discord": "Discord에 읽는 책 표시",
|
||||
"Show on Discord": "Discord에 표시",
|
||||
"Instant {{action}}": "즉시 {{action}}",
|
||||
"Instant {{action}} Disabled": "즉시 {{action}} 비활성화",
|
||||
"Annotation": "주석",
|
||||
"Reset Template": "템플릿 초기화",
|
||||
"Annotation style": "주석 스타일",
|
||||
"Annotation color": "주석 색상",
|
||||
"Annotation time": "주석 시간",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "이 책을 다시 인덱싱하시겠습니까?",
|
||||
"Enable AI in Settings": "설정에서 AI 활성화",
|
||||
"Index This Book": "이 책 인덱싱",
|
||||
"Enable AI search and chat for this book": "이 책에 대한 AI 검색 및 채팅 활성화",
|
||||
"Start Indexing": "인덱싱 시작",
|
||||
"Indexing book...": "책 인덱싱 중...",
|
||||
"Preparing...": "준비 중...",
|
||||
"Delete this conversation?": "이 대화를 삭제하시겠습니까?",
|
||||
"No conversations yet": "아직 대화가 없습니다",
|
||||
"Start a new chat to ask questions about this book": "이 책에 대해 질문하려면 새 채팅을 시작하세요",
|
||||
"Rename": "이름 변경",
|
||||
"New Chat": "새 채팅",
|
||||
"Chat": "채팅",
|
||||
"Please enter a model ID": "모델 ID를 입력하세요",
|
||||
"Model not available or invalid": "모델을 사용할 수 없거나 유효하지 않습니다",
|
||||
"Failed to validate model": "모델 검증 실패",
|
||||
"Couldn't connect to Ollama. Is it running?": "Ollama에 연결할 수 없습니다. 실행 중인가요?",
|
||||
"Invalid API key or connection failed": "API 키가 유효하지 않거나 연결 실패",
|
||||
"Connection failed": "연결 실패",
|
||||
"AI Assistant": "AI 어시스턴트",
|
||||
"Enable AI Assistant": "AI 어시스턴트 활성화",
|
||||
"Provider": "제공자",
|
||||
"Ollama (Local)": "Ollama (로컬)",
|
||||
"AI Gateway (Cloud)": "AI 게이트웨이 (클라우드)",
|
||||
"Ollama Configuration": "Ollama 설정",
|
||||
"Refresh Models": "모델 새로고침",
|
||||
"AI Model": "AI 모델",
|
||||
"No models detected": "모델이 감지되지 않았습니다",
|
||||
"AI Gateway Configuration": "AI 게이트웨이 설정",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "고품질의 경제적인 AI 모델 중에서 선택하세요. 아래의 \"사용자 정의 모델\"을 선택하여 자신의 모델을 사용할 수도 있습니다.",
|
||||
"API Key": "API 키",
|
||||
"Get Key": "키 받기",
|
||||
"Model": "모델",
|
||||
"Custom Model...": "사용자 정의 모델...",
|
||||
"Custom Model ID": "사용자 정의 모델 ID",
|
||||
"Validate": "검증",
|
||||
"Model available": "모델 사용 가능",
|
||||
"Connection": "연결",
|
||||
"Test Connection": "연결 테스트",
|
||||
"Connected": "연결됨",
|
||||
"Custom Colors": "사용자 정의 색상",
|
||||
"Color E-Ink Mode": "컬러 E-Ink 모드",
|
||||
"Reading Ruler": "읽기 자",
|
||||
"Enable Reading Ruler": "읽기 자 활성화",
|
||||
"Lines to Highlight": "강조할 줄 수",
|
||||
"Ruler Color": "자 색상",
|
||||
"Command Palette": "명령 팔레트",
|
||||
"Search settings and actions...": "설정 및 작업 검색...",
|
||||
"No results found for": "에 대한 결과를 찾을 수 없음",
|
||||
"Type to search settings and actions": "입력하여 설정 및 작업 검색",
|
||||
"Recent": "최근",
|
||||
"navigate": "탐색",
|
||||
"select": "선택",
|
||||
"close": "닫기",
|
||||
"Search Settings": "설정 검색",
|
||||
"Page Margins": "페이지 여백",
|
||||
"AI Provider": "AI 제공처",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama 모델",
|
||||
"AI Gateway Model": "AI 게이트웨이 모델",
|
||||
"Actions": "작업",
|
||||
"Navigation": "탐색",
|
||||
"Set status for {{count}} book(s)_other": "책 {{count}}권 상태 설정",
|
||||
"Mark as Unread": "읽지 않음으로 표시",
|
||||
"Mark as Finished": "완료로 표시",
|
||||
"Finished": "완료",
|
||||
"Unread": "읽지 않음",
|
||||
"Clear Status": "상태 지우기",
|
||||
"Status": "상태",
|
||||
"Loading": "로드 중...",
|
||||
"Exit Paragraph Mode": "단락 모드 종료",
|
||||
"Paragraph Mode": "단락 모드",
|
||||
"Embedding Model": "임베딩 모델",
|
||||
"{{count}} book(s) synced_other": "{{count}}권의 책이 동기화되었습니다",
|
||||
"Unable to start RSVP": "RSVP를 시작할 수 없습니다",
|
||||
"RSVP not supported for PDF": "PDF는 RSVP를 지원하지 않습니다",
|
||||
"Select Chapter": "챕터 선택",
|
||||
"Context": "문맥",
|
||||
"Ready": "준비됨",
|
||||
"Chapter Progress": "챕터 진행 상황",
|
||||
"words": "단어",
|
||||
"{{time}} left": "{{time}} 남음",
|
||||
"Reading progress": "독서 진행 상황",
|
||||
"Click to seek": "클릭하여 탐색",
|
||||
"Skip back 15 words": "15단어 뒤로",
|
||||
"Back 15 words (Shift+Left)": "15단어 뒤로 (Shift+왼쪽 화살표)",
|
||||
"Pause (Space)": "일시정지 (스페이스바)",
|
||||
"Play (Space)": "재생 (스페이스바)",
|
||||
"Skip forward 15 words": "15단어 앞으로",
|
||||
"Forward 15 words (Shift+Right)": "15단어 앞으로 (Shift+오른쪽 화살표)",
|
||||
"Decrease speed": "속도 줄이기",
|
||||
"Slower (Left/Down)": "느리게 (왼쪽/아래 화살표)",
|
||||
"Increase speed": "속도 높이기",
|
||||
"Faster (Right/Up)": "빠르게 (오른쪽/위 화살표)",
|
||||
"Start RSVP Reading": "RSVP 독서 시작",
|
||||
"Choose where to start reading": "독서를 시작할 위치 선택",
|
||||
"From Chapter Start": "챕터 시작부터",
|
||||
"Start reading from the beginning of the chapter": "챕터 처음부터 다시 시작",
|
||||
"Resume": "재개",
|
||||
"Continue from where you left off": "중단한 지점부터 계속 읽기",
|
||||
"From Current Page": "현재 페이지부터",
|
||||
"Start from where you are currently reading": "현재 읽고 있는 위치부터 시작",
|
||||
"From Selection": "선택 범위부터",
|
||||
"Speed Reading Mode": "속독 모드",
|
||||
"Scroll left": "왼쪽으로 스크롤",
|
||||
"Scroll right": "오른쪽으로 스크롤",
|
||||
"Library Sync Progress": "라이브러리 동기화 진행 상황",
|
||||
"Back to library": "라이브러리로 돌아가기",
|
||||
"Group by...": "그룹화 기준...",
|
||||
"Export as Plain Text": "일반 텍스트로 내보내기",
|
||||
"Export as Markdown": "Markdown으로 내보내기",
|
||||
"Show Page Navigation Buttons": "탐색 버튼 표시",
|
||||
"Page {{number}}": "{{number}} 페이지",
|
||||
"highlight": "하이라이트",
|
||||
"underline": "밑줄",
|
||||
"squiggly": "구불구불한 선",
|
||||
"red": "빨간색",
|
||||
"violet": "보라색",
|
||||
"blue": "파란색",
|
||||
"green": "초록색",
|
||||
"yellow": "노란색",
|
||||
"Select {{style}} style": "{{style}} 스타일 선택",
|
||||
"Select {{color}} color": "{{color}} 색상 선택",
|
||||
"Close Book": "책 닫기",
|
||||
"Speed Reading": "속독",
|
||||
"Close Speed Reading": "속독 닫기",
|
||||
"Authors": "저자",
|
||||
"Books": "도서",
|
||||
"Groups": "그룹",
|
||||
"Back to TTS Location": "TTS 위치로 돌아가기",
|
||||
"Metadata": "메타데이터",
|
||||
"Image viewer": "이미지 뷰어",
|
||||
"Previous Image": "이전 이미지",
|
||||
"Next Image": "다음 이미지",
|
||||
"Zoomed": "확대됨",
|
||||
"Zoom level": "확대 수준",
|
||||
"Table viewer": "테이블 뷰어",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Readwise에 연결할 수 없습니다. 네트워크 연결을 확인해 주세요.",
|
||||
"Invalid Readwise access token": "유효하지 않은 Readwise 액세스 토큰입니다",
|
||||
"Disconnected from Readwise": "Readwise와 연결이 끊어졌습니다",
|
||||
"Never": "안 함",
|
||||
"Readwise Settings": "Readwise 설정",
|
||||
"Connected to Readwise": "Readwise에 연결됨",
|
||||
"Last synced: {{time}}": "마지막 동기화: {{time}}",
|
||||
"Sync Enabled": "동기화 활성화됨",
|
||||
"Disconnect": "연결 해제",
|
||||
"Connect your Readwise account to sync highlights.": "하이라이트를 동기화하려면 Readwise 계정렬 연결하세요.",
|
||||
"Get your access token at": "액세스 토큰 받기:",
|
||||
"Access Token": "액세스 토큰",
|
||||
"Paste your Readwise access token": "Readwise 액세스 토큰을 붙여넣으세요",
|
||||
"Config": "구성",
|
||||
"Readwise Sync": "Readwise 동기화",
|
||||
"Push Highlights": "하이라이트 푸시",
|
||||
"Highlights synced to Readwise": "하이라이트가 Readwise에 동기화되었습니다",
|
||||
"Readwise sync failed: no internet connection": "Readwise 동기화 실패: 인터넷 연결 없음",
|
||||
"Readwise sync failed: {{error}}": "Readwise 동기화 실패: {{error}}",
|
||||
"System Screen Brightness": "시스템 화면 밝기",
|
||||
"Page:": "페이지:",
|
||||
"Page: {{number}}": "페이지: {{number}}",
|
||||
"Annotation page number": "주석 페이지 번호",
|
||||
"Translating...": "번역 중...",
|
||||
"Show Battery Percentage": "배터리 백분율 표시",
|
||||
"Hide Scrollbar": "스크롤바 숨기기",
|
||||
"Skip to last reading position": "마지막 읽기 위치로 이동",
|
||||
"Show context": "맥락 표시",
|
||||
"Hide context": "맥락 숨기기",
|
||||
"Decrease font size": "글꼴 크기 줄이기",
|
||||
"Increase font size": "글꼴 크기 늘리기",
|
||||
"Focus": "초점",
|
||||
"Theme color": "테마 색상",
|
||||
"Import failed": "가져오기 실패",
|
||||
"Available Formatters:": "사용 가능한 포맷터:",
|
||||
"Format date": "날짜 형식",
|
||||
"Markdown block quote (> per line)": "Markdown 인용 블록 (줄마다 >)",
|
||||
"Newlines to <br>": "줄바꿈을 <br>로 변환",
|
||||
"Change case": "대소문자 변경",
|
||||
"Trim whitespace": "공백 제거",
|
||||
"Truncate to n characters": "n자로 자르기",
|
||||
"Replace text": "텍스트 바꾸기",
|
||||
"Fallback value": "기본값",
|
||||
"Get length": "길이 가져오기",
|
||||
"First/last element": "첫 번째/마지막 요소",
|
||||
"Join array": "배열 합치기",
|
||||
"Backup failed: {{error}}": "백업 실패: {{error}}",
|
||||
"Select Backup": "백업 선택",
|
||||
"Restore failed: {{error}}": "복원 실패: {{error}}",
|
||||
"Backup & Restore": "백업 및 복원",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "라이브러리를 백업하거나 이전 백업에서 복원하세요. 복원 시 현재 라이브러리와 병합됩니다.",
|
||||
"Backup Library": "라이브러리 백업",
|
||||
"Restore Library": "라이브러리 복원",
|
||||
"Creating backup...": "백업 생성 중...",
|
||||
"Restoring library...": "라이브러리 복원 중...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 항목",
|
||||
"Backup completed successfully!": "백업이 완료되었습니다!",
|
||||
"Restore completed successfully!": "복원이 완료되었습니다!",
|
||||
"Your library has been saved to the selected location.": "라이브러리가 선택한 위치에 저장되었습니다.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}권 추가, {{updated}}권 업데이트되었습니다.",
|
||||
"Operation failed": "작업 실패",
|
||||
"Loading library...": "라이브러리 로딩 중...",
|
||||
"{{count}} books refreshed_other": "{{count}}권 새로고침됨",
|
||||
"Failed to refresh metadata": "메타데이터 새로고침 실패",
|
||||
"Refresh Metadata": "메타데이터 새로고침"
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
"Token": "Token",
|
||||
"Your OTP token": "Uw OTP-token",
|
||||
"Verify token": "Token verifiëren",
|
||||
"Sign in with Google": "Inloggen met Google",
|
||||
"Sign in with Apple": "Inloggen met Apple",
|
||||
"Sign in with GitHub": "Inloggen met GitHub",
|
||||
"New Password": "Nieuw wachtwoord",
|
||||
"Your new password": "Uw nieuwe wachtwoord",
|
||||
"Update password": "Wachtwoord bijwerken",
|
||||
@@ -55,10 +52,7 @@
|
||||
"Search Books...": "Boeken zoeken...",
|
||||
"Clear Search": "Zoekopdracht wissen",
|
||||
"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",
|
||||
@@ -84,9 +78,9 @@
|
||||
"Descending": "Aflopend",
|
||||
"Sort by...": "Sorteren op...",
|
||||
"No supported files found. Supported formats: {{formats}}": "Geen ondersteunde bestanden gevonden. Ondersteunde formaten: {{formats}}",
|
||||
"No chapters detected.": "Geen hoofdstukken gedetecteerd.",
|
||||
"Failed to parse the EPUB file.": "EPUB-bestand kon niet worden verwerkt.",
|
||||
"This book format is not supported.": "Dit boekformaat wordt niet ondersteund.",
|
||||
"No chapters detected": "Geen hoofdstukken gedetecteerd",
|
||||
"Failed to parse the EPUB file": "EPUB-bestand kon niet worden verwerkt",
|
||||
"This book format is not supported": "Dit boekformaat wordt niet ondersteund",
|
||||
"Failed to import book(s): {{filenames}}": "Importeren van boek(en) mislukt: {{filenames}}",
|
||||
"Book uploaded: {{title}}": "Boek geüpload: {{title}}",
|
||||
"Insufficient storage quota": "Onvoldoende opslagruimte",
|
||||
@@ -187,10 +181,6 @@
|
||||
"Behavior": "Gedrag",
|
||||
"Continuous Scroll": "Doorlopend scrollen",
|
||||
"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",
|
||||
@@ -203,7 +193,6 @@
|
||||
"Link Color": "Linkkleur",
|
||||
"Preview": "Voorbeeld",
|
||||
"Font & Layout": "Lettertype & opmaak",
|
||||
"Book Cover": "Boekomslag",
|
||||
"More Info": "Meer informatie",
|
||||
"Parallel Read": "Parallel lezen",
|
||||
"Export Annotations": "Annotaties exporteren",
|
||||
@@ -215,9 +204,8 @@
|
||||
"Match Whole Words": "Hele woorden",
|
||||
"Match Diacritics": "Diakritische tekens",
|
||||
"TOC": "Inhoudsopgave",
|
||||
"Table of Contents": "Inhoudsopgave",
|
||||
"Sidebar": "Zijbalk",
|
||||
"TTS not supported for PDF": "TTS niet ondersteund voor PDF",
|
||||
"TTS not supported in this device": "TTS niet ondersteund op dit apparaat",
|
||||
"No Timeout": "Geen time-out",
|
||||
"{{value}} minute": "{{value}} minuut",
|
||||
"{{value}} minutes": "{{value}} minuten",
|
||||
@@ -227,31 +215,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,24 +230,22 @@
|
||||
"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!",
|
||||
"A new version of Readest is available!": "Een nieuwe versie van Readest is beschikbaar!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} is beschikbaar (geïnstalleerde versie {{currentVersion}}).",
|
||||
"Download and install now?": "Nu downloaden en installeren?",
|
||||
"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,8 +263,8 @@
|
||||
"Reveal in Finder": "Tonen in Finder",
|
||||
"Reveal in File Explorer": "Tonen in Verkenner",
|
||||
"Reveal in Folder": "Tonen in map",
|
||||
"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",
|
||||
"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",
|
||||
"Next Page": "Volgende pagina",
|
||||
@@ -326,12 +290,11 @@
|
||||
"Disable Translation": "Vertaling uitschakelen",
|
||||
"Scroll": "Scrollen",
|
||||
"Overlap Pixels": "Overlap pixels",
|
||||
"Click": "Klik",
|
||||
"System Language": "Systemtaal",
|
||||
"Security": "Beveiliging",
|
||||
"Allow JavaScript": "JavaScript toestaan",
|
||||
"Enable only if you trust the file.": "Schakel alleen in als u het bestand vertrouwt.",
|
||||
"Sort TOC by Page": "__STRING_NOT_TRANSLATED__",
|
||||
"Sort TOC by Page": "TOC sorteren op pagina",
|
||||
"Search in {{count}} Book(s)..._one": "Zoeken in {{count}} Boek...",
|
||||
"Search in {{count}} Book(s)..._other": "Zoeken in {{count}} Boeken...",
|
||||
"No notes match your search": "Geen notities gevonden",
|
||||
@@ -340,14 +303,12 @@
|
||||
"Synced at {{time}}": "Gesynchroniseerd om {{time}}",
|
||||
"Never synced": "Nooit gesynchroniseerd",
|
||||
"Show Remaining Time": "Toon resterende tijd",
|
||||
"Show Page Number": "Toon paginanummer",
|
||||
"{{time}} min left in chapter": "Nog {{time}} min in hoofdstuk",
|
||||
"Override Book Color": "Boekkleur overschrijven",
|
||||
"Login Required": "Inloggen vereist",
|
||||
"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 +324,783 @@
|
||||
"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",
|
||||
"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": "<1>Nog </1><0>{{count}}</0><1> pagina in hoofdstuk</1>",
|
||||
"{{count}} pages left in chapter_other": "<1>Nog </1><0>{{count}}</0><1> pagina’s in hoofdstuk</1>",
|
||||
"Show Remaining Pages": "Toon resterende pagina’s",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
"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:",
|
||||
"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}}",
|
||||
"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",
|
||||
"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",
|
||||
"TTS not supported for this document": "TTS niet ondersteund voor dit document",
|
||||
"Reset Password": "Wachtwoord resetten",
|
||||
"Show Reading Progress": "Leesvoortgang weergeven",
|
||||
"Reading Progress Style": "Stijl van leesvoortgang",
|
||||
"Page Number": "Pagina nummer",
|
||||
"Percentage": "Percentage",
|
||||
"Deleted local copy of the book: {{title}}": "Verwijderde lokale kopie van het boek: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "Verwijderen van cloudback-up van het boek is mislukt: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "Verwijderen van lokale kopie van het boek is mislukt: {{title}}",
|
||||
"Are you sure to delete the local copy of the selected book?": "Weet je zeker dat je de lokale kopie van het geselecteerde boek wilt verwijderen?",
|
||||
"Remove from Cloud & Device": "Verwijder uit Cloud & Apparaat",
|
||||
"Remove from Cloud Only": "Verwijder alleen uit Cloud",
|
||||
"Remove from Device Only": "Verwijder alleen uit Apparaat",
|
||||
"Disconnected": "Verbroken",
|
||||
"KOReader Sync Settings": "KOReader Sync-instellingen",
|
||||
"Sync Strategy": "Synchronisatiestrategie",
|
||||
"Ask on conflict": "Vraag bij conflict",
|
||||
"Always use latest": "Altijd de nieuwste gebruiken",
|
||||
"Send changes only": "Verzend alleen wijzigingen",
|
||||
"Receive changes only": "Ontvang alleen wijzigingen",
|
||||
"Checksum Method": "Checksum-methode",
|
||||
"File Content (recommended)": "Bestandsinhoud (aanbevolen)",
|
||||
"File Name": "Bestandsnaam",
|
||||
"Device Name": "Apparaatnaam",
|
||||
"Connect to your KOReader Sync server.": "Verbind met je KOReader Sync-server.",
|
||||
"Server URL": "Server-URL",
|
||||
"Username": "Gebruikersnaam",
|
||||
"Your Username": "Je gebruikersnaam",
|
||||
"Password": "Wachtwoord",
|
||||
"Connect": "Verbinden",
|
||||
"KOReader Sync": "KOReader Sync",
|
||||
"Sync Conflict": "Synchronisatieconflict",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "Leesvoortgang synchroniseren van \"{{deviceName}}\"?",
|
||||
"another device": "een ander apparaat",
|
||||
"Local Progress": "Lokale voortgang",
|
||||
"Remote Progress": "Externe voortgang",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "Pagina {{page}} van {{total}} ({{percentage}}%)",
|
||||
"Current position": "Huidige positie",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "Ongeveer pagina {{page}} van {{total}} ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "Ongeveer {{percentage}}%",
|
||||
"Failed to connect": "Verbinding mislukt",
|
||||
"Sync Server Connected": "Synchronisatie server verbonden",
|
||||
"Sync as {{userDisplayName}}": "Synchroniseren als {{userDisplayName}}",
|
||||
"Custom Fonts": "Aangepaste Lettertypen",
|
||||
"Cancel Delete": "Verwijderen Annuleren",
|
||||
"Import Font": "Lettertype Importeren",
|
||||
"Delete Font": "Lettertype Verwijderen",
|
||||
"Tips": "Tips",
|
||||
"Custom fonts can be selected from the Font Face menu": "Aangepaste lettertypen kunnen worden geselecteerd vanuit het Lettertype menu",
|
||||
"Manage Custom Fonts": "Aangepaste Lettertypen Beheren",
|
||||
"Select Files": "Bestanden Selecteren",
|
||||
"Select Image": "Afbeelding Selecteren",
|
||||
"Select Video": "Video Selecteren",
|
||||
"Select Audio": "Audio Selecteren",
|
||||
"Select Fonts": "Lettertypen Selecteren",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Ondersteunde lettertypeformaten: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Voortgang pushen",
|
||||
"Pull Progress": "Voortgang pullen",
|
||||
"Previous Paragraph": "Vorige alinea",
|
||||
"Previous Sentence": "Vorige zin",
|
||||
"Pause": "Pauzeren",
|
||||
"Play": "Afspelen",
|
||||
"Next Sentence": "Volgende zin",
|
||||
"Next Paragraph": "Volgende alinea",
|
||||
"Separate Cover Page": "Afzonderlijke omslagpagina",
|
||||
"Resize Notebook": "Formaat Notitieblok Aanpassen",
|
||||
"Resize Sidebar": "Formaat Zijbalk Aanpassen",
|
||||
"Get Help from the Readest Community": "Hulp krijgen van de Readest Community",
|
||||
"Remove cover image": "Omslagafbeelding verwijderen",
|
||||
"Bookshelf": "Boekenkast",
|
||||
"View Menu": "Menu bekijken",
|
||||
"Settings Menu": "Instellingenmenu",
|
||||
"View account details and quota": "Accountgegevens en quotum bekijken",
|
||||
"Library Header": "Bibliotheekkop",
|
||||
"Book Content": "Boekinhoud",
|
||||
"Footer Bar": "Voettekstbalk",
|
||||
"Header Bar": "Koptekstbalk",
|
||||
"View Options": "Weergaveopties",
|
||||
"Book Menu": "Boekmenu",
|
||||
"Search Options": "Zoekopties",
|
||||
"Close": "Sluiten",
|
||||
"Delete Book Options": "Boekverwijderopties",
|
||||
"ON": "AAN",
|
||||
"OFF": "UIT",
|
||||
"Reading Progress": "Leesvoortgang",
|
||||
"Page Margin": "Pagina Marges",
|
||||
"Remove Bookmark": "Bladwijzer Verwijderen",
|
||||
"Add Bookmark": "Bladwijzer Toevoegen",
|
||||
"Books Content": "Boekinhoud",
|
||||
"Jump to Location": "Ga naar Locatie",
|
||||
"Unpin Notebook": "Notitieblok Losmaken",
|
||||
"Pin Notebook": "Notitieblok Vastmaken",
|
||||
"Hide Search Bar": "Verberg Zoekbalk",
|
||||
"Show Search Bar": "Toon Zoekbalk",
|
||||
"On {{current}} of {{total}} page": "Op {{current}} van {{total}} pagina",
|
||||
"Section Title": "Sectietitel",
|
||||
"Decrease": "Verkleinen",
|
||||
"Increase": "Vergroten",
|
||||
"Settings Panels": "Instellingenpanelen",
|
||||
"Settings": "Instellingen",
|
||||
"Unpin Sidebar": "Zijbalk Losmaken",
|
||||
"Pin Sidebar": "Zijbalk Vastmaken",
|
||||
"Toggle Sidebar": "Zijbalk Wisselen",
|
||||
"Toggle Translation": "Vertaling Wisselen",
|
||||
"Translation Disabled": "Vertaling Uitgeschakeld",
|
||||
"Minimize": "Minimaliseren",
|
||||
"Maximize or Restore": "Maximaliseren of Herstellen",
|
||||
"Exit Parallel Read": "Parallel Lezen Afsluiten",
|
||||
"Enter Parallel Read": "Parallel Lezen Starten",
|
||||
"Zoom Level": "Zoomniveau",
|
||||
"Zoom Out": "Zoom Verkleinen",
|
||||
"Reset Zoom": "Zoom Reset",
|
||||
"Zoom In": "Zoom Vergroten",
|
||||
"Zoom Mode": "Zoom Modus",
|
||||
"Single Page": "Enkele Pagina",
|
||||
"Auto Spread": "Automatische Verspreiding",
|
||||
"Fit Page": "Pagina Aanpassen",
|
||||
"Fit Width": "Breedte Aanpassen",
|
||||
"Failed to select directory": "Selecteren van map is mislukt",
|
||||
"The new data directory must be different from the current one.": "De nieuwe gegevensmap moet anders zijn dan de huidige.",
|
||||
"Migration failed: {{error}}": "Migratie mislukt: {{error}}",
|
||||
"Change Data Location": "Gegevenslocatie Wijzigen",
|
||||
"Current Data Location": "Huidige Gegevenslocatie",
|
||||
"Total size: {{size}}": "Totale grootte: {{size}}",
|
||||
"Calculating file info...": "Bestandinformatie wordt berekend...",
|
||||
"New Data Location": "Nieuwe Gegevenslocatie",
|
||||
"Choose Different Folder": "Kies Een Andere Map",
|
||||
"Choose New Folder": "Kies Nieuwe Map",
|
||||
"Migrating data...": "Gegevens worden gemigreerd...",
|
||||
"Copying: {{file}}": "Kopiëren: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} van {{total}} bestanden",
|
||||
"Migration completed successfully!": "Migratie succesvol voltooid!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Uw gegevens zijn naar de nieuwe locatie verplaatst. Start de applicatie opnieuw op om het proces te voltooien.",
|
||||
"Migration failed": "Migratie mislukt",
|
||||
"Important Notice": "Belangrijke Kennisgeving",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Dit zal al uw app-gegevens naar de nieuwe locatie verplaatsen. Zorg ervoor dat de bestemming voldoende vrije ruimte heeft.",
|
||||
"Restart App": "Herstart App",
|
||||
"Start Migration": "Start Migratie",
|
||||
"Advanced Settings": "Geavanceerde Instellingen",
|
||||
"File count: {{size}}": "Bestandsaantal: {{size}}",
|
||||
"Background Read Aloud": "Achtergrond Voorlezen",
|
||||
"Ready to read aloud": "Klaar om voor te lezen",
|
||||
"Read Aloud": "Voorlezen",
|
||||
"Screen Brightness": "Schermhelderheid",
|
||||
"Background Image": "Achtergrondafbeelding",
|
||||
"Import Image": "Afbeelding importeren",
|
||||
"Opacity": "Doorzichtigheid",
|
||||
"Size": "Grootte",
|
||||
"Cover": "Omslag",
|
||||
"Contain": "Bevatten",
|
||||
"{{number}} pages left in chapter": "<1>Nog </1><0>{{number}}</0><1> pagina’s in hoofdstuk</1>",
|
||||
"Device": "Apparaat",
|
||||
"E-Ink Mode": "E-Ink Modus",
|
||||
"Highlight Colors": "Markeerkleuren",
|
||||
"Pagination": "Paginering",
|
||||
"Disable Double Tap": "Schakel Dubbel Tikken Uit",
|
||||
"Tap to Paginate": "Tik om te Pagineren",
|
||||
"Click to Paginate": "Klik om te Pagineren",
|
||||
"Tap Both Sides": "Tik op Beide Zijden",
|
||||
"Click Both Sides": "Klik op Beide Zijden",
|
||||
"Swap Tap Sides": "Wissel Tap Zijden",
|
||||
"Swap Click Sides": "Wissel Klik Zijden",
|
||||
"Source and Translated": "Bron en Vertaling",
|
||||
"Translated Only": "Alleen Vertaling",
|
||||
"Source Only": "Alleen Bron",
|
||||
"TTS Text": "TTS Tekst",
|
||||
"The book file is corrupted": "Het boekbestand is beschadigd",
|
||||
"The book file is empty": "Het boekbestand is leeg",
|
||||
"Failed to open the book file": "Het openen van het boekbestand is mislukt",
|
||||
"On-Demand Purchase": "On-Demand Aankoop",
|
||||
"Full Customization": "Volledige Aanpassing",
|
||||
"Lifetime Plan": "Levenslange Abonnement",
|
||||
"One-Time Payment": "Eenmalige Betaling",
|
||||
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Doe een eenmalige betaling om levenslange toegang te krijgen tot specifieke functies op alle apparaten. Koop specifieke functies of diensten alleen wanneer je ze nodig hebt.",
|
||||
"Expand Cloud Sync Storage": "Cloud Sync Opslag Uitbreiden",
|
||||
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Breid je cloudopslag voor altijd uit met een eenmalige aankoop. Elke extra aankoop voegt meer ruimte toe.",
|
||||
"Unlock All Customization Options": "Ontgrendel Alle Aanpassingsopties",
|
||||
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Ontgrendel extra thema's, lettertypen, lay-outopties en voorlezen, vertalers, cloudopslagdiensten.",
|
||||
"Purchase Successful!": "Aankoop Succesvol!",
|
||||
"lifetime": "levenslang",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Bedankt voor je aankoop! Je betaling is succesvol verwerkt.",
|
||||
"Order ID:": "Bestel-ID:",
|
||||
"TTS Highlighting": "TTS-markering",
|
||||
"Style": "Stijl",
|
||||
"Underline": "Onderstrepen",
|
||||
"Strikethrough": "Doorhalen",
|
||||
"Squiggly": "Kronkelig",
|
||||
"Outline": "Omtrek",
|
||||
"Save Current Color": "Huidige kleur opslaan",
|
||||
"Quick Colors": "Snelle kleuren",
|
||||
"Highlighter": "Marker",
|
||||
"Save Book Cover": "Opslaan boekomslag",
|
||||
"Auto-save last book cover": "Automatisch opslaan laatste boekomslag",
|
||||
"Back": "Terug",
|
||||
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Bevestigingsmail verzonden! Controleer je oude en nieuwe e-mailadres om de wijziging te bevestigen.",
|
||||
"Failed to update email": "E-mail bijwerken mislukt",
|
||||
"New Email": "Nieuw e-mailadres",
|
||||
"Your new email": "Je nieuwe e-mailadres",
|
||||
"Updating email ...": "E-mail wordt bijgewerkt...",
|
||||
"Update email": "E-mail bijwerken",
|
||||
"Current email": "Huidig e-mailadres",
|
||||
"Update Email": "E-mail bijwerken",
|
||||
"All": "Alles",
|
||||
"Unable to open book": "Kan boek niet openen",
|
||||
"Punctuation": "Interpunctie",
|
||||
"Replace Quotation Marks": "Aanhalingstekens vervangen",
|
||||
"Enabled only in vertical layout.": "Alleen ingeschakeld in verticale lay-out.",
|
||||
"No Conversion": "Geen conversie",
|
||||
"Simplified to Traditional": "Vereenvoudigd → Traditioneel",
|
||||
"Traditional to Simplified": "Traditioneel → Vereenvoudigd",
|
||||
"Simplified to Traditional (Taiwan)": "Vereenvoudigd → Traditioneel (Taiwan)",
|
||||
"Simplified to Traditional (Hong Kong)": "Vereenvoudigd → Traditioneel (Hong Kong)",
|
||||
"Simplified to Traditional (Taiwan), with phrases": "Vereenvoudigd → Traditioneel (Taiwan • zinnen)",
|
||||
"Traditional (Taiwan) to Simplified": "Traditioneel (Taiwan) → Vereenvoudigd",
|
||||
"Traditional (Hong Kong) to Simplified": "Traditioneel (Hong Kong) → Vereenvoudigd",
|
||||
"Traditional (Taiwan) to Simplified, with phrases": "Traditioneel (Taiwan • zinnen) → Vereenvoudigd",
|
||||
"Convert Simplified and Traditional Chinese": "Converteer Vereenvoudigd/Traditioneel Chinees",
|
||||
"Convert Mode": "Conversiemodus",
|
||||
"Failed to auto-save book cover for lock screen: {{error}}": "Automatisch opslaan van boekomslag voor vergrendelscherm mislukt: {{error}}",
|
||||
"Download from Cloud": "Downloaden van Cloud",
|
||||
"Upload to Cloud": "Uploaden naar Cloud",
|
||||
"Clear Custom Fonts": "Aangepaste Lettertypen Wissen",
|
||||
"Columns": "Kolommen",
|
||||
"OPDS Catalogs": "OPDS-catalogi",
|
||||
"Adding LAN addresses is not supported in the web app version.": "Het toevoegen van LAN-adressen wordt niet ondersteund in de webversie.",
|
||||
"Invalid OPDS catalog. Please check the URL.": "Ongeldige OPDS-catalogus. Controleer de URL.",
|
||||
"Browse and download books from online catalogs": "Blader door en download boeken uit online catalogi",
|
||||
"My Catalogs": "Mijn catalogi",
|
||||
"Add Catalog": "Catalogus toevoegen",
|
||||
"No catalogs yet": "Nog geen catalogi",
|
||||
"Add your first OPDS catalog to start browsing books": "Voeg je eerste OPDS-catalogus toe om boeken te bekijken",
|
||||
"Add Your First Catalog": "Voeg je eerste catalogus toe",
|
||||
"Browse": "Bladeren",
|
||||
"Popular Catalogs": "Populaire catalogi",
|
||||
"Add": "Toevoegen",
|
||||
"Add OPDS Catalog": "OPDS-catalogus toevoegen",
|
||||
"Catalog Name": "Catalogusnaam",
|
||||
"My Calibre Library": "Mijn Calibre-bibliotheek",
|
||||
"OPDS URL": "OPDS-URL",
|
||||
"Username (optional)": "Gebruikersnaam (optioneel)",
|
||||
"Password (optional)": "Wachtwoord (optioneel)",
|
||||
"Description (optional)": "Beschrijving (optioneel)",
|
||||
"A brief description of this catalog": "Een korte beschrijving van deze catalogus",
|
||||
"Validating...": "Valideren...",
|
||||
"View All": "Alles bekijken",
|
||||
"Forward": "Verder",
|
||||
"Home": "Startpagina",
|
||||
"{{count}} items_one": "{{count}} item",
|
||||
"{{count}} items_other": "{{count}} items",
|
||||
"Download completed": "Download voltooid",
|
||||
"Download failed": "Download mislukt",
|
||||
"Open Access": "Open toegang",
|
||||
"Borrow": "Lenen",
|
||||
"Buy": "Kopen",
|
||||
"Subscribe": "Abonneren",
|
||||
"Sample": "Voorbeeld",
|
||||
"Download": "Downloaden",
|
||||
"Open & Read": "Openen & Lezen",
|
||||
"Tags": "Tags",
|
||||
"Tag": "Tag",
|
||||
"First": "Eerste",
|
||||
"Previous": "Vorige",
|
||||
"Next": "Volgende",
|
||||
"Last": "Laatste",
|
||||
"Cannot Load Page": "Pagina kan niet worden geladen",
|
||||
"An error occurred": "Er is een fout opgetreden",
|
||||
"Online Library": "Online Bibliotheek",
|
||||
"URL must start with http:// or https://": "URL moet beginnen met http:// of https://",
|
||||
"Title, Author, Tag, etc...": "Titel, Auteur, Tag, enzovoort...",
|
||||
"Query": "Zoekopdracht",
|
||||
"Subject": "Onderwerp",
|
||||
"Enter {{terms}}": "Voer {{terms}} in",
|
||||
"No search results found": "Geen zoekresultaten gevonden",
|
||||
"Failed to load OPDS feed: {{status}} {{statusText}}": "Het laden van de OPDS-feed is mislukt: {{status}} {{statusText}}",
|
||||
"Search in {{title}}": "Zoeken in {{title}}",
|
||||
"Manage Storage": "Opslag beheren",
|
||||
"Failed to load files": "Bestanden laden mislukt",
|
||||
"Deleted {{count}} file(s)_one": "{{count}} bestand verwijderd",
|
||||
"Deleted {{count}} file(s)_other": "{{count}} bestanden verwijderd",
|
||||
"Failed to delete {{count}} file(s)_one": "Kon {{count}} bestand niet verwijderen",
|
||||
"Failed to delete {{count}} file(s)_other": "Kon {{count}} bestanden niet verwijderen",
|
||||
"Failed to delete files": "Bestanden verwijderen mislukt",
|
||||
"Total Files": "Totaal aantal bestanden",
|
||||
"Total Size": "Totale grootte",
|
||||
"Quota": "Quota",
|
||||
"Used": "Gebruikt",
|
||||
"Files": "Bestanden",
|
||||
"Search files...": "Bestanden zoeken...",
|
||||
"Newest First": "Nieuwste eerst",
|
||||
"Oldest First": "Oudste eerst",
|
||||
"Largest First": "Grootste eerst",
|
||||
"Smallest First": "Kleinste eerst",
|
||||
"Name A-Z": "Naam A-Z",
|
||||
"Name Z-A": "Naam Z-A",
|
||||
"{{count}} selected_one": "{{count}} geselecteerd",
|
||||
"{{count}} selected_other": "{{count}} geselecteerd",
|
||||
"Delete Selected": "Verwijder geselecteerde",
|
||||
"Created": "Gemaakt",
|
||||
"No files found": "Geen bestanden gevonden",
|
||||
"No files uploaded yet": "Nog geen bestanden geüpload",
|
||||
"files": "bestanden",
|
||||
"Page {{current}} of {{total}}": "Pagina {{current}} van {{total}}",
|
||||
"Are you sure to delete {{count}} selected file(s)?_one": "Weet je zeker dat je {{count}} geselecteerd bestand wilt verwijderen?",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "Weet je zeker dat je {{count}} geselecteerde bestanden wilt verwijderen?",
|
||||
"Cloud Storage Usage": "Cloudopslaggebruik",
|
||||
"Rename Group": "Groep hernoemen",
|
||||
"From Directory": "Vanuit map",
|
||||
"Successfully imported {{count}} book(s)_one": "Succesvol 1 boek geïmporteerd",
|
||||
"Successfully imported {{count}} book(s)_other": "Succesvol {{count}} boeken geïmporteerd",
|
||||
"Count": "Aantal",
|
||||
"Start Page": "Startpagina",
|
||||
"Search in OPDS Catalog...": "Zoeken in OPDS-catalogus...",
|
||||
"Please log in to use advanced TTS features": "Log in om geavanceerde TTS-functies te gebruiken",
|
||||
"Word limit of 30 words exceeded.": "De limiet van 30 woorden is overschreden.",
|
||||
"Proofread": "Correctie",
|
||||
"Current selection": "Huidige selectie",
|
||||
"All occurrences in this book": "Alle voorkomens in dit boek",
|
||||
"All occurrences in your library": "Alle voorkomens in je bibliotheek",
|
||||
"Selected text:": "Geselecteerde tekst:",
|
||||
"Replace with:": "Vervangen door:",
|
||||
"Enter text...": "Tekst invoeren…",
|
||||
"Case sensitive:": "Hoofdlettergevoelig:",
|
||||
"Scope:": "Bereik:",
|
||||
"Selection": "Selectie",
|
||||
"Library": "Bibliotheek",
|
||||
"Yes": "Ja",
|
||||
"No": "Nee",
|
||||
"Proofread Replacement Rules": "Correctie-vervangingsregels",
|
||||
"Selected Text Rules": "Regels voor geselecteerde tekst",
|
||||
"No selected text replacement rules": "Geen vervangingsregels voor geselecteerde tekst",
|
||||
"Book Specific Rules": "Boekspecifieke regels",
|
||||
"No book-level replacement rules": "Geen vervangingsregels op boekniveau",
|
||||
"Disable Quick Action": "Snelle actie uitschakelen",
|
||||
"Enable Quick Action on Selection": "Snelle actie bij selectie inschakelen",
|
||||
"None": "Geen",
|
||||
"Annotation Tools": "Annotatiehulpmiddelen",
|
||||
"Enable Quick Actions": "Snelle acties inschakelen",
|
||||
"Quick Action": "Snelle actie",
|
||||
"Copy to Notebook": "Kopiëren naar notitieboek",
|
||||
"Copy text after selection": "Kopieer tekst na selectie",
|
||||
"Highlight text after selection": "Markeer tekst na selectie",
|
||||
"Annotate text after selection": "Annoteren tekst na selectie",
|
||||
"Search text after selection": "Zoek tekst na selectie",
|
||||
"Look up text in dictionary after selection": "Zoek tekst op in woordenboek na selectie",
|
||||
"Look up text in Wikipedia after selection": "Zoek tekst op in Wikipedia na selectie",
|
||||
"Translate text after selection": "Vertaal tekst na selectie",
|
||||
"Read text aloud after selection": "Lees tekst hardop na selectie",
|
||||
"Proofread text after selection": "Corrigeer tekst na selectie",
|
||||
"{{activeCount}} active, {{pendingCount}} pending": "{{activeCount}} actief, {{pendingCount}} wachtend",
|
||||
"{{failedCount}} failed": "{{failedCount}} mislukt",
|
||||
"Waiting...": "Wachten...",
|
||||
"Failed": "Mislukt",
|
||||
"Completed": "Voltooid",
|
||||
"Cancelled": "Geannuleerd",
|
||||
"Retry": "Opnieuw proberen",
|
||||
"Active": "Actief",
|
||||
"Transfer Queue": "Overdrachtwachtrij",
|
||||
"Upload All": "Alles uploaden",
|
||||
"Download All": "Alles downloaden",
|
||||
"Resume Transfers": "Overdrachten hervatten",
|
||||
"Pause Transfers": "Overdrachten pauzeren",
|
||||
"Pending": "Wachtend",
|
||||
"No transfers": "Geen overdrachten",
|
||||
"Retry All": "Alles opnieuw proberen",
|
||||
"Clear Completed": "Voltooide wissen",
|
||||
"Clear Failed": "Mislukte wissen",
|
||||
"Upload queued: {{title}}": "Upload in wachtrij: {{title}}",
|
||||
"Download queued: {{title}}": "Download in wachtrij: {{title}}",
|
||||
"Book not found in library": "Boek niet gevonden in bibliotheek",
|
||||
"Unknown error": "Onbekende fout",
|
||||
"Please log in to continue": "Log in om door te gaan",
|
||||
"Cloud File Transfers": "Cloud Bestandoverdrachten",
|
||||
"Show Search Results": "Zoekresultaten tonen",
|
||||
"Search results for '{{term}}'": "Resultaten voor '{{term}}'",
|
||||
"Close Search": "Zoekopdracht sluiten",
|
||||
"Previous Result": "Vorig resultaat",
|
||||
"Next Result": "Volgend resultaat",
|
||||
"Bookmarks": "Bladwijzers",
|
||||
"Annotations": "Aantekeningen",
|
||||
"Show Results": "Resultaten tonen",
|
||||
"Clear search": "Zoekopdracht wissen",
|
||||
"Clear search history": "Zoekgeschiedenis wissen",
|
||||
"Tap to Toggle Footer": "Tik om voettekst te wisselen",
|
||||
"Show Current Time": "Huidige tijd weergeven",
|
||||
"Use 24 Hour Clock": "24-uurs klok gebruike",
|
||||
"Show Current Battery Status": "Toon huidige batterijstatus",
|
||||
"Exported successfully": "Succesvol geëxporteerd",
|
||||
"Book exported successfully.": "Boek succesvol geëxporteerd.",
|
||||
"Failed to export the book.": "Exporteren van het boek mislukt.",
|
||||
"Export Book": "Boek exporteren",
|
||||
"Whole word:": "Heel woord:",
|
||||
"Error": "Fout",
|
||||
"Unable to load the article. Try searching directly on {{link}}.": "Kan het artikel niet laden. Probeer direct te zoeken op {{link}}.",
|
||||
"Unable to load the word. Try searching directly on {{link}}.": "Kan het woord niet laden. Probeer direct te zoeken op {{link}}.",
|
||||
"Date Published": "Publicatiedatum",
|
||||
"Only for TTS:": "Alleen voor TTS:",
|
||||
"Uploaded": "Geüpload",
|
||||
"Downloaded": "Gedownload",
|
||||
"Deleted": "Verwijderd",
|
||||
"Note:": "Notitie:",
|
||||
"Time:": "Tijd:",
|
||||
"Format Options": "Formaatopties",
|
||||
"Export Date": "Exportdatum",
|
||||
"Chapter Titles": "Hoofdstuktitels",
|
||||
"Chapter Separator": "Hoofdstukscheiding",
|
||||
"Highlights": "Markeringen",
|
||||
"Note Date": "Notitiedatum",
|
||||
"Advanced": "Geavanceerd",
|
||||
"Hide": "Verbergen",
|
||||
"Show": "Tonen",
|
||||
"Use Custom Template": "Aangepast sjabloon gebruiken",
|
||||
"Export Template": "Exportsjabloon",
|
||||
"Template Syntax:": "Sjabloonsyntaxis:",
|
||||
"Insert value": "Waarde invoegen",
|
||||
"Format date (locale)": "Datum opmaken (lokaal)",
|
||||
"Format date (custom)": "Datum opmaken (aangepast)",
|
||||
"Conditional": "Voorwaardelijk",
|
||||
"Loop": "Lus",
|
||||
"Available Variables:": "Beschikbare variabelen:",
|
||||
"Book title": "Boektitel",
|
||||
"Book author": "Boekauteur",
|
||||
"Export date": "Exportdatum",
|
||||
"Array of chapters": "Reeks hoofdstukken",
|
||||
"Chapter title": "Hoofdstuktitel",
|
||||
"Array of annotations": "Reeks annotaties",
|
||||
"Highlighted text": "Gemarkeerde tekst",
|
||||
"Annotation note": "Annotatienotitie",
|
||||
"Date Format Tokens:": "Datumformaattokens:",
|
||||
"Year (4 digits)": "Jaar (4 cijfers)",
|
||||
"Month (01-12)": "Maand (01-12)",
|
||||
"Day (01-31)": "Dag (01-31)",
|
||||
"Hour (00-23)": "Uur (00-23)",
|
||||
"Minute (00-59)": "Minuut (00-59)",
|
||||
"Second (00-59)": "Seconde (00-59)",
|
||||
"Show Source": "Bron tonen",
|
||||
"No content to preview": "Geen inhoud om te bekijken",
|
||||
"Export": "Exporteren",
|
||||
"Set Timeout": "Time-out instellen",
|
||||
"Select Voice": "Stem selecteren",
|
||||
"Toggle Sticky Bottom TTS Bar": "Vaste TTS-balk schakelen",
|
||||
"Display what I'm reading on Discord": "Toon wat ik lees op Discord",
|
||||
"Show on Discord": "Toon op Discord",
|
||||
"Instant {{action}}": "Directe {{action}}",
|
||||
"Instant {{action}} Disabled": "Directe {{action}} uitgeschakeld",
|
||||
"Annotation": "Annotatie",
|
||||
"Reset Template": "Sjabloon resetten",
|
||||
"Annotation style": "Annotatiestijl",
|
||||
"Annotation color": "Annotatiekleur",
|
||||
"Annotation time": "Annotatietijd",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Weet u zeker dat u dit boek opnieuw wilt indexeren?",
|
||||
"Enable AI in Settings": "AI inschakelen in Instellingen",
|
||||
"Index This Book": "Dit boek indexeren",
|
||||
"Enable AI search and chat for this book": "AI-zoeken en chat voor dit boek inschakelen",
|
||||
"Start Indexing": "Indexeren starten",
|
||||
"Indexing book...": "Boek wordt geïndexeerd...",
|
||||
"Preparing...": "Voorbereiden...",
|
||||
"Delete this conversation?": "Dit gesprek verwijderen?",
|
||||
"No conversations yet": "Nog geen gesprekken",
|
||||
"Start a new chat to ask questions about this book": "Start een nieuwe chat om vragen te stellen over dit boek",
|
||||
"Rename": "Hernoemen",
|
||||
"New Chat": "Nieuwe chat",
|
||||
"Chat": "Chat",
|
||||
"Please enter a model ID": "Voer een model-ID in",
|
||||
"Model not available or invalid": "Model niet beschikbaar of ongeldig",
|
||||
"Failed to validate model": "Model valideren mislukt",
|
||||
"Couldn't connect to Ollama. Is it running?": "Kon geen verbinding maken met Ollama. Is het actief?",
|
||||
"Invalid API key or connection failed": "Ongeldige API-sleutel of verbinding mislukt",
|
||||
"Connection failed": "Verbinding mislukt",
|
||||
"AI Assistant": "AI-assistent",
|
||||
"Enable AI Assistant": "AI-assistent inschakelen",
|
||||
"Provider": "Provider",
|
||||
"Ollama (Local)": "Ollama (Lokaal)",
|
||||
"AI Gateway (Cloud)": "AI-gateway (Cloud)",
|
||||
"Ollama Configuration": "Ollama-configuratie",
|
||||
"Refresh Models": "Modellen vernieuwen",
|
||||
"AI Model": "AI-model",
|
||||
"No models detected": "Geen modellen gedetecteerd",
|
||||
"AI Gateway Configuration": "AI-gateway configuratie",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Kies uit een selectie van hoogwaardige, voordelige AI-modellen. U kunt ook uw eigen model gebruiken door hieronder \"Aangepast model\" te selecteren.",
|
||||
"API Key": "API-sleutel",
|
||||
"Get Key": "Sleutel ophalen",
|
||||
"Model": "Model",
|
||||
"Custom Model...": "Aangepast model...",
|
||||
"Custom Model ID": "Aangepast model-ID",
|
||||
"Validate": "Valideren",
|
||||
"Model available": "Model beschikbaar",
|
||||
"Connection": "Verbinding",
|
||||
"Test Connection": "Verbinding testen",
|
||||
"Connected": "Verbonden",
|
||||
"Custom Colors": "Aangepaste kleuren",
|
||||
"Color E-Ink Mode": "Kleur E-Ink modus",
|
||||
"Reading Ruler": "Leesliniaal",
|
||||
"Enable Reading Ruler": "Leesliniaal inschakelen",
|
||||
"Lines to Highlight": "Regels om te markeren",
|
||||
"Ruler Color": "Kleur van liniaal",
|
||||
"Command Palette": "Opdrachtenpalet",
|
||||
"Search settings and actions...": "Zoek instellingen en acties...",
|
||||
"No results found for": "Geen resultaten gevonden voor",
|
||||
"Type to search settings and actions": "Typ om instellingen en acties te zoeken",
|
||||
"Recent": "Recent",
|
||||
"navigate": "navigeren",
|
||||
"select": "selecteren",
|
||||
"close": "sluiten",
|
||||
"Search Settings": "Instellingen zoeken",
|
||||
"Page Margins": "Paginamarges",
|
||||
"AI Provider": "AI-provider",
|
||||
"Ollama URL": "Ollama-URL",
|
||||
"Ollama Model": "Ollama-model",
|
||||
"AI Gateway Model": "AI Gateway-model",
|
||||
"Actions": "Acties",
|
||||
"Navigation": "Navigatie",
|
||||
"Set status for {{count}} book(s)_one": "Stel status in voor {{count}} boek",
|
||||
"Set status for {{count}} book(s)_other": "Stel status in voor {{count}} boeken",
|
||||
"Mark as Unread": "Markeren als ongelezen",
|
||||
"Mark as Finished": "Markeren als voltooid",
|
||||
"Finished": "Voltooid",
|
||||
"Unread": "Ongelezen",
|
||||
"Clear Status": "Status wissen",
|
||||
"Status": "Status",
|
||||
"Loading": "Laden...",
|
||||
"Exit Paragraph Mode": "Alineamodus verlaten",
|
||||
"Paragraph Mode": "Alineamodus",
|
||||
"Embedding Model": "Insluitmodel",
|
||||
"{{count}} book(s) synced_one": "{{count}} boek gesynchroniseerd",
|
||||
"{{count}} book(s) synced_other": "{{count}} boeken gesynchroniseerd",
|
||||
"Unable to start RSVP": "Kan RSVP niet starten",
|
||||
"RSVP not supported for PDF": "RSVP niet ondersteund voor PDF",
|
||||
"Select Chapter": "Hoofdstuk selecteren",
|
||||
"Context": "Context",
|
||||
"Ready": "Klaar",
|
||||
"Chapter Progress": "Hoofdstukvoortgang",
|
||||
"words": "woorden",
|
||||
"{{time}} left": "{{time}} over",
|
||||
"Reading progress": "Leesvoortgang",
|
||||
"Click to seek": "Klik om te zoeken",
|
||||
"Skip back 15 words": "15 woorden terug",
|
||||
"Back 15 words (Shift+Left)": "15 woorden terug (Shift+Links)",
|
||||
"Pause (Space)": "Pauze (Spatie)",
|
||||
"Play (Space)": "Afspelen (Spatie)",
|
||||
"Skip forward 15 words": "15 woorden vooruit",
|
||||
"Forward 15 words (Shift+Right)": "15 woorden vooruit (Shift+Rechts)",
|
||||
"Decrease speed": "Snelheid verlagen",
|
||||
"Slower (Left/Down)": "Langzamer (Links/Omlaag)",
|
||||
"Increase speed": "Snelheid verhogen",
|
||||
"Faster (Right/Up)": "Sneller (Rechts/Omhoog)",
|
||||
"Start RSVP Reading": "Start RSVP-lezen",
|
||||
"Choose where to start reading": "Kies waar u wilt beginnen met lezen",
|
||||
"From Chapter Start": "Vanaf begin hoofdstuk",
|
||||
"Start reading from the beginning of the chapter": "Begin met lezen aan het begin van het hoofdstuk",
|
||||
"Resume": "Hervatten",
|
||||
"Continue from where you left off": "Ga verder waar u was gebleven",
|
||||
"From Current Page": "Vanaf huidige pagina",
|
||||
"Start from where you are currently reading": "Begin waar u momenteel leest",
|
||||
"From Selection": "Vanaf selectie",
|
||||
"Speed Reading Mode": "Snelle leesmodus",
|
||||
"Scroll left": "Naar links scrollen",
|
||||
"Scroll right": "Naar rechts scrollen",
|
||||
"Library Sync Progress": "Voortgang bibliotheeksynchronisatie",
|
||||
"Back to library": "Terug naar bibliotheek",
|
||||
"Group by...": "Groeperen op...",
|
||||
"Export as Plain Text": "Exporteren als platte tekst",
|
||||
"Export as Markdown": "Exporteren als Markdown",
|
||||
"Show Page Navigation Buttons": "Navigatieknoppen",
|
||||
"Page {{number}}": "Pagina {{number}}",
|
||||
"highlight": "markering",
|
||||
"underline": "onderstreping",
|
||||
"squiggly": "gegolfd",
|
||||
"red": "rood",
|
||||
"violet": "paars",
|
||||
"blue": "blauw",
|
||||
"green": "groen",
|
||||
"yellow": "geel",
|
||||
"Select {{style}} style": "Selecteer {{style}} stijl",
|
||||
"Select {{color}} color": "Selecteer {{color}} kleur",
|
||||
"Close Book": "Boek sluiten",
|
||||
"Speed Reading": "Snel lezen",
|
||||
"Close Speed Reading": "Snel lezen sluiten",
|
||||
"Authors": "Auteurs",
|
||||
"Books": "Boeken",
|
||||
"Groups": "Groepen",
|
||||
"Back to TTS Location": "Terug naar TTS-locatie",
|
||||
"Metadata": "Metadata",
|
||||
"Image viewer": "Afbeeldingenviewer",
|
||||
"Previous Image": "Vorige afbeelding",
|
||||
"Next Image": "Volgende afbeelding",
|
||||
"Zoomed": "Gezoomd",
|
||||
"Zoom level": "Zoomniveau",
|
||||
"Table viewer": "Tabelviewer",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Kan geen verbinding maken met Readwise. Controleer uw netwerkverbinding.",
|
||||
"Invalid Readwise access token": "Ongeldige Readwise-toegangstoken",
|
||||
"Disconnected from Readwise": "Verbinding met Readwise verbroken",
|
||||
"Never": "Nooit",
|
||||
"Readwise Settings": "Readwise-instellingen",
|
||||
"Connected to Readwise": "Verbonden met Readwise",
|
||||
"Last synced: {{time}}": "Laatst gesynchroniseerd: {{time}}",
|
||||
"Sync Enabled": "Synchronisatie ingeschakeld",
|
||||
"Disconnect": "Verbinding verbreken",
|
||||
"Connect your Readwise account to sync highlights.": "Verbind uw Readwise-account om markeringen te synchroniseren.",
|
||||
"Get your access token at": "Verkrijg uw toegangstoken op",
|
||||
"Access Token": "Toegangstoken",
|
||||
"Paste your Readwise access token": "Plak uw Readwise-toegangstoken",
|
||||
"Config": "Configuratie",
|
||||
"Readwise Sync": "Readwise-synchronisatie",
|
||||
"Push Highlights": "Markeringen verzenden",
|
||||
"Highlights synced to Readwise": "Markeringen gesynchroniseerd met Readwise",
|
||||
"Readwise sync failed: no internet connection": "Readwise-synchronisatie mislukt: geen internetverbinding",
|
||||
"Readwise sync failed: {{error}}": "Readwise-synchronisatie mislukt: {{error}}",
|
||||
"System Screen Brightness": "Systeemhelderheid van het scherm",
|
||||
"Page:": "Pagina:",
|
||||
"Page: {{number}}": "Pagina: {{number}}",
|
||||
"Annotation page number": "Annotatiepaginanummer",
|
||||
"Translating...": "Vertalen...",
|
||||
"Show Battery Percentage": "Batterijpercentage weergeven",
|
||||
"Hide Scrollbar": "Schuifbalk verbergen",
|
||||
"Skip to last reading position": "Ga naar laatste leespositie",
|
||||
"Show context": "Context tonen",
|
||||
"Hide context": "Context verbergen",
|
||||
"Decrease font size": "Lettergrootte verkleinen",
|
||||
"Increase font size": "Lettergrootte vergroten",
|
||||
"Focus": "Focus",
|
||||
"Theme color": "Themakleur",
|
||||
"Import failed": "Import mislukt",
|
||||
"Available Formatters:": "Beschikbare opmaak:",
|
||||
"Format date": "Datum opmaken",
|
||||
"Markdown block quote (> per line)": "Markdown-blokcitaat (> per regel)",
|
||||
"Newlines to <br>": "Regeleinden naar <br>",
|
||||
"Change case": "Hoofdlettergebruik wijzigen",
|
||||
"Trim whitespace": "Witruimte verwijderen",
|
||||
"Truncate to n characters": "Inkorten tot n tekens",
|
||||
"Replace text": "Tekst vervangen",
|
||||
"Fallback value": "Terugvalwaarde",
|
||||
"Get length": "Lengte ophalen",
|
||||
"First/last element": "Eerste/laatste element",
|
||||
"Join array": "Array samenvoegen",
|
||||
"Backup failed: {{error}}": "Back-up mislukt: {{error}}",
|
||||
"Select Backup": "Selecteer back-up",
|
||||
"Restore failed: {{error}}": "Herstel mislukt: {{error}}",
|
||||
"Backup & Restore": "Back-up & Herstel",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Maak een back-up van uw bibliotheek of herstel vanuit een eerdere back-up. Herstel wordt samengevoegd met uw huidige bibliotheek.",
|
||||
"Backup Library": "Bibliotheek back-uppen",
|
||||
"Restore Library": "Bibliotheek herstellen",
|
||||
"Creating backup...": "Back-up maken...",
|
||||
"Restoring library...": "Bibliotheek herstellen...",
|
||||
"{{current}} of {{total}} items": "{{current}} van {{total}} items",
|
||||
"Backup completed successfully!": "Back-up succesvol voltooid!",
|
||||
"Restore completed successfully!": "Herstel succesvol voltooid!",
|
||||
"Your library has been saved to the selected location.": "Uw bibliotheek is opgeslagen op de geselecteerde locatie.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} boeken toegevoegd, {{updated}} boeken bijgewerkt.",
|
||||
"Operation failed": "Bewerking mislukt",
|
||||
"Loading library...": "Bibliotheek laden...",
|
||||
"{{count}} books refreshed_one": "{{count}} boek vernieuwd",
|
||||
"{{count}} books refreshed_other": "{{count}} boeken vernieuwd",
|
||||
"Failed to refresh metadata": "Metadata vernieuwen mislukt",
|
||||
"Refresh Metadata": "Metadata vernieuwen"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"Auto Mode": "Chế độ tự động",
|
||||
"Behavior": "Hành vi",
|
||||
"Book": "Sách",
|
||||
"Book Cover": "Bìa sách",
|
||||
"Bookmark": "Đánh dấu",
|
||||
"Cancel": "Hủy",
|
||||
"Chapter": "Chương",
|
||||
@@ -18,7 +17,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 +26,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 +45,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 +66,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",
|
||||
@@ -85,7 +81,6 @@
|
||||
"Search...": "Tìm kiếm...",
|
||||
"Select Book": "Chọn sách",
|
||||
"Select Books": "Chọn sách",
|
||||
"Select Multiple Books": "Chọn nhiều sách",
|
||||
"Sepia": "Nâu cổ",
|
||||
"Serif Font": "Phông chữ có chân",
|
||||
"Show Book Details": "Hiển thị chi tiết sách",
|
||||
@@ -96,23 +91,21 @@
|
||||
"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ủ đề",
|
||||
"Theme Mode": "Chế độ chủ đề",
|
||||
"Translate": "Dịch",
|
||||
"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.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Chế độ viết",
|
||||
"Your Library": "Thư viện của bạn",
|
||||
"TTS not supported for PDF": "TTS không được hỗ trợ cho PDF",
|
||||
"Override Book Font": "Ghi đè phông chữ sách",
|
||||
"Apply to All Books": "Áp dụng cho tất cả sách",
|
||||
"Apply to This Book": "Áp dụng cho sách này",
|
||||
@@ -122,14 +115,13 @@
|
||||
"Book Details": "Chi tiết sách",
|
||||
"From Local File": "Từ tệp địa phương",
|
||||
"TOC": "Mục lục",
|
||||
"Table of Contents": "Mục lục",
|
||||
"Book uploaded: {{title}}": "Sách đã tải lên: {{title}}",
|
||||
"Failed to upload book: {{title}}": "Không thể tải lên sách: {{title}}",
|
||||
"Book downloaded: {{title}}": "Sách đã tải về: {{title}}",
|
||||
"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 +153,7 @@
|
||||
"Create a Password": "Tạo mật khẩu",
|
||||
"Sign up": "Đăng ký",
|
||||
"Signing up...": "Đang đăng ký...",
|
||||
"Don’t 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",
|
||||
@@ -180,40 +172,15 @@
|
||||
"Token": "Mã token",
|
||||
"Your OTP token": "Mã OTP của bạn",
|
||||
"Verify token": "Xác minh mã token",
|
||||
"Sign in with Google": "Đăng nhập với Google",
|
||||
"Sign in with Apple": "Đăng nhập với Apple",
|
||||
"Sign in with GitHub": "Đăng nhập với GitHub",
|
||||
"Account": "Tài khoản",
|
||||
"Failed to delete user. Please try again later.": "Không thể xóa người dùng. Vui lòng thử lại sau.",
|
||||
"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",
|
||||
@@ -222,7 +189,7 @@
|
||||
"No supported files found. Supported formats: {{formats}}": "Không tìm thấy tệp được hỗ trợ. Định dạng được hỗ trợ: {{formats}}",
|
||||
"Drop to Import Books": "Kéo và thả để nhập sách",
|
||||
"Custom": "Tùy chỉnh",
|
||||
"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": "Cả thế giới này là một sân khấu,\nVà tất cả đàn ông và phụ nữ chỉ là những diễn viên;\nHọ có cửa ra và cửa vào của mình,\nVà một người trong cuộc đời mình đóng nhiều vai,\nHành động của mình là bảy tuổi.\n\n— William Shakespeare",
|
||||
"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": "Cả thế giới này là một sân khấu,\nVà tất cả đàn ông và phụ nữ chỉ là những diễn viên;\nHọ có cửa ra và cửa vào của mình,\nVà một người trong cuộc đời mình đóng nhiều vai,\nHành động của mình là bảy tuổi.\n\n— William Shakespeare",
|
||||
"Custom Theme": "Chủ đề tùy chỉnh",
|
||||
"Theme Name": "Tên chủ đề",
|
||||
"Text Color": "Màu văn bản",
|
||||
@@ -249,9 +216,9 @@
|
||||
"Auto Import on File Open": "Tự động nhập khi mở tệp",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai SC",
|
||||
"LXGW WenKai TC": "LXGW WenKai TC",
|
||||
"No chapters detected.": "Không phát hiện thấy chương nào.",
|
||||
"Failed to parse the EPUB file.": "Không thể phân tích tệp EPUB.",
|
||||
"This book format is not supported.": "Định dạng sách này không được hỗ trợ.",
|
||||
"No chapters detected": "Không phát hiện thấy chương nào",
|
||||
"Failed to parse the EPUB file": "Không thể phân tích tệp EPUB",
|
||||
"This book format is not supported": "Định dạng sách này không được hỗ trợ",
|
||||
"Unable to fetch the translation. Please log in first and try again.": "Không thể tải dịch vụ. Vui lòng đăng nhập trước và thử lại.",
|
||||
"Group": "Nhóm",
|
||||
"Always on Top": "Luôn ở trên cùng",
|
||||
@@ -264,13 +231,13 @@
|
||||
"Clear Search": "Xóa tìm kiếm",
|
||||
"Header & Footer": "Đầu trang & Chân trang",
|
||||
"Apply also in Scrolled Mode": "Áp dụng cũng trong chế độ cuộn",
|
||||
"A new version of Readest is Available!": "Đã có phiên bản mới của Readest!",
|
||||
"A new version of Readest is available!": "Đã có phiên bản mới của Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} đã có sẵn (phiên bản đã cài đặt {{currentVersion}}).",
|
||||
"Download and install now?": "Tải xuống và cài đặt ngay bây giờ?",
|
||||
"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,18 +246,15 @@
|
||||
"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",
|
||||
"(from 'As You Like It', Act II)": "(từ 'Như bạn thích', Hành động II)",
|
||||
"Link Color": "Màu liên kết",
|
||||
"Volume Keys for Page Flip": "Phím âm lượng để lật trang",
|
||||
"Clicks for Page Flip": "Nhấp để lật trang",
|
||||
"Swap Clicks Area": "Đổi khu vực nhấp",
|
||||
"Screen": "Màn hình",
|
||||
"Orientation": "Định hướng",
|
||||
"Portrait": "Chân dung",
|
||||
@@ -299,7 +263,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",
|
||||
@@ -325,7 +289,6 @@
|
||||
"Disable Translation": "Tắt dịch",
|
||||
"Scroll": "Cuộn",
|
||||
"Overlap Pixels": "Chồng lấn pixel",
|
||||
"Click": "Nhấp",
|
||||
"System Language": "Ngôn ngữ hệ thống",
|
||||
"Security": "An ninh",
|
||||
"Allow JavaScript": "Cho phép JavaScript",
|
||||
@@ -338,14 +301,12 @@
|
||||
"Synced at {{time}}": "Đã đồng bộ lúc {{time}}",
|
||||
"Never synced": "Chưa từng đồng bộ",
|
||||
"Show Remaining Time": "Hiển thị thời gian còn lại",
|
||||
"Show Page Number": "Hiển thị số trang",
|
||||
"{{time}} min left in chapter": "{{time}} phút còn lại trong chương",
|
||||
"Override Book Color": "Đổi màu sách",
|
||||
"Login Required": "Cần đăng nhập",
|
||||
"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 +321,773 @@
|
||||
"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",
|
||||
"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": "<1>Còn </1><0>{{count}}</0><1> trang trong chương</1>",
|
||||
"Show Remaining Pages": "Hiển thị trang còn lại",
|
||||
"Source Han Serif CN": "Source Han Serif",
|
||||
"Huiwen-MinchoGBK": "Huiwen Mincho",
|
||||
"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:",
|
||||
"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}}",
|
||||
"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ử",
|
||||
"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",
|
||||
"TTS not supported for this document": "TTS không được hỗ trợ cho tài liệu này",
|
||||
"Reset Password": "Đặt lại mật khẩu",
|
||||
"Show Reading Progress": "Hiển thị tiến độ đọc",
|
||||
"Reading Progress Style": "Phong cách tiến độ đọc",
|
||||
"Page Number": "Số trang",
|
||||
"Percentage": "Phần trăm",
|
||||
"Deleted local copy of the book: {{title}}": "Đã xóa bản sao cục bộ của sách: {{title}}",
|
||||
"Failed to delete cloud backup of the book: {{title}}": "Không thể xóa bản sao lưu đám mây của sách: {{title}}",
|
||||
"Failed to delete local copy of the book: {{title}}": "Không thể xóa bản sao cục bộ của sách: {{title}}",
|
||||
"Are you sure to delete the local copy of the selected book?": "Bạn có chắc chắn muốn xóa bản sao cục bộ của cuốn sách đã chọn không?",
|
||||
"Remove from Cloud & Device": "Xóa khỏi Đám mây & Thiết bị",
|
||||
"Remove from Cloud Only": "Xóa chỉ khỏi Đám mây",
|
||||
"Remove from Device Only": "Xóa chỉ khỏi Thiết bị",
|
||||
"Disconnected": "Mất kết nối",
|
||||
"KOReader Sync Settings": "Cài đặt đồng bộ KOReader",
|
||||
"Sync Strategy": "Chiến lược đồng bộ",
|
||||
"Ask on conflict": "Hỏi khi có xung đột",
|
||||
"Always use latest": "Luôn sử dụng phiên bản mới nhất",
|
||||
"Send changes only": "Chỉ gửi thay đổi",
|
||||
"Receive changes only": "Chỉ nhận thay đổi",
|
||||
"Checksum Method": "Phương pháp kiểm tra",
|
||||
"File Content (recommended)": "Nội dung tệp (được khuyến nghị)",
|
||||
"File Name": "Tên tệp",
|
||||
"Device Name": "Tên thiết bị",
|
||||
"Connect to your KOReader Sync server.": "Kết nối với máy chủ đồng bộ KOReader của bạn.",
|
||||
"Server URL": "URL máy chủ",
|
||||
"Username": "Tên người dùng",
|
||||
"Your Username": "Tên người dùng của bạn",
|
||||
"Password": "Mật khẩu",
|
||||
"Connect": "Kết nối",
|
||||
"KOReader Sync": "Đồng bộ KOReader",
|
||||
"Sync Conflict": "Xung đột đồng bộ",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "Đồng bộ tiến độ đọc từ \"{{deviceName}}\"?",
|
||||
"another device": "thiết bị khác",
|
||||
"Local Progress": "Tiến độ cục bộ",
|
||||
"Remote Progress": "Tiến độ từ xa",
|
||||
"Page {{page}} of {{total}} ({{percentage}}%)": "Trang {{page}} của {{total}} ({{percentage}}%)",
|
||||
"Current position": "Vị trí hiện tại",
|
||||
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "Khoảng trang {{page}} của {{total}} ({{percentage}}%)",
|
||||
"Approximately {{percentage}}%": "Khoảng {{percentage}}%",
|
||||
"Failed to connect": "Không thể kết nối",
|
||||
"Sync Server Connected": "Máy chủ đồng bộ đã kết nối",
|
||||
"Sync as {{userDisplayName}}": "Đồng bộ dưới tên {{userDisplayName}}",
|
||||
"Custom Fonts": "Phông Chữ Tùy Chỉnh",
|
||||
"Cancel Delete": "Hủy Xóa",
|
||||
"Import Font": "Nhập Phông Chữ",
|
||||
"Delete Font": "Xóa Phông Chữ",
|
||||
"Tips": "Mẹo",
|
||||
"Custom fonts can be selected from the Font Face menu": "Phông chữ tùy chỉnh có thể được chọn từ menu Phông Chữ",
|
||||
"Manage Custom Fonts": "Quản Lý Phông Chữ Tùy Chỉnh",
|
||||
"Select Files": "Chọn Tệp",
|
||||
"Select Image": "Chọn Hình Ảnh",
|
||||
"Select Video": "Chọn Video",
|
||||
"Select Audio": "Chọn Âm Thanh",
|
||||
"Select Fonts": "Chọn Phông Chữ",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Định dạng phông chữ được hỗ trợ: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Đẩy tiến độ",
|
||||
"Pull Progress": "Kéo tiến độ",
|
||||
"Previous Paragraph": "Đoạn trước",
|
||||
"Previous Sentence": "Câu trước",
|
||||
"Pause": "Tạm dừng",
|
||||
"Play": "Phát",
|
||||
"Next Sentence": "Câu tiếp theo",
|
||||
"Next Paragraph": "Đoạn tiếp theo",
|
||||
"Separate Cover Page": "Trang Bìa Riêng",
|
||||
"Resize Notebook": "Thay Đổi Kích Thước Sổ Ghi Chép",
|
||||
"Resize Sidebar": "Thay Đổi Kích Thước Thanh Bên",
|
||||
"Get Help from the Readest Community": "Nhận Trợ Giúp từ Cộng Đồng Readest",
|
||||
"Remove cover image": "Xóa ảnh bìa",
|
||||
"Bookshelf": "Kệ sách",
|
||||
"View Menu": "Xem menu",
|
||||
"Settings Menu": "Menu cài đặt",
|
||||
"View account details and quota": "Xem chi tiết tài khoản và hạn mức",
|
||||
"Library Header": "Tiêu đề thư viện",
|
||||
"Book Content": "Nội dung sách",
|
||||
"Footer Bar": "Thanh chân trang",
|
||||
"Header Bar": "Thanh đầu trang",
|
||||
"View Options": "Tùy chọn xem",
|
||||
"Book Menu": "Menu sách",
|
||||
"Search Options": "Tùy chọn tìm kiếm",
|
||||
"Close": "Đóng",
|
||||
"Delete Book Options": "Tùy chọn xóa sách",
|
||||
"ON": "BẬT",
|
||||
"OFF": "TẮT",
|
||||
"Reading Progress": "Tiến độ đọc",
|
||||
"Page Margin": "Lề trang",
|
||||
"Remove Bookmark": "Xóa đánh dấu",
|
||||
"Add Bookmark": "Thêm đánh dấu",
|
||||
"Books Content": "Nội dung sách",
|
||||
"Jump to Location": "Nhảy đến vị trí",
|
||||
"Unpin Notebook": "Bỏ ghim sổ tay",
|
||||
"Pin Notebook": "Ghim sổ tay",
|
||||
"Hide Search Bar": "Ẩn thanh tìm kiếm",
|
||||
"Show Search Bar": "Hiện thanh tìm kiếm",
|
||||
"On {{current}} of {{total}} page": "Trên {{current}} của {{total}} trang",
|
||||
"Section Title": "Tiêu đề phần",
|
||||
"Decrease": "Giảm",
|
||||
"Increase": "Tăng",
|
||||
"Settings Panels": "Các bảng điều khiển cài đặt",
|
||||
"Settings": "Cài đặt",
|
||||
"Unpin Sidebar": "Bỏ ghim thanh bên",
|
||||
"Pin Sidebar": "Ghim thanh bên",
|
||||
"Toggle Sidebar": "Chuyển đổi thanh bên",
|
||||
"Toggle Translation": "Chuyển đổi dịch thuật",
|
||||
"Translation Disabled": "Dịch thuật đã tắt",
|
||||
"Minimize": "Thu nhỏ",
|
||||
"Maximize or Restore": "Tối đa hóa hoặc Khôi phục",
|
||||
"Exit Parallel Read": "Thoát Đọc Song Song",
|
||||
"Enter Parallel Read": "Vào Đọc Song Song",
|
||||
"Zoom Level": "Độ phóng to",
|
||||
"Zoom Out": "Thu nhỏ",
|
||||
"Reset Zoom": "Đặt lại độ phóng to",
|
||||
"Zoom In": "Phóng to",
|
||||
"Zoom Mode": "Chế độ phóng to",
|
||||
"Single Page": "Một trang",
|
||||
"Auto Spread": "Tự động trải rộng",
|
||||
"Fit Page": "Vừa trang",
|
||||
"Fit Width": "Vừa chiều rộng",
|
||||
"Failed to select directory": "Không thể chọn thư mục",
|
||||
"The new data directory must be different from the current one.": "Thư mục dữ liệu mới phải khác với thư mục hiện tại.",
|
||||
"Migration failed: {{error}}": "Di chuyển không thành công: {{error}}",
|
||||
"Change Data Location": "Thay đổi vị trí dữ liệu",
|
||||
"Current Data Location": "Vị trí dữ liệu hiện tại",
|
||||
"Total size: {{size}}": "Tổng kích thước: {{size}}",
|
||||
"Calculating file info...": "Đang tính toán thông tin tệp...",
|
||||
"New Data Location": "Vị trí dữ liệu mới",
|
||||
"Choose Different Folder": "Chọn thư mục khác",
|
||||
"Choose New Folder": "Chọn thư mục mới",
|
||||
"Migrating data...": "Đang di chuyển dữ liệu...",
|
||||
"Copying: {{file}}": "Đang sao chép: {{file}}",
|
||||
"{{current}} of {{total}} files": "{{current}} của {{total}} tệp",
|
||||
"Migration completed successfully!": "Di chuyển hoàn tất thành công!",
|
||||
"Your data has been moved to the new location. Please restart the application to complete the process.": "Dữ liệu của bạn đã được chuyển đến vị trí mới. Vui lòng khởi động lại ứng dụng để hoàn tất quá trình.",
|
||||
"Migration failed": "Di chuyển không thành công",
|
||||
"Important Notice": "Thông báo quan trọng",
|
||||
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Điều này sẽ di chuyển tất cả dữ liệu ứng dụng của bạn đến vị trí mới. Hãy chắc chắn rằng đích đến có đủ không gian trống.",
|
||||
"Restart App": "Khởi động lại ứng dụng",
|
||||
"Start Migration": "Bắt đầu di chuyển",
|
||||
"Advanced Settings": "Cài đặt nâng cao",
|
||||
"File count: {{size}}": "Số lượng tệp: {{size}}",
|
||||
"Background Read Aloud": "Đọc To Nền",
|
||||
"Ready to read aloud": "Sẵn sàng để đọc to",
|
||||
"Read Aloud": "Đọc To",
|
||||
"Screen Brightness": "Độ sáng màn hình",
|
||||
"Background Image": "Ảnh nền",
|
||||
"Import Image": "Nhập ảnh",
|
||||
"Opacity": "Độ mờ",
|
||||
"Size": "Kích thước",
|
||||
"Cover": "Bìa",
|
||||
"Contain": "Chứa",
|
||||
"{{number}} pages left in chapter": "<1>Còn </1><0>{{number}}</0><1> trang trong chương</1>",
|
||||
"Device": "Thiết bị",
|
||||
"E-Ink Mode": "Chế độ E-Ink",
|
||||
"Highlight Colors": "Màu nổi bật",
|
||||
"Pagination": "Phân trang",
|
||||
"Disable Double Tap": "Vô hiệu hóa chạm hai lần",
|
||||
"Tap to Paginate": "Chạm để phân trang",
|
||||
"Click to Paginate": "Nhấp để phân trang",
|
||||
"Tap Both Sides": "Chạm vào cả hai bên",
|
||||
"Click Both Sides": "Nhấp vào cả hai bên",
|
||||
"Swap Tap Sides": "Đổi bên chạm",
|
||||
"Swap Click Sides": "Đổi bên nhấp",
|
||||
"Source and Translated": "Nguồn và Đã dịch",
|
||||
"Translated Only": "Chỉ Đã dịch",
|
||||
"Source Only": "Chỉ Nguồn",
|
||||
"TTS Text": "Văn bản TTS",
|
||||
"The book file is corrupted": "Tệp sách bị hỏng",
|
||||
"The book file is empty": "Tệp sách trống",
|
||||
"Failed to open the book file": "Không thể mở tệp sách",
|
||||
"On-Demand Purchase": "Mua theo yêu cầu",
|
||||
"Full Customization": "Tùy chỉnh đầy đủ",
|
||||
"Lifetime Plan": "Kế hoạch trọn đời",
|
||||
"One-Time Payment": "Thanh toán một lần",
|
||||
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Thực hiện một khoản thanh toán duy nhất để tận hưởng quyền truy cập trọn đời vào các tính năng cụ thể trên tất cả các thiết bị. Mua các tính năng hoặc dịch vụ cụ thể chỉ khi bạn cần chúng.",
|
||||
"Expand Cloud Sync Storage": "Mở rộng dung lượng lưu trữ đồng bộ hóa đám mây",
|
||||
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Mở rộng dung lượng lưu trữ đám mây của bạn mãi mãi với một lần mua. Mỗi lần mua thêm sẽ thêm không gian.",
|
||||
"Unlock All Customization Options": "Mở khóa tất cả tùy chọn tùy chỉnh",
|
||||
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Mở khóa các chủ đề, phông chữ, tùy chọn bố cục và đọc to, dịch giả, dịch vụ lưu trữ đám mây bổ sung.",
|
||||
"Purchase Successful!": "Mua hàng thành công!",
|
||||
"lifetime": "trọn đời",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Cảm ơn bạn đã mua hàng! Thanh toán của bạn đã được xử lý thành công.",
|
||||
"Order ID:": "Mã đơn hàng:",
|
||||
"TTS Highlighting": "Đánh dấu TTS",
|
||||
"Style": "Kiểu",
|
||||
"Underline": "Gạch chân",
|
||||
"Strikethrough": "Gạch ngang",
|
||||
"Squiggly": "Gạch ngoằn ngoèo",
|
||||
"Outline": "Đường viền",
|
||||
"Save Current Color": "Lưu màu hiện tại",
|
||||
"Quick Colors": "Màu nhanh",
|
||||
"Highlighter": "Bút đánh dấu",
|
||||
"Save Book Cover": "Lưu bìa sách",
|
||||
"Auto-save last book cover": "Tự động lưu bìa sách cuối cùng",
|
||||
"Back": "Quay lại",
|
||||
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Email xác nhận đã được gửi! Vui lòng kiểm tra địa chỉ email cũ và mới của bạn để xác nhận thay đổi.",
|
||||
"Failed to update email": "Cập nhật email thất bại",
|
||||
"New Email": "Email mới",
|
||||
"Your new email": "Email mới của bạn",
|
||||
"Updating email ...": "Đang cập nhật email ...",
|
||||
"Update email": "Cập nhật email",
|
||||
"Current email": "Email hiện tại",
|
||||
"Update Email": "Cập nhật email",
|
||||
"All": "Tất cả",
|
||||
"Unable to open book": "Không thể mở sách",
|
||||
"Punctuation": "Dấu câu",
|
||||
"Replace Quotation Marks": "Thay thế dấu ngoặc kép",
|
||||
"Enabled only in vertical layout.": "Chỉ bật trong bố cục dọc.",
|
||||
"No Conversion": "Không chuyển đổi",
|
||||
"Simplified to Traditional": "Giản thể → Phồn thể",
|
||||
"Traditional to Simplified": "Phồn thể → Giản thể",
|
||||
"Simplified to Traditional (Taiwan)": "Giản thể → Phồn thể (Đài Loan)",
|
||||
"Simplified to Traditional (Hong Kong)": "Giản thể → Phồn thể (Hồng Kông)",
|
||||
"Simplified to Traditional (Taiwan), with phrases": "Giản thể → Phồn thể (Đài Loan • cụm từ)",
|
||||
"Traditional (Taiwan) to Simplified": "Phồn thể (Đài Loan) → Giản thể",
|
||||
"Traditional (Hong Kong) to Simplified": "Phồn thể (Hồng Kông) → Giản thể",
|
||||
"Traditional (Taiwan) to Simplified, with phrases": "Phồn thể (Đài Loan • cụm từ) → Giản thể",
|
||||
"Convert Simplified and Traditional Chinese": "Chuyển đổi Trung giản/Phồn thể",
|
||||
"Convert Mode": "Chế độ chuyển đổi",
|
||||
"Failed to auto-save book cover for lock screen: {{error}}": "Không thể tự động lưu bìa sách cho màn hình khóa: {{error}}",
|
||||
"Download from Cloud": "Tải xuống từ đám mây",
|
||||
"Upload to Cloud": "Tải lên đám mây",
|
||||
"Clear Custom Fonts": "Xóa phông chữ tùy chỉnh",
|
||||
"Columns": "Cột",
|
||||
"OPDS Catalogs": "Danh mục OPDS",
|
||||
"Adding LAN addresses is not supported in the web app version.": "Việc thêm địa chỉ LAN không được hỗ trợ trong phiên bản web.",
|
||||
"Invalid OPDS catalog. Please check the URL.": "Danh mục OPDS không hợp lệ. Vui lòng kiểm tra URL.",
|
||||
"Browse and download books from online catalogs": "Duyệt và tải sách từ các danh mục trực tuyến",
|
||||
"My Catalogs": "Danh mục của tôi",
|
||||
"Add Catalog": "Thêm danh mục",
|
||||
"No catalogs yet": "Chưa có danh mục nào",
|
||||
"Add your first OPDS catalog to start browsing books": "Thêm danh mục OPDS đầu tiên để bắt đầu duyệt sách",
|
||||
"Add Your First Catalog": "Thêm danh mục đầu tiên",
|
||||
"Browse": "Duyệt",
|
||||
"Popular Catalogs": "Danh mục phổ biến",
|
||||
"Add": "Thêm",
|
||||
"Add OPDS Catalog": "Thêm danh mục OPDS",
|
||||
"Catalog Name": "Tên danh mục",
|
||||
"My Calibre Library": "Thư viện Calibre của tôi",
|
||||
"OPDS URL": "URL OPDS",
|
||||
"Username (optional)": "Tên đăng nhập (tùy chọn)",
|
||||
"Password (optional)": "Mật khẩu (tùy chọn)",
|
||||
"Description (optional)": "Mô tả (tùy chọn)",
|
||||
"A brief description of this catalog": "Mô tả ngắn gọn về danh mục này",
|
||||
"Validating...": "Đang xác thực...",
|
||||
"View All": "Xem tất cả",
|
||||
"Forward": "Tiếp",
|
||||
"Home": "Trang chủ",
|
||||
"{{count}} items_other": "{{count}} mục",
|
||||
"Download completed": "Tải xuống hoàn tất",
|
||||
"Download failed": "Tải xuống thất bại",
|
||||
"Open Access": "Truy cập mở",
|
||||
"Borrow": "Mượn",
|
||||
"Buy": "Mua",
|
||||
"Subscribe": "Đăng ký",
|
||||
"Sample": "Mẫu",
|
||||
"Download": "Tải xuống",
|
||||
"Open & Read": "Mở & Đọc",
|
||||
"Tags": "Thẻ",
|
||||
"Tag": "Thẻ",
|
||||
"First": "Đầu tiên",
|
||||
"Previous": "Trước",
|
||||
"Next": "Tiếp theo",
|
||||
"Last": "Cuối cùng",
|
||||
"Cannot Load Page": "Không thể tải trang",
|
||||
"An error occurred": "Đã xảy ra lỗi",
|
||||
"Online Library": "Thư viện trực tuyến",
|
||||
"URL must start with http:// or https://": "URL phải bắt đầu bằng http:// hoặc https://",
|
||||
"Title, Author, Tag, etc...": "Tiêu đề, Tác giả, Thẻ, v.v...",
|
||||
"Query": "Truy vấn",
|
||||
"Subject": "Chủ đề",
|
||||
"Enter {{terms}}": "Nhập {{terms}}",
|
||||
"No search results found": "Không tìm thấy kết quả tìm kiếm",
|
||||
"Failed to load OPDS feed: {{status}} {{statusText}}": "Không tải được nguồn cấp OPDS: {{status}} {{statusText}}",
|
||||
"Search in {{title}}": "Tìm kiếm trong {{title}}",
|
||||
"Manage Storage": "Quản lý bộ nhớ",
|
||||
"Failed to load files": "Không tải được tệp",
|
||||
"Deleted {{count}} file(s)_other": "Đã xóa {{count}} tệp",
|
||||
"Failed to delete {{count}} file(s)_other": "Không xóa được {{count}} tệp",
|
||||
"Failed to delete files": "Không xóa được tệp",
|
||||
"Total Files": "Tổng số tệp",
|
||||
"Total Size": "Tổng dung lượng",
|
||||
"Quota": "Dung lượng cho phép",
|
||||
"Used": "Đã sử dụng",
|
||||
"Files": "Tệp",
|
||||
"Search files...": "Tìm tệp...",
|
||||
"Newest First": "Mới nhất trước",
|
||||
"Oldest First": "Cũ nhất trước",
|
||||
"Largest First": "Lớn nhất trước",
|
||||
"Smallest First": "Nhỏ nhất trước",
|
||||
"Name A-Z": "Tên A-Z",
|
||||
"Name Z-A": "Tên Z-A",
|
||||
"{{count}} selected_other": "Đã chọn {{count}} tệp",
|
||||
"Delete Selected": "Xóa các mục đã chọn",
|
||||
"Created": "Đã tạo",
|
||||
"No files found": "Không tìm thấy tệp",
|
||||
"No files uploaded yet": "Chưa tải lên tệp nào",
|
||||
"files": "tệp",
|
||||
"Page {{current}} of {{total}}": "Trang {{current}} trên {{total}}",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "Bạn có chắc muốn xóa {{count}} tệp đã chọn không?",
|
||||
"Cloud Storage Usage": "Sử dụng lưu trữ đám mây",
|
||||
"Rename Group": "Đổi tên nhóm",
|
||||
"From Directory": "Từ thư mục",
|
||||
"Successfully imported {{count}} book(s)_other": "Đã nhập thành công {{count}} sách",
|
||||
"Count": "Số lượng",
|
||||
"Start Page": "Trang bắt đầu",
|
||||
"Search in OPDS Catalog...": "Tìm kiếm trong danh mục OPDS...",
|
||||
"Please log in to use advanced TTS features": "Vui lòng đăng nhập để sử dụng các tính năng TTS nâng cao",
|
||||
"Word limit of 30 words exceeded.": "Đã vượt quá giới hạn 30 từ.",
|
||||
"Proofread": "Hiệu đính",
|
||||
"Current selection": "Vùng chọn hiện tại",
|
||||
"All occurrences in this book": "Tất cả các lần xuất hiện trong sách này",
|
||||
"All occurrences in your library": "Tất cả các lần xuất hiện trong thư viện của bạn",
|
||||
"Selected text:": "Văn bản đã chọn:",
|
||||
"Replace with:": "Thay thế bằng:",
|
||||
"Enter text...": "Nhập văn bản…",
|
||||
"Case sensitive:": "Phân biệt chữ hoa/thường:",
|
||||
"Scope:": "Phạm vi:",
|
||||
"Selection": "Vùng chọn",
|
||||
"Library": "Thư viện",
|
||||
"Yes": "Có",
|
||||
"No": "Không",
|
||||
"Proofread Replacement Rules": "Quy tắc thay thế hiệu đính",
|
||||
"Selected Text Rules": "Quy tắc cho văn bản đã chọn",
|
||||
"No selected text replacement rules": "Không có quy tắc thay thế cho văn bản đã chọn",
|
||||
"Book Specific Rules": "Quy tắc dành riêng cho sách",
|
||||
"No book-level replacement rules": "Không có quy tắc thay thế ở cấp sách",
|
||||
"Disable Quick Action": "Vô hiệu hóa Hành động Nhanh",
|
||||
"Enable Quick Action on Selection": "Bật Hành động Nhanh khi chọn",
|
||||
"None": "Không có",
|
||||
"Annotation Tools": "Công cụ chú thích",
|
||||
"Enable Quick Actions": "Bật Hành động Nhanh",
|
||||
"Quick Action": "Hành động Nhanh",
|
||||
"Copy to Notebook": "Sao chép vào Sổ tay",
|
||||
"Copy text after selection": "Sao chép văn bản sau khi chọn",
|
||||
"Highlight text after selection": "Đánh dấu văn bản sau khi chọn",
|
||||
"Annotate text after selection": "Chú thích văn bản sau khi chọn",
|
||||
"Search text after selection": "Tìm kiếm văn bản sau khi chọn",
|
||||
"Look up text in dictionary after selection": "Tra cứu văn bản trong từ điển sau khi chọn",
|
||||
"Look up text in Wikipedia after selection": "Tra cứu văn bản trong Wikipedia sau khi chọn",
|
||||
"Translate text after selection": "Dịch văn bản sau khi chọn",
|
||||
"Read text aloud after selection": "Đọc to văn bản sau khi chọn",
|
||||
"Proofread text after selection": "Hiệu đính văn bản sau khi chọn",
|
||||
"{{activeCount}} active, {{pendingCount}} pending": "{{activeCount}} đang hoạt động, {{pendingCount}} đang chờ",
|
||||
"{{failedCount}} failed": "{{failedCount}} thất bại",
|
||||
"Waiting...": "Đang chờ...",
|
||||
"Failed": "Thất bại",
|
||||
"Completed": "Hoàn thành",
|
||||
"Cancelled": "Đã hủy",
|
||||
"Retry": "Thử lại",
|
||||
"Active": "Đang hoạt động",
|
||||
"Transfer Queue": "Hàng đợi truyền",
|
||||
"Upload All": "Tải lên tất cả",
|
||||
"Download All": "Tải xuống tất cả",
|
||||
"Resume Transfers": "Tiếp tục truyền",
|
||||
"Pause Transfers": "Tạm dừng truyền",
|
||||
"Pending": "Đang chờ",
|
||||
"No transfers": "Không có truyền",
|
||||
"Retry All": "Thử lại tất cả",
|
||||
"Clear Completed": "Xóa đã hoàn thành",
|
||||
"Clear Failed": "Xóa thất bại",
|
||||
"Upload queued: {{title}}": "Tải lên trong hàng đợi: {{title}}",
|
||||
"Download queued: {{title}}": "Tải xuống trong hàng đợi: {{title}}",
|
||||
"Book not found in library": "Không tìm thấy sách trong thư viện",
|
||||
"Unknown error": "Lỗi không xác định",
|
||||
"Please log in to continue": "Vui lòng đăng nhập để tiếp tục",
|
||||
"Cloud File Transfers": "Truyền tệp đám mây",
|
||||
"Show Search Results": "Hiển thị kết quả tìm kiếm",
|
||||
"Search results for '{{term}}'": "Kết quả cho '{{term}}'",
|
||||
"Close Search": "Đóng tìm kiếm",
|
||||
"Previous Result": "Kết quả trước",
|
||||
"Next Result": "Kết quả tiếp theo",
|
||||
"Bookmarks": "Dấu trang",
|
||||
"Annotations": "Chú thích",
|
||||
"Show Results": "Hiển thị kết quả",
|
||||
"Clear search": "Xóa tìm kiếm",
|
||||
"Clear search history": "Xóa lịch sử tìm kiếm",
|
||||
"Tap to Toggle Footer": "Nhấn để bật/tắt chân trang",
|
||||
"Show Current Time": "Hiển thị thời gian hiện tại",
|
||||
"Use 24 Hour Clock": "Sử dụng định dạng 24 giờ",
|
||||
"Show Current Battery Status": "Hiển thị trạng thái pin hiện tại",
|
||||
"Exported successfully": "Xuất thành công",
|
||||
"Book exported successfully.": "Sách đã được xuất thành công.",
|
||||
"Failed to export the book.": "Xuất sách thất bại.",
|
||||
"Export Book": "Xuất sách",
|
||||
"Whole word:": "Toàn bộ từ:",
|
||||
"Error": "Lỗi",
|
||||
"Unable to load the article. Try searching directly on {{link}}.": "Không thể tải bài viết. Hãy thử tìm kiếm trực tiếp trên {{link}}.",
|
||||
"Unable to load the word. Try searching directly on {{link}}.": "Không thể tải từ. Hãy thử tìm kiếm trực tiếp trên {{link}}.",
|
||||
"Date Published": "Ngày xuất bản",
|
||||
"Only for TTS:": "Chỉ dành cho TTS:",
|
||||
"Uploaded": "Đã tải lên",
|
||||
"Downloaded": "Đã tải xuống",
|
||||
"Deleted": "Đã xóa",
|
||||
"Note:": "Ghi chú:",
|
||||
"Time:": "Thời gian:",
|
||||
"Format Options": "Tùy chọn định dạng",
|
||||
"Export Date": "Ngày xuất",
|
||||
"Chapter Titles": "Tiêu đề chương",
|
||||
"Chapter Separator": "Dấu phân cách chương",
|
||||
"Highlights": "Đánh dấu",
|
||||
"Note Date": "Ngày ghi chú",
|
||||
"Advanced": "Nâng cao",
|
||||
"Hide": "Ẩn",
|
||||
"Show": "Hiển thị",
|
||||
"Use Custom Template": "Sử dụng mẫu tùy chỉnh",
|
||||
"Export Template": "Mẫu xuất",
|
||||
"Template Syntax:": "Cú pháp mẫu:",
|
||||
"Insert value": "Chèn giá trị",
|
||||
"Format date (locale)": "Định dạng ngày (địa phương)",
|
||||
"Format date (custom)": "Định dạng ngày (tùy chỉnh)",
|
||||
"Conditional": "Điều kiện",
|
||||
"Loop": "Vòng lặp",
|
||||
"Available Variables:": "Biến có sẵn:",
|
||||
"Book title": "Tiêu đề sách",
|
||||
"Book author": "Tác giả sách",
|
||||
"Export date": "Ngày xuất",
|
||||
"Array of chapters": "Danh sách chương",
|
||||
"Chapter title": "Tiêu đề chương",
|
||||
"Array of annotations": "Danh sách chú thích",
|
||||
"Highlighted text": "Văn bản được đánh dấu",
|
||||
"Annotation note": "Ghi chú chú thích",
|
||||
"Date Format Tokens:": "Token định dạng ngày:",
|
||||
"Year (4 digits)": "Năm (4 chữ số)",
|
||||
"Month (01-12)": "Tháng (01-12)",
|
||||
"Day (01-31)": "Ngày (01-31)",
|
||||
"Hour (00-23)": "Giờ (00-23)",
|
||||
"Minute (00-59)": "Phút (00-59)",
|
||||
"Second (00-59)": "Giây (00-59)",
|
||||
"Show Source": "Hiển thị nguồn",
|
||||
"No content to preview": "Không có nội dung để xem trước",
|
||||
"Export": "Xuất",
|
||||
"Set Timeout": "Đặt thời gian chờ",
|
||||
"Select Voice": "Chọn giọng nói",
|
||||
"Toggle Sticky Bottom TTS Bar": "Bật/tắt thanh TTS cố định",
|
||||
"Display what I'm reading on Discord": "Hiển thị sách đang đọc trên Discord",
|
||||
"Show on Discord": "Hiện trên Discord",
|
||||
"Instant {{action}}": "{{action}} tức thì",
|
||||
"Instant {{action}} Disabled": "{{action}} tức thì đã tắt",
|
||||
"Annotation": "Chú thích",
|
||||
"Reset Template": "Đặt lại mẫu",
|
||||
"Annotation style": "Kiểu chú thích",
|
||||
"Annotation color": "Màu chú thích",
|
||||
"Annotation time": "Thời gian chú thích",
|
||||
"AI": "AI",
|
||||
"Are you sure you want to re-index this book?": "Bạn có chắc muốn lập chỉ mục lại cuốn sách này?",
|
||||
"Enable AI in Settings": "Bật AI trong Cài đặt",
|
||||
"Index This Book": "Lập chỉ mục sách này",
|
||||
"Enable AI search and chat for this book": "Bật tìm kiếm AI và trò chuyện cho cuốn sách này",
|
||||
"Start Indexing": "Bắt đầu lập chỉ mục",
|
||||
"Indexing book...": "Đang lập chỉ mục sách...",
|
||||
"Preparing...": "Đang chuẩn bị...",
|
||||
"Delete this conversation?": "Xóa cuộc trò chuyện này?",
|
||||
"No conversations yet": "Chưa có cuộc trò chuyện nào",
|
||||
"Start a new chat to ask questions about this book": "Bắt đầu cuộc trò chuyện mới để hỏi về cuốn sách này",
|
||||
"Rename": "Đổi tên",
|
||||
"New Chat": "Cuộc trò chuyện mới",
|
||||
"Chat": "Trò chuyện",
|
||||
"Please enter a model ID": "Vui lòng nhập ID mô hình",
|
||||
"Model not available or invalid": "Mô hình không khả dụng hoặc không hợp lệ",
|
||||
"Failed to validate model": "Xác thực mô hình thất bại",
|
||||
"Couldn't connect to Ollama. Is it running?": "Không thể kết nối với Ollama. Nó có đang chạy không?",
|
||||
"Invalid API key or connection failed": "Khóa API không hợp lệ hoặc kết nối thất bại",
|
||||
"Connection failed": "Kết nối thất bại",
|
||||
"AI Assistant": "Trợ lý AI",
|
||||
"Enable AI Assistant": "Bật trợ lý AI",
|
||||
"Provider": "Nhà cung cấp",
|
||||
"Ollama (Local)": "Ollama (Cục bộ)",
|
||||
"AI Gateway (Cloud)": "Cổng AI (Đám mây)",
|
||||
"Ollama Configuration": "Cấu hình Ollama",
|
||||
"Refresh Models": "Làm mới mô hình",
|
||||
"AI Model": "Mô hình AI",
|
||||
"No models detected": "Không phát hiện mô hình nào",
|
||||
"AI Gateway Configuration": "Cấu hình cổng AI",
|
||||
"Choose from a selection of high-quality, economical AI models. You can also bring your own model by selecting \"Custom Model\" below.": "Chọn từ các mô hình AI chất lượng cao, tiết kiệm. Bạn cũng có thể sử dụng mô hình riêng bằng cách chọn \"Mô hình tùy chỉnh\" bên dưới.",
|
||||
"API Key": "Khóa API",
|
||||
"Get Key": "Lấy khóa",
|
||||
"Model": "Mô hình",
|
||||
"Custom Model...": "Mô hình tùy chỉnh...",
|
||||
"Custom Model ID": "ID mô hình tùy chỉnh",
|
||||
"Validate": "Xác thực",
|
||||
"Model available": "Mô hình khả dụng",
|
||||
"Connection": "Kết nối",
|
||||
"Test Connection": "Kiểm tra kết nối",
|
||||
"Connected": "Đã kết nối",
|
||||
"Custom Colors": "Màu tùy chỉnh",
|
||||
"Color E-Ink Mode": "Chế độ E-Ink màu",
|
||||
"Reading Ruler": "Thước đọc",
|
||||
"Enable Reading Ruler": "Bật thước đọc",
|
||||
"Lines to Highlight": "Số dòng cần làm nổi bật",
|
||||
"Ruler Color": "Màu thước",
|
||||
"Command Palette": "Bảng lệnh",
|
||||
"Search settings and actions...": "Tìm kiếm cài đặt và hành động...",
|
||||
"No results found for": "Không tìm thấy kết quả cho",
|
||||
"Type to search settings and actions": "Nhập để tìm kiếm cài đặt và hành động",
|
||||
"Recent": "Gần đây",
|
||||
"navigate": "điều hướng",
|
||||
"select": "chọn",
|
||||
"close": "đóng",
|
||||
"Search Settings": "Tìm kiếm cài đặt",
|
||||
"Page Margins": "Lề trang",
|
||||
"AI Provider": "Nhà cung cấp AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Mô hình Ollama",
|
||||
"AI Gateway Model": "Mô hình AI Gateway",
|
||||
"Actions": "Hành động",
|
||||
"Navigation": "Điều hướng",
|
||||
"Set status for {{count}} book(s)_other": "Đặt trạng thái cho {{count}} cuốn sách",
|
||||
"Mark as Unread": "Đánh dấu là chưa đọc",
|
||||
"Mark as Finished": "Đánh dấu là đã hoàn thành",
|
||||
"Finished": "Đã hoàn thành",
|
||||
"Unread": "Chưa đọc",
|
||||
"Clear Status": "Xóa trạng thái",
|
||||
"Status": "Trạng thái",
|
||||
"Loading": "Đang tải...",
|
||||
"Exit Paragraph Mode": "Thoát chế độ đoạn văn",
|
||||
"Paragraph Mode": "Chế độ đoạn văn",
|
||||
"Embedding Model": "Mô hình nhúng",
|
||||
"{{count}} book(s) synced_other": "Đã đồng bộ {{count}} cuốn sách",
|
||||
"Unable to start RSVP": "Không thể bắt đầu RSVP",
|
||||
"RSVP not supported for PDF": "RSVP không được hỗ trợ cho PDF",
|
||||
"Select Chapter": "Chọn chương",
|
||||
"Context": "Ngữ cảnh",
|
||||
"Ready": "Sẵn sàng",
|
||||
"Chapter Progress": "Tiến độ chương",
|
||||
"words": "từ",
|
||||
"{{time}} left": "còn lại {{time}}",
|
||||
"Reading progress": "Tiến độ đọc",
|
||||
"Click to seek": "Nhấp để tua",
|
||||
"Skip back 15 words": "Lùi lại 15 từ",
|
||||
"Back 15 words (Shift+Left)": "Lùi lại 15 từ (Shift+Trái)",
|
||||
"Pause (Space)": "Tạm dừng (Khoảng trắng)",
|
||||
"Play (Space)": "Phát (Khoảng trắng)",
|
||||
"Skip forward 15 words": "Tiến tới 15 từ",
|
||||
"Forward 15 words (Shift+Right)": "Tiến tới 15 từ (Shift+Phải)",
|
||||
"Decrease speed": "Giảm tốc độ",
|
||||
"Slower (Left/Down)": "Chậm hơn (Trái/Xuống)",
|
||||
"Increase speed": "Tăng tốc độ",
|
||||
"Faster (Right/Up)": "Nhanh hơn (Phải/Lên)",
|
||||
"Start RSVP Reading": "Bắt đầu đọc RSVP",
|
||||
"Choose where to start reading": "Chọn nơi để bắt đầu đọc",
|
||||
"From Chapter Start": "Từ đầu chương",
|
||||
"Start reading from the beginning of the chapter": "Bắt đầu đọc lại từ đầu chương này",
|
||||
"Resume": "Tiếp tục",
|
||||
"Continue from where you left off": "Tiếp tục từ nơi bạn đã dừng lại",
|
||||
"From Current Page": "Từ trang hiện tại",
|
||||
"Start from where you are currently reading": "Bắt đầu từ nơi bạn hiện đang đọc",
|
||||
"From Selection": "Từ phần đã chọn",
|
||||
"Speed Reading Mode": "Chế độ đọc nhanh",
|
||||
"Scroll left": "Cuộn sang trái",
|
||||
"Scroll right": "Cuộn sang phải",
|
||||
"Library Sync Progress": "Tiến trình đồng bộ hóa thư viện",
|
||||
"Back to library": "Quay lại thư viện",
|
||||
"Group by...": "Nhóm theo...",
|
||||
"Export as Plain Text": "Xuất dưới dạng văn bản thuần túy",
|
||||
"Export as Markdown": "Xuất dưới dạng Markdown",
|
||||
"Show Page Navigation Buttons": "Nút điều hướng trang",
|
||||
"Page {{number}}": "Trang {{number}}",
|
||||
"highlight": "làm nổi bật",
|
||||
"underline": "gạch chân",
|
||||
"squiggly": "ngoằn ngoèo",
|
||||
"red": "đỏ",
|
||||
"violet": "tím",
|
||||
"blue": "xanh lam",
|
||||
"green": "xanh lá",
|
||||
"yellow": "vàng",
|
||||
"Select {{style}} style": "Chọn kiểu {{style}}",
|
||||
"Select {{color}} color": "Chọn màu {{color}}",
|
||||
"Close Book": "Đóng sách",
|
||||
"Speed Reading": "Đọc nhanh",
|
||||
"Close Speed Reading": "Đóng đọc nhanh",
|
||||
"Authors": "Tác giả",
|
||||
"Books": "Sách",
|
||||
"Groups": "Nhóm",
|
||||
"Back to TTS Location": "Quay lại vị trí TTS",
|
||||
"Metadata": "Siêu dữ liệu",
|
||||
"Image viewer": "Trình xem ảnh",
|
||||
"Previous Image": "Ảnh trước",
|
||||
"Next Image": "Ảnh sau",
|
||||
"Zoomed": "Đã thu phóng",
|
||||
"Zoom level": "Mức thu phóng",
|
||||
"Table viewer": "Trình xem bảng",
|
||||
"Unable to connect to Readwise. Please check your network connection.": "Không thể kết nối với Readwise. Vui lòng kiểm tra kết nối mạng của bạn.",
|
||||
"Invalid Readwise access token": "Mã xác thực Readwise không hợp lệ",
|
||||
"Disconnected from Readwise": "Đã ngắt kết nối với Readwise",
|
||||
"Never": "Không bao giờ",
|
||||
"Readwise Settings": "Cài đặt Readwise",
|
||||
"Connected to Readwise": "Đã kết nối với Readwise",
|
||||
"Last synced: {{time}}": "Lần đồng bộ cuối: {{time}}",
|
||||
"Sync Enabled": "Đã bật đồng bộ",
|
||||
"Disconnect": "Ngắt kết nối",
|
||||
"Connect your Readwise account to sync highlights.": "Kết nối tài khoản Readwise của bạn để đồng bộ các phần đánh dấu.",
|
||||
"Get your access token at": "Lấy mã xác thực tại",
|
||||
"Access Token": "Mã xác thực",
|
||||
"Paste your Readwise access token": "Dán mã xác thực Readwise của bạn vào đây",
|
||||
"Config": "Cấu hình",
|
||||
"Readwise Sync": "Đồng bộ Readwise",
|
||||
"Push Highlights": "Đẩy các phần đánh dấu",
|
||||
"Highlights synced to Readwise": "Các phần đánh dấu đã được đồng bộ với Readwise",
|
||||
"Readwise sync failed: no internet connection": "Đồng bộ Readwise thất bại: không có kết nối internet",
|
||||
"Readwise sync failed: {{error}}": "Đồng bộ Readwise thất bại: {{error}}",
|
||||
"System Screen Brightness": "Độ sáng màn hình hệ thống",
|
||||
"Page:": "Trang:",
|
||||
"Page: {{number}}": "Trang: {{number}}",
|
||||
"Annotation page number": "Số trang chú thích",
|
||||
"Translating...": "Đang dịch...",
|
||||
"Show Battery Percentage": "Hiển thị phần trăm pin",
|
||||
"Hide Scrollbar": "Ẩn thanh cuộn",
|
||||
"Skip to last reading position": "Chuyển đến vị trí đọc cuối",
|
||||
"Show context": "Hiển thị ngữ cảnh",
|
||||
"Hide context": "Ẩn ngữ cảnh",
|
||||
"Decrease font size": "Giảm cỡ chữ",
|
||||
"Increase font size": "Tăng cỡ chữ",
|
||||
"Focus": "Tiêu điểm",
|
||||
"Theme color": "Màu chủ đề",
|
||||
"Import failed": "Nhập thất bại",
|
||||
"Available Formatters:": "Bộ định dạng có sẵn:",
|
||||
"Format date": "Định dạng ngày",
|
||||
"Markdown block quote (> per line)": "Trích dẫn khối Markdown (> mỗi dòng)",
|
||||
"Newlines to <br>": "Xuống dòng thành <br>",
|
||||
"Change case": "Đổi chữ hoa/thường",
|
||||
"Trim whitespace": "Cắt khoảng trắng",
|
||||
"Truncate to n characters": "Cắt ngắn còn n ký tự",
|
||||
"Replace text": "Thay thế văn bản",
|
||||
"Fallback value": "Giá trị dự phòng",
|
||||
"Get length": "Lấy độ dài",
|
||||
"First/last element": "Phần tử đầu/cuối",
|
||||
"Join array": "Nối mảng",
|
||||
"Backup failed: {{error}}": "Sao lưu thất bại: {{error}}",
|
||||
"Select Backup": "Chọn bản sao lưu",
|
||||
"Restore failed: {{error}}": "Khôi phục thất bại: {{error}}",
|
||||
"Backup & Restore": "Sao lưu & Khôi phục",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Tạo bản sao lưu thư viện hoặc khôi phục từ bản sao lưu trước đó. Khôi phục sẽ hợp nhất với thư viện hiện tại của bạn.",
|
||||
"Backup Library": "Sao lưu thư viện",
|
||||
"Restore Library": "Khôi phục thư viện",
|
||||
"Creating backup...": "Đang tạo bản sao lưu...",
|
||||
"Restoring library...": "Đang khôi phục thư viện...",
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} mục",
|
||||
"Backup completed successfully!": "Sao lưu thành công!",
|
||||
"Restore completed successfully!": "Khôi phục thành công!",
|
||||
"Your library has been saved to the selected location.": "Thư viện của bạn đã được lưu tại vị trí đã chọn.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Đã thêm {{added}} sách, cập nhật {{updated}} sách.",
|
||||
"Operation failed": "Thao tác thất bại",
|
||||
"Loading library...": "Đang tải thư viện...",
|
||||
"{{count}} books refreshed_other": "Đã làm mới {{count}} sách",
|
||||
"Failed to refresh metadata": "Không thể làm mới siêu dữ liệu",
|
||||
"Refresh Metadata": "Làm mới siêu dữ liệu"
|
||||
}
|
||||
|
||||