Compare commits

...

28 Commits

Author SHA1 Message Date
Rajarshee Chatterjee ae8d055689 perf: Limit Chapter Neighbor Queries To One Row
Lint / ESLint (push) Has been cancelled
Tests / Jest (push) Has been cancelled
Type Check / TypeScript (push) Has been cancelled
2026-09-26 20:43:28 +05:30
trietng 1607f80a0a feat: Add Volume Button Navigation and Slide Animation Settings for Paged Reading (#2058)
* feat: volume navigation and disable animation settings for paged reading

* fix: transition style removal

* fix: pageReader incorrect initialization

* fix: remove unneeded check

* chore: update Vietnamese translations for paged reading settings
2026-09-26 20:40:43 +05:30
github-actions[bot] a0c20e209c chore: Release v2.1.4 2026-09-26 07:14:58 +00:00
Rajarshee Chatterjee 648477931b feat: Add Version and Build Information to Error Fallback 2026-09-26 11:05:34 +05:30
Palloxin e42e8f66f2 fix: Improve Code Editor Newline Regex (#2021)
* Code Editor, New Lines regex improvement

* Update SimpleCodeEditor.tsx
2026-09-25 11:15:13 +05:30
Palloxin de5358727a fix: Restore Broken Preview to Previous State (#2004)
* Fix broken preview

* Optimized base64 image (reduced)

* fix image base64
2026-09-25 11:06:24 +05:30
Rajarshee Chatterjee 706459783f feat: Add Optional Continue Reading Buttons To Library 2026-09-23 18:55:51 +05:30
Patrick Loser 1ca9a679cf perf: Optimize Background Task Memory Usage and Lifecycle Management (#2059)
* fix: bound background task memory usage

* Fix background task cancellation and updates

* Update TaskActionReceiver.kt
2026-09-23 18:41:50 +05:30
Rajarshee Chatterjee 920f88aee4 fix: Resolve Jest CI Failures 2026-09-23 12:08:49 +05:30
Rajarshee Chatterjee cef3ae5bdf feat: Polish Statistics Screen Layout 2026-09-23 11:53:50 +05:30
Rajarshee Chatterjee 39c788e576 feat: Improve Genre Taxonomy Screen UX 2026-09-23 11:26:21 +05:30
Rajarshee Chatterjee d40713ac99 feat: Improve Custom Code UX 2026-09-23 10:41:38 +05:30
Rajarshee Chatterjee f53bed7190 chore: Update Translations (#1981)
* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

* New translations strings.json (Serbian (Cyrillic))

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

* New translations strings.json (Serbian (Cyrillic))

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

* New translations strings.json (Serbian (Cyrillic))

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

* New translations strings.json (Serbian (Cyrillic))

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

* New translations strings.json (Serbian (Cyrillic))

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

* New translations strings.json (Serbian (Cyrillic))

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* fix: Restore Overwritten Locale Translations

* New translations strings.json (Romanian)

[ci skip]
[ci skip]

* New translations strings.json (French)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Afrikaans)

[ci skip]
[ci skip]

* New translations strings.json (Arabic)

[ci skip]
[ci skip]

* New translations strings.json (Catalan)

[ci skip]
[ci skip]

* New translations strings.json (Czech)

[ci skip]
[ci skip]

* New translations strings.json (Korean)

[ci skip]
[ci skip]

* New translations strings.json (Indonesian)

[ci skip]
[ci skip]

* New translations strings.json (Danish)

[ci skip]
[ci skip]

* New translations strings.json (German)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Finnish)

[ci skip]
[ci skip]

* New translations strings.json (Hebrew)

[ci skip]
[ci skip]

* New translations strings.json (Hungarian)

[ci skip]
[ci skip]

* New translations strings.json (Italian)

[ci skip]
[ci skip]

* New translations strings.json (Japanese)

[ci skip]
[ci skip]

* New translations strings.json (Dutch)

[ci skip]
[ci skip]

* New translations strings.json (Norwegian)

[ci skip]
[ci skip]

* New translations strings.json (Polish)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* New translations strings.json (Albanian)

[ci skip]
[ci skip]

* New translations strings.json (Serbian (Cyrillic))

[ci skip]
[ci skip]

* New translations strings.json (Swedish)

[ci skip]
[ci skip]

* New translations strings.json (Turkish)

[ci skip]
[ci skip]

* New translations strings.json (Ukrainian)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Simplified)

[ci skip]
[ci skip]

* New translations strings.json (Chinese Traditional)

[ci skip]
[ci skip]

* New translations strings.json (Vietnamese)

[ci skip]
[ci skip]

* New translations strings.json (Portuguese, Brazilian)

[ci skip]
[ci skip]

* New translations strings.json (Hindi)

[ci skip]
[ci skip]

* New translations strings.json (Assamese)

[ci skip]
[ci skip]

* New translations strings.json (Odia)

[ci skip]
[ci skip]

* New translations strings.json (Spanish)

[ci skip]
[ci skip]

* New translations strings.json (Greek)

[ci skip]
[ci skip]

* New translations strings.json (Russian)

[ci skip]
[ci skip]

* fix: Restore Overwritten Locale Translations
2026-09-22 10:06:03 +05:30
Rajarshee Chatterjee 0b9c415681 fix: Prevent Chapter Drawer Blank Flash On Open (Closes #2038) (#2048)
Opening the reader's chapter drawer flashed an empty panel before the
chapter list appeared, most visibly on slower devices.

Three things combined to cause it:

- The panel carried `backgroundColor: 'transparent'`, which overrode the
  opaque default `react-native-drawer-layout` applies to its own
  `styles.drawer`. Since `drawerStyle` is applied last in the panel's
  style array, the panel had no background at all, so the reader showed
  through it for every frame before the content painted. It now carries
  the drawer's own `theme.surface`, matching what the content already
  paints, so the panel is opaque from the first frame.

- The chapter list was mounted by the same handler that opened the
  drawer, so the slide-in animation (UI thread) ran while the list was
  still mounting and measuring (JS thread). The list is now mounted once
  the chapter has loaded and interactions have settled, before the
  drawer is first opened. A closed panel stays laid out -- it is only
  translated off-screen -- so the list measures and renders its first
  rows out of sight instead of during the animation.

- When the list mounted after the chapters arrived, it was scrolled to
  the current chapter with an animation even though `initialScrollIndex`
  had already placed it there, adding a second visible jump. That scroll
  is now skipped on the initial mount and kept for later chapter
  changes.


Claude-Session: https://claude.ai/code/session_01Et1ihq4SBUhfvxgWJisHTe

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-22 09:15:02 +05:30
Rajarshee Chatterjee 38003c59e0 fix: Stop Bottom Nav Icons Trembling On Tab Press (Closes #2041) (#2047)
The focused tab indicator animated `width` directly, which triggered layout every frame — Yoga's per-node pixel rounding then shifted the icon and label inconsistently as the width swept through fractional values, causing a visible tremble. Fixed by giving the icon a fixed-size box and animating the indicator via `scaleX` on an absolutely positioned sibling instead, since transforms don't trigger layout.
2026-09-22 08:23:58 +05:30
Rajarshee Chatterjee f1119d8c30 feat: Add Always Ask Category Selection (Closes #537) 2026-09-16 10:14:29 +05:30
Rajarshee Chatterjee a5e2b4d4f0 feat: Add Crash Log Collection And Sharing (Closes #11) 2026-09-15 14:16:51 +05:30
Rajarshee Chatterjee a12cba030d fix: Skip Dash-Only TTS Dividers 2026-09-15 13:38:39 +05:30
Rajarshee Chatterjee 56d1140aad chore: Require Issue Templates 2026-09-14 08:44:15 +05:30
Rajarshee Chatterjee 0379141304 feat: Add Individual Task Cancellation (Closes #1985) 2026-09-11 18:41:51 +05:30
Rajarshee Chatterjee b6e9f7157d chore: Rename Feature Request Label 2026-09-08 07:03:28 +05:30
Rajarshee Chatterjee edb3df719f fix: Repair Invalid Default Category Names (Closes #2031) 2026-09-07 22:25:31 +05:30
Rajarshee Chatterjee 9bde3df74d fix: Prevent Novel Screen Empty State Flash (Closes #2034) 2026-09-07 21:13:47 +05:30
Rajarshee Chatterjee 2426e6fe3e chore: Lowercase Issue Template Labels 2026-09-07 20:49:59 +05:30
Rajarshee Chatterjee 93209abebd fix: Make Backup Restore Non-Destructive (Closes #2026) 2026-09-07 20:43:20 +05:30
Claude 158786ccd5 fix: Prevent Crash in parseChapterNumber When Chapter or Novel Name Is Missing
Guards against undefined/null chapterName and novelName so
parseChapterNumber no longer throws "Cannot read property
'toLowerCase' of undefined" when rendering the Updates screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011WW3AHpNPxoRVsbAMVRX6T
2026-09-02 19:15:46 +00:00
Patrick Loser 77be5624f2 feat: Add URL Novel Search and Intent-Based Novel Opening (#1987)
* Add native share intent handling module

* Handle shared URLs via intent handler

* i18n: add global search and novel error strings

Generated with omp (AI coding agent)

* novel store: track bootstrap failures as user-facing errors

Bootstrap failures no longer surface raw plugin/DB errors; missing novels map to a custom not-found message. Generated with omp (AI coding agent)

* novel screen: render error view when bootstrap fails

Shows a custom error with a back action instead of a blank screen, and hides the appbar actions in the error state. Generated with omp (AI coding agent)

* global search: never auto-navigate or search URL text

URLs are handled by the screen's explicit submit flow; navigation and search stay out of the hook. Generated with omp (AI coding agent)

* global search: add animated open-novel FAB with clipboard offer

Submit or FAB tap opens a matching URL's novel; unmatched URLs toast. The FAB floats above the keyboard via keyboard-controller and slides in from the right. Generated with omp (AI coding agent)

* fix review issues

* fix rebase issue
2026-09-01 18:59:32 +05:30
Rajarshee Chatterjee 4b580c90ef fix: Prevent TTS From Speaking Surrounding Quotes (Closes #2008) 2026-08-29 11:41:59 +05:30
200 changed files with 10396 additions and 2928 deletions
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+3 -3
View File
@@ -2,7 +2,7 @@
name: Issue report
description: Report an issue in LNReader
labels: [Bug]
labels: [bug]
body:
- type: textarea
id: reproduce-steps
@@ -45,7 +45,7 @@ body:
label: LNReader version
description: You can find your LNReader version in **More → About**.
placeholder: |
Example: "2.1.3"
Example: "2.1.4"
validations:
required: true
@@ -88,7 +88,7 @@ body:
required: true
- label: If this is an issue with a plugin, I should be opening an issue in the [plugins repository](https://github.com/lnreader/lnreader-plugins/issues/new/choose).
required: true
- label: I have updated the app to version **[2.1.3](https://github.com/lnreader/lnreader/releases/latest)**.
- label: I have updated the app to version **[2.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+2 -2
View File
@@ -2,7 +2,7 @@
name: Feature request
description: Suggest a feature to improve LNReader
labels: [Feature Request]
labels: [feature]
body:
- type: textarea
id: feature-description
@@ -34,7 +34,7 @@ body:
required: true
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
required: true
- label: I have updated the app to version **[2.1.3](https://github.com/lnreader/lnreader/releases/latest)**.
- label: I have updated the app to version **[2.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+27 -25
View File
@@ -14,6 +14,7 @@ import {
import AppErrorBoundary, {
ErrorFallback,
} from '@components/AppErrorBoundary/AppErrorBoundary';
import CrashRecoveryGate from '@components/AppErrorBoundary/CrashRecoveryGate';
import Main from './src/navigators/Main';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
@@ -64,7 +65,7 @@ const ThemedPaperProvider = ({ children }: PropsWithChildren) => {
return <PaperProvider theme={paperTheme}>{children}</PaperProvider>;
};
const App = () => {
const Application = () => {
useRozeniteSqlitePlugin({ adapters: sqliteAdapters });
const { success: databaseReady, error: databaseError } = useInitDatabase();
const { ready: servicesReady, error: servicesError } =
@@ -80,9 +81,7 @@ const App = () => {
if (initializationError) {
return (
<ThemeProvider>
<ErrorFallback error={initializationError} resetError={() => null} />
</ThemeProvider>
<ErrorFallback error={initializationError} resetError={() => null} />
);
}
@@ -91,30 +90,33 @@ const App = () => {
}
return (
<Suspense fallback={null}>
<ThemeProvider>
<ThemedRootView>
<KeyboardProvider>
<AppErrorBoundary>
<SafeAreaProvider>
<ThemedPaperProvider>
<BottomSheetModalProvider>
<StatusBar
translucent={true}
backgroundColor="transparent"
/>
<Main />
</BottomSheetModalProvider>
</ThemedPaperProvider>
</SafeAreaProvider>
</AppErrorBoundary>
</KeyboardProvider>
</ThemedRootView>
</ThemeProvider>
</Suspense>
<ThemedRootView>
<KeyboardProvider>
<AppErrorBoundary>
<SafeAreaProvider>
<ThemedPaperProvider>
<BottomSheetModalProvider>
<StatusBar translucent={true} backgroundColor="transparent" />
<Main />
</BottomSheetModalProvider>
</ThemedPaperProvider>
</SafeAreaProvider>
</AppErrorBoundary>
</KeyboardProvider>
</ThemedRootView>
);
};
const App = () => (
<Suspense fallback={null}>
<ThemeProvider>
<CrashRecoveryGate>
<Application />
</CrashRecoveryGate>
</ThemeProvider>
</Suspense>
);
export default App;
const styles = StyleSheet.create({
+9 -2
View File
@@ -1,6 +1,6 @@
{
"expo": {
"version": "2.1.3",
"version": "2.1.4",
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
@@ -56,7 +56,7 @@
]
],
"android": {
"versionCode": 4196355,
"versionCode": 4196356,
"package": "com.rajarsheechatterjee.LNReader",
"adaptiveIcon": {
"foregroundImage": "./assets/native/ic_launcher_foreground.png",
@@ -70,6 +70,13 @@
"android.permission.FOREGROUND_SERVICE_DATA_SYNC",
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
"android.permission.POST_NOTIFICATIONS"
],
"intentFilters": [
{
"action": "SEND",
"category": ["DEFAULT"],
"data": [{ "mimeType": "text/plain" }]
}
]
},
"extra": {
+35
View File
@@ -14,6 +14,41 @@ html {
background-color: var(--theme-secondary);
}
#text-selection-ui {
position: fixed;
z-index: 100000;
display: flex;
overflow: hidden;
border: 1px solid var(--theme-outlineVariant);
border-radius: 24px;
background-color: var(--theme-surface-0-9);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
opacity: 0;
pointer-events: none;
transition: opacity 150ms;
user-select: none;
}
#text-selection-ui button {
min-width: 88px;
min-height: 44px;
padding: 0 16px;
border: 0;
background: transparent;
color: var(--theme-onSurface);
font-size: 14px;
font-weight: 600;
touch-action: manipulation;
}
#text-selection-ui button + button {
border-inline-start: 1px solid var(--theme-outlineVariant);
}
#text-selection-ui button:active {
background-color: var(--theme-surfaceVariant);
}
body {
margin-left: 0;
margin-right: 0;
+6
View File
@@ -15,6 +15,12 @@ body.page-reader > #LNReader-chapter {
column-fill: auto;
transition: transform 200ms;
}
body.page-reader.no-animation > #LNReader-chapter {
transition: none !important;
}
body.page-reader.no-animation .transition-chapter {
transition: none !important;
}
.transition-chapter {
height: 100vh;
width: 100vw;
+55 -14
View File
@@ -211,10 +211,22 @@ window.tts = new (function () {
this.normalizeText = text => {
if (!text) return '';
return text
const normalized = text
.replace(/\s+/g, ' ')
.trim()
.replace(/^["'“”‘’]+|["'“”‘’]+$/g, '')
.replace(/\s*([.,!?;:])\s*/g, '$1 ')
.trim();
const dashOnlyText = normalized.replace(/\s/g, '');
if (
dashOnlyText.length >= 3 &&
/^[\-‐‑‒–—―−⁓⸺⸻﹘﹣-]+$/u.test(dashOnlyText)
) {
return '';
}
return normalized;
};
// if can find a readable node, else stop tts
@@ -512,7 +524,8 @@ van.derive(() => {
}
});
window.pageReader = new (function () {
var pageReader;
window.pageReader = pageReader = new (function () {
const config =
typeof initialPageReaderConfig === 'undefined'
? {}
@@ -532,18 +545,21 @@ window.pageReader = new (function () {
document.getElementsByClassName('transition-chapter')[0];
if (!this.chapterEnding) return;
}
const noAnim = Boolean(
reader.generalSettings?.val?.pageReaderDisableAnimation,
);
this.chapterEnding.style.transition = 'unset';
if (bool) {
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
requestAnimationFrame(() => {
if (!instant) {
if (!instant && !noAnim) {
this.chapterEnding.style.transition = 'transform 200ms';
}
this.chapterEnding.style.transform = 'translateX(-100vw)';
});
this.chapterEndingVisible.val = true;
} else {
if (!instant) {
if (!instant && !noAnim) {
this.chapterEnding.style.transition = 'transform 200ms';
}
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
@@ -567,26 +583,35 @@ window.pageReader = new (function () {
return;
}
destPage = parseInt(destPage, 10);
const noAnim = Boolean(
reader.generalSettings?.val?.pageReaderDisableAnimation,
);
if (destPage < 0) {
if (!reader.prevChapter) return;
document.getElementsByClassName('transition-chapter')[0].innerText =
reader.prevChapter.name;
this.showChapterEnding(true, false, true);
this.showChapterEnding(true, noAnim, true);
this.chapterNavigationPending = true;
setTimeout(() => {
reader.post({ type: 'prev' });
}, 200);
setTimeout(
() => {
reader.post({ type: 'prev' });
},
noAnim ? 0 : 200,
);
return;
}
if (destPage >= this.totalPages.val) {
if (!reader.nextChapter) return;
document.getElementsByClassName('transition-chapter')[0].innerText =
reader.nextChapter.name;
this.showChapterEnding(true);
this.showChapterEnding(true, noAnim);
this.chapterNavigationPending = true;
setTimeout(() => {
reader.post({ type: 'next' });
}, 200);
setTimeout(
() => {
reader.post({ type: 'next' });
},
noAnim ? 0 : 200,
);
return;
}
this.page.val = destPage;
@@ -594,7 +619,7 @@ window.pageReader = new (function () {
'translateX(-' + destPage * 100 + '%)';
const newProgress = parseInt(
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
((this.page.val + 1) / this.totalPages.val) * 100,
10,
);
@@ -675,6 +700,18 @@ window.pageReader = new (function () {
});
}
});
van.derive(() => {
document.body.classList.toggle(
'no-animation',
Boolean(reader.generalSettings.val.pageReaderDisableAnimation),
);
reader.chapterElement?.style.removeProperty('transition');
const chapterEnding =
this.chapterEnding ||
document.getElementsByClassName('transition-chapter')[0];
chapterEnding?.style.removeProperty('transition');
});
})();
document.addEventListener('DOMContentLoaded', () => {
@@ -893,7 +930,11 @@ window.addEventListener('load', () => {
if (Math.abs(diffX) > 8 || Math.abs(diffY) > 8) {
pageReader.ignoreClickUntil = Date.now() + 400;
}
reader.chapterElement.style.transition = 'transform 200ms';
reader.chapterElement.style.transition = reader.generalSettings.val
.pageReaderDisableAnimation
? 'none'
: 'transform 200ms';
reader.chapterElement.style.removeProperty('transition');
const diffXPercentage = diffX / reader.layoutWidth;
if (diffXPercentage < -0.3) {
pageReader.movePage(pageReader.page.val + 1);
+2
View File
@@ -29,6 +29,8 @@ export interface Reader {
finished: string;
nextChapter: string;
noNextChapter: string;
removeText: string;
replaceText: string;
};
setAdjacentChapters: (adjacent: {
+7 -30
View File
@@ -10,28 +10,11 @@ window.textRemover = new (function () {
selectionUI = div(
{
id: 'text-selection-ui',
style: `
position: fixed;
background: color-mix(in srgb, var(--theme-surface), transparent 10%);
border-radius: 8px;
padding: 8px;
z-index: 100000;
opacity: 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
transition: opacity 150ms
`,
role: 'group',
},
button(
{
style: `
background: var(--theme-secondary);
color: var(--theme-onSecondary);
padding: 6px 12px;
margin: 2px;
border: 0;
border-radius: 4px;
font-size: 12px;
`,
type: 'button',
onclick: e => {
if (reader.hidden.val) {
e.stopPropagation();
@@ -39,19 +22,11 @@ window.textRemover = new (function () {
removeSelectedText();
},
},
'Remove',
reader.strings.removeText,
),
button(
{
style: `
background: var(--theme-secondary);
color: var(--theme-onSecondary);
padding: 6px 12px;
margin: 2px;
border: 0;
border-radius: 4px;
font-size: 12px;
`,
type: 'button',
onclick: e => {
if (reader.hidden.val) {
e.stopPropagation();
@@ -59,7 +34,7 @@ window.textRemover = new (function () {
replaceSelectedText();
},
},
'Replace',
reader.strings.replaceText,
),
);
@@ -138,12 +113,14 @@ window.textRemover = new (function () {
}
ui.style.opacity = '1';
ui.style.pointerEvents = 'auto';
isUIActive = true;
}
function hideSelectionUI() {
if (selectionUI) {
selectionUI.style.opacity = '0';
selectionUI.style.pointerEvents = 'none';
}
isUIActive = false;
}
+3
View File
@@ -3,6 +3,9 @@ import { registerRootComponent } from 'expo';
import { AppRegistry, I18nManager } from 'react-native';
import { i18n } from './src/i18n/translations';
import { runHeadlessBackgroundTask } from './src/services/backgroundTasks';
import { installJsCrashHandler } from './src/services/crashLogs/installJsCrashHandler';
installJsCrashHandler();
AppRegistry.registerHeadlessTask(
'LNReaderBackgroundTask',
+1 -1
View File
@@ -31,7 +31,7 @@ const baseTransform = {
const baseTransformIgnorePatterns = [
// 'node_modules/(?!(.pnpm/|@op-engineering|drizzle-orm|lodash-es|@babel/runtime))',
'node_modules/(?!(?:.pnpm/)?((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|color|@op-engineering|drizzle-orm|lodash-es))',
'node_modules/(?!(?:.pnpm/)?((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|@material/material-color-utilities|@pchmn/expo-material3-theme|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|color|@op-engineering|drizzle-orm|lodash-es))',
];
module.exports = {
@@ -5,13 +5,41 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
data class BackgroundTaskSummary(
val id: String,
val type: String,
val title: String,
val description: String,
val state: String,
val progress: Double?,
val progressText: String?,
val attempt: Int,
val createdAt: Long,
val updatedAt: Long,
)
@Dao
interface BackgroundTaskDao {
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(task: BackgroundTaskEntity)
@Query("SELECT * FROM background_tasks ORDER BY createdAt ASC")
suspend fun getAll(): List<BackgroundTaskEntity>
@Query(
"SELECT id, type, title, description, state, progress, progressText, " +
"attempt, createdAt, updatedAt " +
"FROM background_tasks " +
"WHERE state IN ('queued', 'running', 'paused') ORDER BY createdAt ASC",
)
suspend fun getActive(): List<BackgroundTaskSummary>
@Query(
"DELETE FROM background_tasks " +
"WHERE state IN ('succeeded', 'failed', 'cancelled') " +
"AND updatedAt < :cutoff",
)
suspend fun deleteOldTerminal(cutoff: Long)
@Query("DELETE FROM background_tasks WHERE id = :id AND state = :state")
suspend fun deleteIfState(id: String, state: String): Int
@Query("SELECT * FROM background_tasks WHERE id = :id LIMIT 1")
suspend fun get(id: String): BackgroundTaskEntity?
@@ -19,11 +47,42 @@ interface BackgroundTaskDao {
@Query("SELECT * FROM background_tasks WHERE type = :type AND state IN ('queued', 'running', 'paused') LIMIT 1")
suspend fun getActiveByType(type: String): BackgroundTaskEntity?
@Query("UPDATE background_tasks SET state = :state, updatedAt = :updatedAt WHERE id = :id")
suspend fun updateState(id: String, state: String, updatedAt: Long)
@Query("SELECT * FROM background_tasks WHERE queueName = :queueName AND state = 'queued' ORDER BY createdAt ASC")
suspend fun getQueuedByQueueName(queueName: String): List<BackgroundTaskEntity>
@Query(
"UPDATE background_tasks SET state = 'paused', updatedAt = :updatedAt " +
"WHERE id = :id AND state IN ('queued', 'running')",
)
suspend fun markPaused(id: String, updatedAt: Long): Int
@Query(
"UPDATE background_tasks SET state = 'queued', updatedAt = :updatedAt " +
"WHERE id = :id AND state = 'paused'",
)
suspend fun markQueued(id: String, updatedAt: Long): Int
@Query(
"UPDATE background_tasks SET state = 'cancelled', updatedAt = :updatedAt " +
"WHERE id = :id AND state IN ('queued', 'running', 'paused')",
)
suspend fun markCancelled(id: String, updatedAt: Long): Int
@Query("UPDATE background_tasks SET state = :state, updatedAt = :updatedAt WHERE id = :id AND state = 'running'")
suspend fun finishRunning(id: String, state: String, updatedAt: Long)
suspend fun finishRunning(id: String, state: String, updatedAt: Long): Int
@Query(
"UPDATE background_tasks " +
"SET state = :state, progress = :progress, progressText = :progressText, updatedAt = :updatedAt " +
"WHERE id = :id AND state = 'running'",
)
suspend fun finishRunningWithProgress(
id: String,
state: String,
progress: Double?,
progressText: String?,
updatedAt: Long,
): Int
@Query(
"""
@@ -48,11 +107,11 @@ interface BackgroundTaskDao {
maxConcurrentDownloads: Int,
): Int
@Query("UPDATE background_tasks SET progress = :progress, progressText = :progressText, updatedAt = :updatedAt WHERE id = :id")
suspend fun updateProgress(id: String, progress: Double?, progressText: String?, updatedAt: Long)
@Query("UPDATE background_tasks SET progress = :progress, progressText = :progressText, updatedAt = :updatedAt WHERE id = :id AND state = 'running'")
suspend fun updateProgress(id: String, progress: Double?, progressText: String?, updatedAt: Long): Int
@Query("UPDATE background_tasks SET checkpoint = :checkpoint, updatedAt = :updatedAt WHERE id = :id")
suspend fun updateCheckpoint(id: String, checkpoint: String?, updatedAt: Long)
@Query("UPDATE background_tasks SET checkpoint = :checkpoint, updatedAt = :updatedAt WHERE id = :id AND state = 'running'")
suspend fun updateCheckpoint(id: String, checkpoint: String?, updatedAt: Long): Int
@Query("UPDATE background_tasks SET workId = :workId, updatedAt = :updatedAt WHERE id = :id")
suspend fun assignWork(id: String, workId: String, updatedAt: Long)
@@ -37,8 +37,19 @@ object BackgroundTaskScheduler {
return request.id
}
suspend fun cancel(context: Context, taskId: String) {
suspend fun cancel(context: Context, taskId: String, isRunning: Boolean) {
// A queued worker must finish as a no-op so WorkManager does not cancel
// every task chained after it.
if (!isRunning) return
val dao = BackgroundTaskDatabase.get(context).tasks()
// The running worker may delete the row as it consumes cancellation.
// Capture the queue before cancelling that worker.
val queueName = dao.get(taskId)?.queueName ?: return
WorkManager.getInstance(context).cancelAllWorkByTag(taskId).await()
// Cancelling running work also cancels its dependents, so rebuild the
// remaining queue after the worker has stopped.
dao.getQueuedByQueueName(queueName).forEach { enqueue(context, it.id) }
}
suspend fun enqueueLibraryUpdate(
@@ -8,6 +8,8 @@ import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
class LNReaderTaskWorker(
appContext: Context,
@@ -16,8 +18,12 @@ class LNReaderTaskWorker(
override suspend fun doWork(): Result {
val taskId = inputData.getString(BackgroundTaskScheduler.TASK_ID) ?: return Result.failure()
val dao = BackgroundTaskDatabase.get(applicationContext).tasks()
val task = dao.get(taskId) ?: return Result.failure()
if (task.state == BackgroundTaskState.CANCELLED || task.state == BackgroundTaskState.PAUSED) {
val task = dao.get(taskId) ?: return Result.success()
if (task.state == BackgroundTaskState.CANCELLED) {
consumeCancellation(dao, taskId)
return Result.success()
}
if (task.state == BackgroundTaskState.PAUSED) {
return Result.success()
}
@@ -28,13 +34,23 @@ class LNReaderTaskWorker(
MAX_CONCURRENT_DOWNLOADS,
)
if (claimed == 0) {
return if (dao.get(taskId)?.state == BackgroundTaskState.QUEUED) {
Result.retry()
} else {
Result.success()
return when (dao.get(taskId)?.state) {
BackgroundTaskState.QUEUED -> Result.retry()
BackgroundTaskState.CANCELLED -> {
consumeCancellation(dao, taskId)
Result.success()
}
else -> Result.success()
}
}
val runningTask = dao.get(taskId) ?: return Result.failure()
val runningTask = dao.get(taskId) ?: return Result.success()
if (runningTask.state != BackgroundTaskState.RUNNING) {
if (runningTask.state == BackgroundTaskState.CANCELLED) {
consumeCancellation(dao, taskId)
}
return Result.success()
}
setForeground(createForegroundInfo(runningTask))
val execution = TaskExecutionRegistry.register(taskId)
@@ -47,61 +63,106 @@ class LNReaderTaskWorker(
when (val executionResult = execution.await()) {
TaskExecutionResult.Success -> {
val currentState = dao.get(taskId)?.state
if (currentState == BackgroundTaskState.CANCELLED) {
TaskNotificationFactory.dismiss(applicationContext, taskId)
return Result.success()
}
if (currentState == BackgroundTaskState.PAUSED) {
dao.get(taskId)?.let { TaskNotificationFactory.update(applicationContext, it) }
return Result.success()
}
dao.finishRunning(taskId, BackgroundTaskState.SUCCEEDED, System.currentTimeMillis())
dao.get(taskId)?.let {
TaskNotificationFactory.postTerminal(applicationContext, it)
if (dao.finishRunning(taskId, BackgroundTaskState.SUCCEEDED, System.currentTimeMillis()) > 0) {
postOwnedTerminal(taskId, dao, BackgroundTaskState.SUCCEEDED)
} else {
resultAfterLostOwnership(taskId, dao)
}
Result.success()
}
is TaskExecutionResult.Failure -> {
val currentState = dao.get(taskId)?.state
if (currentState == BackgroundTaskState.PAUSED || currentState == BackgroundTaskState.CANCELLED) {
return Result.success()
}
if (executionResult.shouldRetry) {
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
Result.retry()
} else {
dao.updateState(taskId, BackgroundTaskState.FAILED, System.currentTimeMillis())
dao.get(taskId)?.let {
TaskNotificationFactory.postTerminal(applicationContext, it)
if (dao.finishRunning(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis()) > 0) {
Result.retry()
} else {
resultAfterLostOwnership(taskId, dao)
}
} else if (
dao.finishRunning(taskId, BackgroundTaskState.FAILED, System.currentTimeMillis()) > 0
) {
postOwnedTerminal(taskId, dao, BackgroundTaskState.FAILED)
Result.success()
} else {
resultAfterLostOwnership(taskId, dao)
}
}
}
} catch (error: CancellationException) {
val latestState = dao.get(taskId)?.state
if (latestState !in listOf(BackgroundTaskState.PAUSED, BackgroundTaskState.CANCELLED)) {
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
withContext(NonCancellable) {
when (dao.get(taskId)?.state) {
BackgroundTaskState.CANCELLED -> consumeCancellation(dao, taskId)
BackgroundTaskState.RUNNING -> dao.finishRunning(
taskId,
BackgroundTaskState.QUEUED,
System.currentTimeMillis(),
)
else -> Unit
}
}
throw error
} catch (error: Exception) {
val latestState = dao.get(taskId)?.state
if (latestState !in listOf(BackgroundTaskState.PAUSED, BackgroundTaskState.CANCELLED)) {
val now = System.currentTimeMillis()
val message = error.message ?: error.javaClass.simpleName
dao.updateProgress(taskId, null, message, now)
dao.updateState(taskId, BackgroundTaskState.FAILED, now)
dao.get(taskId)?.let {
TaskNotificationFactory.postTerminal(applicationContext, it)
val now = System.currentTimeMillis()
val message = error.message ?: error.javaClass.simpleName
if (
dao.finishRunningWithProgress(
taskId,
BackgroundTaskState.FAILED,
null,
message,
now,
) > 0
) {
postOwnedTerminal(taskId, dao, BackgroundTaskState.FAILED)
Result.failure()
} else {
when (dao.get(taskId)?.state) {
BackgroundTaskState.CANCELLED -> {
consumeCancellation(dao, taskId)
Result.success()
}
BackgroundTaskState.PAUSED -> Result.success()
else -> Result.failure()
}
}
Result.failure()
} finally {
TaskExecutionRegistry.cancel(taskId)
}
}
private suspend fun resultAfterLostOwnership(
taskId: String,
dao: BackgroundTaskDao,
): Result {
return when (dao.get(taskId)?.state) {
BackgroundTaskState.CANCELLED -> {
consumeCancellation(dao, taskId)
Result.success()
}
BackgroundTaskState.PAUSED -> {
dao.get(taskId)?.let { TaskNotificationFactory.update(applicationContext, it) }
Result.success()
}
else -> Result.success()
}
}
private suspend fun postOwnedTerminal(
taskId: String,
dao: BackgroundTaskDao,
state: String,
) {
val terminalTask = dao.get(taskId) ?: return
if (terminalTask.state != state) return
TaskNotificationFactory.postTerminal(applicationContext, terminalTask)
dao.deleteIfState(taskId, state)
}
private suspend fun consumeCancellation(dao: BackgroundTaskDao, taskId: String) {
if (dao.deleteIfState(taskId, BackgroundTaskState.CANCELLED) > 0) {
TaskNotificationFactory.dismiss(applicationContext, taskId)
}
}
private fun createForegroundInfo(task: BackgroundTaskEntity): ForegroundInfo {
val id = TaskNotificationFactory.notificationId(task.id)
val notification = TaskNotificationFactory.build(applicationContext, task)
@@ -57,7 +57,10 @@ class NativeBackgroundTasksModule : Module() {
AsyncFunction("getTasks") {
runBlocking(Dispatchers.IO) {
dao.getAll().map(::toRecord)
dao.deleteOldTerminal(
System.currentTimeMillis() - TERMINAL_TASK_RETENTION_MS,
)
dao.getActive().map(::toSummary)
}
}
@@ -70,11 +73,12 @@ class NativeBackgroundTasksModule : Module() {
AsyncFunction("pause") { taskId: String ->
runBlocking(Dispatchers.IO) {
requireTask(taskId)
dao.updateState(taskId, BackgroundTaskState.PAUSED, System.currentTimeMillis())
if (TaskExecutionRegistry.isActive(taskId)) {
emitInterruption(taskId, "pause")
if (dao.markPaused(taskId, System.currentTimeMillis()) > 0) {
if (TaskExecutionRegistry.isActive(taskId)) {
emitInterruption(taskId, "pause")
}
dao.get(taskId)?.let { TaskNotificationFactory.update(appContext.reactContext!!, it) }
}
dao.get(taskId)?.let { TaskNotificationFactory.update(appContext.reactContext!!, it) }
}
}
@@ -84,21 +88,29 @@ class NativeBackgroundTasksModule : Module() {
if (TaskExecutionRegistry.isActive(taskId)) {
throw IllegalStateException("Task is still pausing; try resuming again shortly")
}
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
BackgroundTaskScheduler.enqueue(appContext.reactContext!!, taskId)
if (dao.markQueued(taskId, System.currentTimeMillis()) > 0) {
BackgroundTaskScheduler.enqueue(appContext.reactContext!!, taskId)
}
}
}
AsyncFunction("cancel") { taskId: String ->
runBlocking(Dispatchers.IO) {
requireTask(taskId)
dao.updateState(taskId, BackgroundTaskState.CANCELLED, System.currentTimeMillis())
if (TaskExecutionRegistry.isActive(taskId)) {
val task = requireTask(taskId)
val isRunning = task.state == BackgroundTaskState.RUNNING ||
TaskExecutionRegistry.isActive(taskId)
if (dao.markCancelled(taskId, System.currentTimeMillis()) == 0) {
TaskNotificationFactory.dismiss(appContext.reactContext!!, taskId)
return@runBlocking
}
if (isRunning) {
emitInterruption(taskId, "cancel")
}
BackgroundTaskScheduler.cancel(appContext.reactContext!!, taskId)
dao.updateCheckpoint(taskId, null, System.currentTimeMillis())
BackgroundTaskScheduler.cancel(appContext.reactContext!!, taskId, isRunning)
TaskNotificationFactory.dismiss(appContext.reactContext!!, taskId)
if (!isRunning && task.state != BackgroundTaskState.QUEUED) {
dao.deleteIfState(taskId, BackgroundTaskState.CANCELLED)
}
}
}
@@ -126,22 +138,13 @@ class NativeBackgroundTasksModule : Module() {
val now = System.currentTimeMillis()
dao.updateCheckpoint(taskId, null, now)
dao.updateProgress(taskId, null, completionText, now)
dao.finishRunning(taskId, BackgroundTaskState.SUCCEEDED, now)
TaskExecutionRegistry.complete(taskId, TaskExecutionResult.Success)
}
}
AsyncFunction("fail") { taskId: String, error: String, shouldRetry: Boolean ->
runBlocking(Dispatchers.IO) {
val currentState = dao.get(taskId)?.state
if (currentState !in listOf(BackgroundTaskState.PAUSED, BackgroundTaskState.CANCELLED)) {
dao.updateProgress(taskId, null, error, System.currentTimeMillis())
dao.finishRunning(
taskId,
if (shouldRetry) BackgroundTaskState.QUEUED else BackgroundTaskState.FAILED,
System.currentTimeMillis(),
)
}
dao.updateProgress(taskId, null, error, System.currentTimeMillis())
TaskExecutionRegistry.complete(taskId, TaskExecutionResult.Failure(error, shouldRetry))
}
}
@@ -177,22 +180,36 @@ class NativeBackgroundTasksModule : Module() {
private suspend fun requireTask(taskId: String): BackgroundTaskEntity =
dao.get(taskId) ?: throw IllegalArgumentException("Unknown background task: $taskId")
private fun toRecord(task: BackgroundTaskEntity): Map<String, Any?> = mapOf(
private fun toSummary(task: BackgroundTaskSummary): Map<String, Any?> = mapOf(
"id" to task.id,
"type" to task.type,
"payload" to task.payload,
"title" to task.title,
"description" to task.description,
"state" to task.state,
"progress" to task.progress,
"progressText" to task.progressText,
"checkpoint" to task.checkpoint,
"attempt" to task.attempt,
"createdAt" to task.createdAt.toDouble(),
"updatedAt" to task.updatedAt.toDouble(),
)
private fun toRecord(task: BackgroundTaskEntity): Map<String, Any?> = mapOf(
"id" to task.id,
"type" to task.type,
"title" to task.title,
"description" to task.description,
"state" to task.state,
"progress" to task.progress,
"progressText" to task.progressText,
"attempt" to task.attempt,
"createdAt" to task.createdAt.toDouble(),
"updatedAt" to task.updatedAt.toDouble(),
"payload" to task.payload,
"checkpoint" to task.checkpoint,
)
companion object {
private const val TERMINAL_TASK_RETENTION_MS = 24 * 60 * 60 * 1000L
@Volatile
private var reactContextRef: WeakReference<ReactApplicationContext>? = null
@@ -14,27 +14,37 @@ class TaskActionReceiver : BroadcastReceiver() {
CoroutineScope(Dispatchers.IO).launch {
try {
val dao = BackgroundTaskDatabase.get(context).tasks()
dao.get(taskId) ?: return@launch
val task = dao.get(taskId) ?: return@launch
when (intent.action) {
TaskNotificationFactory.ACTION_PAUSE -> {
dao.updateState(taskId, BackgroundTaskState.PAUSED, System.currentTimeMillis())
if (TaskExecutionRegistry.isActive(taskId)) {
NativeBackgroundTasksModule.emitInterruption(taskId, "pause")
if (dao.markPaused(taskId, System.currentTimeMillis()) > 0) {
if (TaskExecutionRegistry.isActive(taskId)) {
NativeBackgroundTasksModule.emitInterruption(taskId, "pause")
}
dao.get(taskId)?.let { TaskNotificationFactory.update(context, it) }
}
dao.get(taskId)?.let { TaskNotificationFactory.update(context, it) }
}
TaskNotificationFactory.ACTION_RESUME -> {
if (TaskExecutionRegistry.isActive(taskId)) return@launch
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
BackgroundTaskScheduler.enqueue(context, taskId)
if (dao.markQueued(taskId, System.currentTimeMillis()) > 0) {
BackgroundTaskScheduler.enqueue(context, taskId)
}
}
TaskNotificationFactory.ACTION_CANCEL -> {
dao.updateState(taskId, BackgroundTaskState.CANCELLED, System.currentTimeMillis())
if (TaskExecutionRegistry.isActive(taskId)) {
val isRunning = task.state == BackgroundTaskState.RUNNING ||
TaskExecutionRegistry.isActive(taskId)
if (dao.markCancelled(taskId, System.currentTimeMillis()) == 0) {
TaskNotificationFactory.dismiss(context, taskId)
return@launch
}
if (isRunning) {
NativeBackgroundTasksModule.emitInterruption(taskId, "cancel")
}
BackgroundTaskScheduler.cancel(context, taskId)
BackgroundTaskScheduler.cancel(context, taskId, isRunning)
TaskNotificationFactory.dismiss(context, taskId)
if (!isRunning && task.state != BackgroundTaskState.QUEUED) {
dao.deleteIfState(taskId, BackgroundTaskState.CANCELLED)
}
}
}
} finally {
+2 -1
View File
@@ -1,5 +1,6 @@
import NativeBackgroundTasks, {
NativeBackgroundTaskRecord,
NativeBackgroundTaskSummary,
} from './src/NativeBackgroundTasksModule';
export default NativeBackgroundTasks;
export { NativeBackgroundTaskRecord };
export { NativeBackgroundTaskRecord, NativeBackgroundTaskSummary };
@@ -1,21 +1,27 @@
import { requireNativeModule } from 'expo-modules-core';
export type NativeBackgroundTaskRecord = {
export type NativeBackgroundTaskSummary = {
id: string;
type: string;
payload: string;
title: string;
description?: string;
state: string;
progress?: number;
progressText?: string;
checkpoint?: string;
attempt: number;
createdAt: number;
updatedAt: number;
};
export type NativeBackgroundTaskRecord = NativeBackgroundTaskSummary & {
payload: string;
checkpoint?: string;
};
type NativeBackgroundTasksModule = {
getTasks(): Promise<NativeBackgroundTaskSummary[]>;
getTask(taskId: string): Promise<NativeBackgroundTaskRecord | null>;
enqueue(
type: string,
payload: string,
@@ -24,8 +30,6 @@ type NativeBackgroundTasksModule = {
allowsDuplicates: boolean,
queueName: string,
): Promise<string>;
getTasks(): Promise<NativeBackgroundTaskRecord[]>;
getTask(taskId: string): Promise<NativeBackgroundTaskRecord | null>;
pause(taskId: string): Promise<void>;
resume(taskId: string): Promise<void>;
cancel(taskId: string): Promise<void>;
@@ -0,0 +1,24 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.modules.nativecrashlogs'
version = '0.1.0'
android {
namespace "expo.modules.nativecrashlogs"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
testImplementation 'junit:junit:4.13.2'
}
@@ -0,0 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<provider
android:name="expo.modules.nativecrashlogs.CrashHandlerInitializationProvider"
android:authorities="${applicationId}.crash-handler-initializer"
android:exported="false"
android:initOrder="110" />
<provider
android:name="expo.modules.nativecrashlogs.CrashLogFileProvider"
android:authorities="${applicationId}.crash-logs"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/crash_log_paths" />
</provider>
</application>
</manifest>
@@ -0,0 +1,57 @@
package expo.modules.nativecrashlogs
import android.content.Context
import android.os.Process
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
internal class CrashExceptionHandler private constructor(
private val applicationContext: Context,
private val defaultHandler: Thread.UncaughtExceptionHandler?,
) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, exception: Throwable) {
try {
val recordedJsCrash = CrashLogStore.readPendingCrash(applicationContext)
CrashLogStore.writePendingCrash(
applicationContext,
buildString {
appendLine("Native crash captured: ${timestamp()}")
appendLine("Thread: ${thread.name}")
appendLine()
append(exception.stackTraceToString())
if (recordedJsCrash != null) {
appendLine()
appendLine()
appendLine("Previously recorded JavaScript crash:")
append(recordedJsCrash)
}
},
)
} catch (_: Throwable) {
// The platform handler must still terminate the process normally.
} finally {
val handler = defaultHandler
if (handler != null) {
handler.uncaughtException(thread, exception)
} else {
Process.killProcess(Process.myPid())
exitProcess(10)
}
}
}
companion object {
fun install(context: Context) {
val currentHandler = Thread.getDefaultUncaughtExceptionHandler()
if (currentHandler is CrashExceptionHandler) return
Thread.setDefaultUncaughtExceptionHandler(
CrashExceptionHandler(context.applicationContext, currentHandler),
)
}
private fun timestamp(): String =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ROOT).format(Date())
}
}
@@ -0,0 +1,34 @@
package expo.modules.nativecrashlogs
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
class CrashHandlerInitializationProvider : ContentProvider() {
override fun onCreate(): Boolean {
context?.let(CrashExceptionHandler::install)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?,
): Int = 0
}
@@ -0,0 +1,5 @@
package expo.modules.nativecrashlogs
import androidx.core.content.FileProvider
class CrashLogFileProvider : FileProvider(R.xml.crash_log_paths)
@@ -0,0 +1,15 @@
package expo.modules.nativecrashlogs
internal object CrashLogRedactor {
private val headerPattern = Regex(
pattern = "(?i)(authorization|cookie|set-cookie)(\\s*[:=]\\s*).*$",
options = setOf(RegexOption.MULTILINE),
)
private val urlQueryPattern = Regex(
pattern = "(?i)(https?://[^\\s?#]+)\\?[^\\s#\"')\\]]+",
)
fun redact(contents: String): String = contents
.replace(headerPattern, "$1$2[REDACTED]")
.replace(urlQueryPattern, "$1?[REDACTED]")
}
@@ -0,0 +1,48 @@
package expo.modules.nativecrashlogs
import android.content.Context
import java.io.File
internal object CrashLogStore {
private const val CRASH_DIRECTORY = "crash-reports"
private const val PENDING_CRASH_FILE = "last_crash.txt"
private const val SHARE_DIRECTORY = "crash-logs"
private const val SHARE_FILE = "lnreader_crash_logs.txt"
fun createShareFile(context: Context): File {
val directory = File(context.cacheDir, SHARE_DIRECTORY)
check(directory.exists() || directory.mkdirs()) {
"Failed to create crash log cache directory"
}
return File(directory, SHARE_FILE)
}
fun readPendingCrash(context: Context): String? {
val file = pendingCrashFile(context)
if (!file.isFile) return null
return file.readText().takeIf { it.isNotBlank() }
}
fun writePendingCrash(context: Context, contents: String) {
val file = pendingCrashFile(context)
val directory = file.parentFile
check(directory != null && (directory.exists() || directory.mkdirs())) {
"Failed to create crash report directory"
}
val temporaryFile = File(directory, "$PENDING_CRASH_FILE.tmp")
temporaryFile.writeText(contents)
if (!temporaryFile.renameTo(file)) {
file.writeText(contents)
temporaryFile.delete()
}
}
fun clearPendingCrash(context: Context) {
val file = pendingCrashFile(context)
check(!file.exists() || file.delete()) { "Failed to clear pending crash report" }
}
private fun pendingCrashFile(context: Context): File =
File(File(context.filesDir, CRASH_DIRECTORY), PENDING_CRASH_FILE)
}
@@ -0,0 +1,73 @@
package expo.modules.nativecrashlogs
import android.content.Context
import java.io.File
import java.util.concurrent.TimeUnit
internal object LogcatCollector {
private const val MAX_LOG_BYTES = 512 * 1024
private const val MAX_LOG_LINES = 2_000
private const val LOGCAT_TIMEOUT_SECONDS = 2L
fun collect(context: Context, requestedLevel: String): String {
val level = if (requestedLevel == "V") "V" else "E"
val output = File.createTempFile("lnreader-logcat-", ".tmp", context.cacheDir)
return try {
val process = ProcessBuilder(
"logcat",
"-d",
"-t",
MAX_LOG_LINES.toString(),
"-v",
"year",
"-v",
"zone",
"*:$level",
)
.redirectErrorStream(true)
.redirectOutput(output)
.start()
if (!process.waitFor(LOGCAT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
process.destroyForcibly()
}
readBounded(output)
} catch (error: Exception) {
"Logcat unavailable: ${error.message ?: error.javaClass.simpleName}"
} finally {
output.delete()
}
}
private fun readBounded(file: File): String {
if (!file.isFile || file.length() == 0L) return "No accessible logcat entries."
val wasTruncated = file.length() > MAX_LOG_BYTES
val bytes = file.inputStream().use { input ->
var bytesToSkip = if (wasTruncated) file.length() - MAX_LOG_BYTES else 0L
while (bytesToSkip > 0) {
val skipped = input.skip(bytesToSkip)
if (skipped <= 0) break
bytesToSkip -= skipped
}
val buffer = ByteArray(MAX_LOG_BYTES)
var totalRead = 0
while (totalRead < buffer.size) {
val count = input.read(buffer, totalRead, buffer.size - totalRead)
if (count == -1) break
totalRead += count
}
buffer.copyOf(totalRead)
}
val sanitized = bytes.toString(Charsets.UTF_8)
.lineSequence()
.joinToString("\n", transform = CrashLogRedactor::redact)
return if (wasTruncated) {
"[Older logcat entries truncated]\n$sanitized"
} else {
sanitized
}
}
}
@@ -0,0 +1,163 @@
package expo.modules.nativecrashlogs
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.os.Build
import android.webkit.WebView
import androidx.core.content.FileProvider
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class NativeCrashLogsModule : Module() {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun definition() = ModuleDefinition {
Name("NativeCrashLogs")
OnCreate {
appContext.reactContext?.let(CrashExceptionHandler::install)
}
OnDestroy {
coroutineScope.cancel()
}
AsyncFunction("shareCrashLogs") { report: String, logLevel: String, promise: Promise ->
coroutineScope.launch {
try {
val context = requireNotNull(appContext.reactContext) {
"React context is unavailable"
}
val file = CrashLogStore.createShareFile(context)
file.writeText(
CrashLogRedactor.redact(
report.take(MAX_REPORT_CHARACTERS).trimEnd(),
),
)
file.appendText("\n\n[App-accessible logcat]\n")
file.appendText(LogcatCollector.collect(context, logLevel))
withContext(Dispatchers.Main) {
shareFile(context, file)
}
promise.resolve(null)
} catch (error: Exception) {
promise.reject("ECRASHLOG", "Failed to share crash logs", error)
}
}
}
Function("recordJsCrash") { stackTrace: String ->
val context = requireNotNull(appContext.reactContext) {
"React context is unavailable"
}
CrashLogStore.writePendingCrash(context, stackTrace.take(MAX_REPORT_CHARACTERS))
}
AsyncFunction("getPendingCrash") { promise: Promise ->
coroutineScope.launch {
try {
val context = requireNotNull(appContext.reactContext) {
"React context is unavailable"
}
promise.resolve(CrashLogStore.readPendingCrash(context))
} catch (error: Exception) {
promise.reject("ECRASHREAD", "Failed to read pending crash", error)
}
}
}
AsyncFunction("clearPendingCrash") { promise: Promise ->
coroutineScope.launch {
try {
val context = requireNotNull(appContext.reactContext) {
"React context is unavailable"
}
CrashLogStore.clearPendingCrash(context)
promise.resolve(null)
} catch (error: Exception) {
promise.reject("ECRASHCLEAR", "Failed to clear pending crash", error)
}
}
}
AsyncFunction("getDebugInfo") { promise: Promise ->
coroutineScope.launch {
try {
val context = requireNotNull(appContext.reactContext) {
"React context is unavailable"
}
val webViewVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WebView.getCurrentWebViewPackage()?.versionName
} else {
null
}
promise.resolve(mapOf("webViewVersion" to webViewVersion))
} catch (error: Exception) {
promise.reject("EDEBUGINFO", "Failed to read native debug info", error)
}
}
}
Function("restartApp") {
val context = requireNotNull(appContext.reactContext) {
"React context is unavailable"
}
val launchIntent = requireNotNull(
context.packageManager.getLaunchIntentForPackage(context.packageName),
) { "Application launch intent is unavailable" }
try {
CrashLogStore.clearPendingCrash(context)
} catch (_: Exception) {
// Restart remains useful even if stale crash cleanup fails.
}
launchIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP,
)
val activity = appContext.currentActivity
if (activity == null) {
context.startActivity(launchIntent)
} else {
activity.runOnUiThread {
activity.finishAffinity()
activity.startActivity(launchIntent)
}
}
}
}
private fun shareFile(context: Context, file: java.io.File) {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.crash-logs",
file,
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri("LNReader crash logs", uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val chooser = Intent.createChooser(shareIntent, "Share crash logs")
if (appContext.currentActivity == null) {
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(chooser)
} else {
appContext.currentActivity?.startActivity(chooser)
}
}
companion object {
private const val MAX_REPORT_CHARACTERS = 256 * 1024
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="crash_logs" path="crash-logs/" />
</paths>
@@ -0,0 +1,26 @@
package expo.modules.nativecrashlogs
import org.junit.Assert.assertEquals
import org.junit.Test
class CrashLogRedactorTest {
@Test
fun redactsSensitiveHeaders() {
val report = "Authorization: Bearer secret\nCookie=session-secret\nmessage"
assertEquals(
"Authorization: [REDACTED]\nCookie=[REDACTED]\nmessage",
CrashLogRedactor.redact(report),
)
}
@Test
fun redactsUrlQueries() {
val report = "Request failed: https://example.com/chapter?id=12&token=secret"
assertEquals(
"Request failed: https://example.com/chapter?[REDACTED]",
CrashLogRedactor.redact(report),
)
}
}
@@ -0,0 +1,6 @@
{
"platforms": ["android"],
"android": {
"modules": ["expo.modules.nativecrashlogs.NativeCrashLogsModule"]
}
}
+3
View File
@@ -0,0 +1,3 @@
import NativeCrashLogs from './src/NativeCrashLogsModule';
export default NativeCrashLogs;
+3
View File
@@ -0,0 +1,3 @@
{
"name": "native-crash-logs"
}
@@ -0,0 +1,18 @@
import { requireOptionalNativeModule } from 'expo-modules-core';
export type NativeCrashDebugInfo = {
webViewVersion: string | null;
};
export type NativeCrashLogsModule = {
shareCrashLogs(report: string, logLevel: 'E' | 'V'): Promise<void>;
recordJsCrash(stackTrace: string): void;
getPendingCrash(): Promise<string | null>;
clearPendingCrash(): Promise<void>;
getDebugInfo(): Promise<NativeCrashDebugInfo>;
restartApp(): void;
};
export default requireOptionalNativeModule<NativeCrashLogsModule>(
'NativeCrashLogs',
);
@@ -0,0 +1,18 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.modules.nativesharereceiver'
version = '0.1.0'
android {
namespace "expo.modules.nativesharereceiver"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}
@@ -0,0 +1,37 @@
package expo.modules.nativesharereceiver
import android.content.Intent
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class NativeShareReceiverModule : Module() {
override fun definition() = ModuleDefinition {
Name("NativeShareReceiver")
Events("SharedText")
Function("getInitialSharedText") {
val intent = appContext.currentActivity?.intent
val text = extractSharedText(intent)
if (text != null) {
// Consume the share so JS reloads (Fast Refresh, OTA) that re-read
// the activity intent don't re-deliver the same text.
intent?.removeExtra(Intent.EXTRA_TEXT)
}
text
}
OnNewIntent { intent ->
extractSharedText(intent)?.let { text ->
sendEvent("SharedText", mapOf("text" to text))
}
}
}
private fun extractSharedText(intent: Intent?): String? {
if (intent?.action != Intent.ACTION_SEND) {
return null
}
return intent.getStringExtra(Intent.EXTRA_TEXT)
}
}
@@ -0,0 +1,6 @@
{
"platforms": ["android"],
"android": {
"modules": ["expo.modules.nativesharereceiver.NativeShareReceiverModule"]
}
}
+2
View File
@@ -0,0 +1,2 @@
import NativeShareReceiver from './src/NativeShareReceiverModule';
export default NativeShareReceiver;
@@ -0,0 +1,3 @@
{
"name": "native-share-receiver"
}
@@ -0,0 +1,13 @@
import { NativeModule, requireNativeModule } from 'expo-modules-core';
export type NativeShareReceiverEvents = {
SharedText: (payload: { text: string }) => void;
};
declare class NativeShareReceiverModule extends NativeModule<NativeShareReceiverEvents> {
getInitialSharedText(): string | null;
}
export default requireNativeModule<NativeShareReceiverModule>(
'NativeShareReceiver',
);
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "lnreader",
"version": "2.1.3",
"version": "2.1.4",
"private": true,
"scripts": {
"dev:android:release": "pnpm run generate:env:release && npx expo run:android --variant release",
@@ -72,6 +72,7 @@
"htmlparser2": "^12.0.0",
"i18n-js": "^4.5.3",
"lodash-es": "^4.18.1",
"prettier": "2.8.8",
"protobufjs": "^8.7.1",
"react": "19.2.3",
"react-native": "^0.86.0",
@@ -133,7 +134,6 @@
"jest-expo": "^57.0.2",
"lint-staged": "^12.5.0",
"nitrogen": "^0.36.1",
"prettier": "2.8.8",
"react-native-nitro-modules": "^0.36.0",
"react-test-renderer": "19.2.3",
"typescript": "~6.0.3"
@@ -148,6 +148,7 @@
"reactNativeDirectoryCheck": {
"exclude": [
"native-file",
"native-crash-logs",
"nitro-tts",
"native-volume-button-listener",
"native-zip-archive",
+3 -3
View File
@@ -119,6 +119,9 @@ importers:
lodash-es:
specifier: ^4.18.1
version: 4.18.1
prettier:
specifier: 2.8.8
version: 2.8.8
protobufjs:
specifier: ^8.7.1
version: 8.7.1
@@ -297,9 +300,6 @@ importers:
nitrogen:
specifier: ^0.36.1
version: 0.36.1(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3)(supports-color@9.4.0))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(supports-color@9.4.0))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
prettier:
specifier: 2.8.8
version: 2.8.8
react-native-nitro-modules:
specifier: ^0.36.0
version: 0.36.1(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3)(supports-color@9.4.0))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(supports-color@9.4.0))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
@@ -1,13 +1,17 @@
import React, { useMemo } from 'react';
import { StyleSheet, View, Text, StatusBar } from 'react-native';
import React, { useMemo, useState } from 'react';
import { StyleSheet, View, Text, StatusBar, ScrollView } from 'react-native';
import ErrorBoundary from 'react-native-error-boundary';
import * as Clipboard from 'expo-clipboard';
import DeviceInfo from 'react-native-device-info';
import { BUILD_TYPE, GIT_HASH } from '@env';
import { version } from '../../../package.json';
import { getString } from '@i18n/translations';
import { getErrorChainMessages } from '@utils/error';
import { showToast } from '@utils/showToast';
import { Button, List } from '@components';
import { useTheme } from '@hooks/persisted';
import { SafeAreaView } from 'react-native-safe-area-context';
import { restartApplication, shareCrashLogs } from '@services/crashLogs';
interface ErrorFallbackProps {
error: Error;
@@ -19,6 +23,7 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
resetError,
}) => {
const theme = useTheme();
const [isSharing, setIsSharing] = useState(false);
const fallbackGetString = (
key: Parameters<typeof getString>[0],
@@ -33,23 +38,53 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
};
const chainMessages = useMemo(() => getErrorChainMessages(error), [error]);
const versionDetails = [
`${fallbackGetString(
'aboutScreen.version',
'Version',
)}: ${version} (${DeviceInfo.getBuildNumber()})`,
BUILD_TYPE || 'Custom build',
GIT_HASH,
]
.filter(Boolean)
.join(' · ');
const handleCopyStackTrace = async () => {
const copyStackTrace = async () => {
try {
const message = chainMessages.join('\n\nCaused by: ');
await Clipboard.setStringAsync(`${message}\n\n${error.stack}`);
showToast(
fallbackGetString(
'common.copiedToClipboard',
'Copied to clipboard: Stack trace',
{ name: 'Stack trace' },
),
);
return true;
} catch {
// clipboard failure is non-critical
return false;
}
};
const handleShareCrashLogs = async () => {
setIsSharing(true);
try {
await shareCrashLogs(error);
} catch {
const copiedStackTrace = await copyStackTrace();
showToast(
copiedStackTrace
? fallbackGetString(
'errorBoundary.shareCrashLogsFailed',
'Could not share crash logs. The stack trace was copied instead.',
)
: fallbackGetString(
'errorBoundary.shareCrashLogsFailedWithoutCopy',
'Could not share crash logs or copy the stack trace.',
),
);
} finally {
setIsSharing(false);
}
};
const handleRestart = () => {
restartApplication(resetError).catch(resetError);
};
return (
<SafeAreaView
style={[styles.mainCtn, { backgroundColor: theme.background }]}
@@ -65,41 +100,49 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
<Text style={[styles.errorDesc, { color: theme.onSurface }]}>
{fallbackGetString(
'errorBoundary.description',
'The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.',
'The application ran into an unexpected error. Please share the crash logs in our Discord support channel.',
)}
</Text>
<Text
style={[styles.versionDetails, { color: theme.onSurfaceVariant }]}
>
{versionDetails}
</Text>
<ScrollView
style={[
styles.errorCtn,
{
backgroundColor: theme.surfaceVariant,
color: theme.onSurfaceVariant,
},
]}
numberOfLines={20}
contentContainerStyle={styles.errorContent}
>
{`${chainMessages.join('\n\nCaused by: ')}\n\n${error.stack}`}
</Text>
<Text style={[styles.errorText, { color: theme.onSurfaceVariant }]}>
{`${chainMessages.join('\n\nCaused by: ')}\n\n${error.stack}`}
</Text>
</ScrollView>
</View>
<List.Divider theme={theme} />
<Button
onPress={handleCopyStackTrace}
title={fallbackGetString(
'errorBoundary.copyStackTrace',
'Copy stack trace',
)}
style={styles.copyButtonCtn}
mode="outlined"
/>
<Button
onPress={resetError}
title={fallbackGetString(
'errorBoundary.restart',
'Restart the application',
)}
style={styles.buttonCtn}
mode="contained"
/>
<View style={styles.actionsCtn}>
<Button
disabled={isSharing}
loading={isSharing}
onPress={handleShareCrashLogs}
title={fallbackGetString(
'errorBoundary.shareCrashLogs',
'Share crash logs',
)}
mode="outlined"
/>
<Button
onPress={handleRestart}
title={fallbackGetString(
'errorBoundary.restart',
'Restart the application',
)}
mode="contained"
/>
</View>
</SafeAreaView>
);
};
@@ -117,35 +160,42 @@ const AppErrorBoundary: React.FC<AppErrorBoundaryProps> = ({ children }) => {
export default AppErrorBoundary;
const styles = StyleSheet.create({
buttonCtn: {
margin: 16,
marginBottom: 32,
},
copyButtonCtn: {
margin: 16,
marginBottom: 8,
actionsCtn: {
gap: 16,
paddingHorizontal: 16,
paddingVertical: 20,
},
errorCtn: {
flex: 1,
borderRadius: 8,
lineHeight: 20,
marginVertical: 16,
marginBottom: 16,
},
errorContent: {
paddingHorizontal: 8,
paddingVertical: 16,
},
errorText: {
lineHeight: 20,
},
errorDesc: {
lineHeight: 20,
marginVertical: 8,
marginTop: 8,
},
errorInfoCtn: {
flex: 1,
justifyContent: 'center',
padding: 16,
paddingTop: 32,
},
errorTitle: {
fontSize: 20,
marginBottom: 8,
textAlign: 'center',
},
versionDetails: {
lineHeight: 24,
marginVertical: 16,
textAlign: 'center',
},
mainCtn: {
flex: 1,
},
@@ -0,0 +1,35 @@
import { PropsWithChildren, useEffect, useMemo, useState } from 'react';
import * as SplashScreen from 'expo-splash-screen';
import { getPendingCrash } from '@services/crashLogs';
import { ErrorFallback } from './AppErrorBoundary';
const CrashRecoveryGate = ({ children }: PropsWithChildren) => {
const [pendingCrash, setPendingCrash] = useState<string | null>();
useEffect(() => {
getPendingCrash()
.then(crash => {
setPendingCrash(crash);
if (crash) {
SplashScreen.hideAsync();
}
})
.catch(() => setPendingCrash(null));
}, []);
const crashError = useMemo(() => {
if (!pendingCrash) return null;
const error = new Error('LNReader stopped unexpectedly on its last run.');
error.stack = pendingCrash;
return error;
}, [pendingCrash]);
if (pendingCrash === undefined) return null;
if (crashError) {
return <ErrorFallback error={crashError} resetError={() => null} />;
}
return children;
};
export default CrashRecoveryGate;
@@ -0,0 +1,94 @@
import type { ReactNode } from 'react';
import { fireEvent, render, screen, waitFor } from '@test-utils';
import { ErrorFallback } from '../AppErrorBoundary';
import { version } from '../../../../package.json';
const mockShareCrashLogs = jest.fn();
const mockRestartApplication = jest.fn();
const mockTheme = {
background: '#ffffff',
onSurface: '#000000',
onSurfaceVariant: '#333333',
surfaceVariant: '#eeeeee',
outlineVariant: '#cccccc',
primary: '#0061a4',
};
jest.mock('@hooks/persisted/useTheme', () => ({
ThemeProvider: ({ children }: { children: ReactNode }) => children,
useTheme: () => mockTheme,
}));
jest.mock('@hooks/persisted', () => ({
useTheme: () => mockTheme,
}));
jest.mock('react-native-device-info', () => ({
__esModule: true,
default: { getBuildNumber: () => '42' },
}));
jest.mock('@env', () => ({ BUILD_TYPE: 'Debug', GIT_HASH: 'abc123' }));
jest.mock('@screens/novel/NovelContext', () => ({
NovelContextProvider: ({ children }: { children: ReactNode }) => children,
}));
jest.mock('react-native-safe-area-context', () => {
const ReactModule = require('react');
const { View } = jest.requireActual('react-native');
const frame = { height: 800, width: 400, x: 0, y: 0 };
const insets = { bottom: 0, left: 0, right: 0, top: 0 };
return {
SafeAreaFrameContext: ReactModule.createContext(frame),
SafeAreaInsetsContext: ReactModule.createContext(insets),
SafeAreaProvider: ({ children }: { children: ReactNode }) => children,
SafeAreaView: ({ children, ...props }: { children: ReactNode }) =>
ReactModule.createElement(View, props, children),
initialWindowMetrics: { frame, insets },
useSafeAreaFrame: () => frame,
useSafeAreaInsets: () => insets,
};
});
jest.mock('@services/crashLogs', () => ({
shareCrashLogs: (error: Error) => mockShareCrashLogs(error),
restartApplication: (fallback: () => void) =>
mockRestartApplication(fallback),
}));
describe('ErrorFallback', () => {
beforeEach(() => {
mockShareCrashLogs.mockResolvedValue(undefined);
mockRestartApplication.mockResolvedValue(undefined);
});
it('shares a diagnostic report for the displayed error', async () => {
const error = new Error('boom');
render(<ErrorFallback error={error} resetError={jest.fn()} />);
fireEvent.press(screen.getByText('Share crash logs'));
await waitFor(() => expect(mockShareCrashLogs).toHaveBeenCalledWith(error));
});
it('shows version and build details on the error screen', () => {
render(<ErrorFallback error={new Error('boom')} resetError={jest.fn()} />);
expect(
screen.getByText(`Version: ${version} (42) · Debug · abc123`),
).toBeTruthy();
});
it('uses the native restart flow', async () => {
const resetError = jest.fn();
render(<ErrorFallback error={new Error('boom')} resetError={resetError} />);
fireEvent.press(screen.getByText('Restart the application'));
await waitFor(() =>
expect(mockRestartApplication).toHaveBeenCalledWith(resetError),
);
});
});
+29 -12
View File
@@ -93,23 +93,32 @@ function CustomBottomTabBar({
style={styles.pressable}
>
{/* Icon */}
<Animated.View
<View
style={[
styles.iconContainer,
{
transitionProperty: ['width', 'backgroundColor'],
transitionDuration: 250,
transitionTimingFunction: 'ease-in-out',
marginBottom: showLabel ? 4 : 20,
width: isFocused ? 64 : 32,
backgroundColor: isFocused
? theme.primaryContainer
: transparentBg,
},
{ marginBottom: showLabel ? 4 : 20 },
]}
>
{/* The indicator is absolutely positioned and animated with a
transform so that growing it never re-runs layout, which
would otherwise make the icon and label tremble sideways as
their rounded pixel positions shift on every frame. */}
<Animated.View
style={[
styles.indicator,
{
transitionProperty: ['transform', 'backgroundColor'],
transitionDuration: 250,
transitionTimingFunction: 'ease-in-out',
transform: [{ scaleX: isFocused ? 1 : 0.5 }],
backgroundColor: isFocused
? theme.primaryContainer
: transparentBg,
},
]}
/>
{renderIcon({ color: iconColor, route })}
</Animated.View>
</View>
{/* Label */}
{showLabel ? (
@@ -155,7 +164,15 @@ const styles = StyleSheet.create({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: 64,
height: 32,
},
indicator: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 16,
},
label: {
+22 -3
View File
@@ -4,6 +4,7 @@ import {
UseLibraryReturnType,
} from '@screens/library/hooks/useLibrary';
import { useLibrarySettings } from '@hooks/persisted';
import SetCategoriesModal from '@screens/novel/components/SetCategoriesModal';
// type Library = Category & { novels: LibraryNovelInfo[] };
@@ -11,6 +12,8 @@ type LibraryContextType = UseLibraryReturnType & {
settings: ReturnType<typeof useLibrarySettings>;
};
const EMPTY_NOVEL_IDS: number[] = [];
const defaultValue = {} as LibraryContextType;
const LibraryContext = createContext<LibraryContextType>(defaultValue);
@@ -21,11 +24,27 @@ export function LibraryContextProvider({
}) {
const useLibraryParams = useLibrary();
const settings = useLibrarySettings();
const {
pendingLibraryAddition,
cancelPendingLibraryAddition,
confirmPendingLibraryAddition,
} = useLibraryParams;
return (
<LibraryContext.Provider value={{ ...useLibraryParams, settings }}>
{children}
</LibraryContext.Provider>
<>
<LibraryContext.Provider value={{ ...useLibraryParams, settings }}>
{children}
</LibraryContext.Provider>
{pendingLibraryAddition ? (
<SetCategoriesModal
novelIds={EMPTY_NOVEL_IDS}
initialCategoryIds={pendingLibraryAddition.initialCategoryIds}
closeModal={cancelPendingLibraryAddition}
onSubmit={confirmPendingLibraryAddition}
visible
/>
) : null}
</>
);
}
@@ -8,7 +8,10 @@ const defaultLibraryContext = {
refreshCategories: jest.fn().mockResolvedValue(undefined),
setLibrary: jest.fn(),
novelInLibrary: jest.fn(() => false),
switchNovelToLibrary: jest.fn().mockResolvedValue(undefined),
switchNovelToLibrary: jest.fn().mockResolvedValue(true),
pendingLibraryAddition: undefined,
cancelPendingLibraryAddition: jest.fn(),
confirmPendingLibraryAddition: jest.fn().mockResolvedValue(undefined),
refetchLibrary: jest.fn(),
setLibrarySearchText: jest.fn(),
settings: {
+3
View File
@@ -16,6 +16,7 @@ interface ListViewProps {
onPress: () => void;
isSelected?: boolean;
onLongPress?: () => void;
continueReadingButton?: React.ReactNode;
}
const ListView = ({
@@ -27,6 +28,7 @@ const ListView = ({
onPress,
isSelected,
onLongPress,
continueReadingButton,
}: ListViewProps) => {
const fadedImage = { opacity: inLibraryBadge ? 0.5 : 1 };
return (
@@ -58,6 +60,7 @@ const ListView = ({
{unreadBadge}
{inLibraryBadge}
</View>
{continueReadingButton}
</Pressable>
);
};
+43
View File
@@ -8,6 +8,7 @@ import {
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import Color from 'color';
import ListView from './ListView';
import { DisplayModes } from '@screens/library/constants/constants';
@@ -19,6 +20,7 @@ import { getString } from '@i18n/translations';
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
import NovelCoverImage from './NovelCoverImage';
import { useNovelCoverLayout } from './NovelCoverLayoutContext';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
interface UnreadBadgeProps {
showDownloadBadges: boolean;
@@ -61,6 +63,7 @@ interface INovelCover<TNovel> {
selectedNovelIds?: number[];
globalSearch?: boolean;
imageRequestInit?: ImageRequestInit;
onContinueReading?: () => void;
}
function isFromDB(
@@ -84,6 +87,7 @@ function NovelCover<
globalSearch,
selectedNovelIds,
imageRequestInit,
onContinueReading,
}: INovelCover<TNovel>) {
const selectionActive =
hasSelection ?? (selectedNovelIds != null && selectedNovelIds.length > 0);
@@ -108,6 +112,25 @@ function NovelCover<
[imageRequestInit],
);
const continueReadingButton =
onContinueReading && !selectionActive ? (
<Pressable
accessibilityLabel={getString('novelScreen.continueReading')}
accessibilityRole="button"
android_ripple={{ color: theme.rippleColor }}
onPress={event => {
event.stopPropagation();
onContinueReading();
}}
style={[
styles.continueReadingButton,
{ backgroundColor: Color(theme.primary).alpha(0.9).string() },
]}
>
<MaterialCommunityIcons name="play" size={20} color={theme.onPrimary} />
</Pressable>
) : null;
if (item.completeRow) {
if (!addSkeletonLoading) {
return <></>;
@@ -197,6 +220,13 @@ function NovelCover<
width={coverWidth}
/>
) : null}
{continueReadingButton ? (
<View
style={[styles.continueReadingOverlay, { top: coverHeight - 40 }]}
>
{continueReadingButton}
</View>
) : null}
</Pressable>
</View>
) : (
@@ -227,6 +257,7 @@ function NovelCover<
onPress={selectionActive ? selectNovel : onPress}
onLongPress={selectNovel}
isSelected={isSelected}
continueReadingButton={continueReadingButton}
/>
);
}
@@ -373,6 +404,18 @@ const styles = StyleSheet.create({
position: 'absolute',
right: 4,
},
continueReadingButton: {
alignItems: 'center',
borderRadius: 8,
height: 36,
justifyContent: 'center',
overflow: 'hidden',
width: 36,
},
continueReadingOverlay: {
position: 'absolute',
right: 12,
},
downloadBadge: {
borderBottomLeftRadius: 4,
borderTopLeftRadius: 4,
@@ -0,0 +1,165 @@
import './mocks';
import { Fragment, type ReactElement } from 'react';
import { fireEvent, render, screen } from '@test-utils';
import NovelCover from '../NovelCover';
import { NovelCoverLayoutProvider } from '../NovelCoverLayoutContext';
import { DisplayModes } from '@screens/library/constants/constants';
import { useTheme } from '@hooks/persisted';
import { getString } from '@i18n/translations';
import LibraryNovelItem from '@screens/library/components/LibraryNovelItem';
import type { History } from '@database/types';
jest.mock(
'@screens/browse/loadingAnimation/SourceScreenSkeletonLoading',
() => 'SourceScreenSkeletonLoading',
);
jest.mock('@hooks', () => ({ useDeviceOrientation: () => 'portrait' }));
const novel = {
id: 1,
name: 'Example Novel',
path: '/example',
pluginId: 'example',
};
const onPress = jest.fn();
const onContinueReading = jest.fn();
const lastReadChapter = {
id: 7,
novelId: novel.id,
name: 'Chapter 7',
} as History;
const renderCover = (element: ReactElement) =>
render(element, { wrapper: Fragment });
const Cover = ({
displayMode,
hasSelection = false,
canContinue = true,
}: {
displayMode: DisplayModes;
hasSelection?: boolean;
canContinue?: boolean;
}) => {
const theme = useTheme();
return (
<NovelCoverLayoutProvider
value={{
coverHeight: 160,
displayMode,
numColumns: displayMode === DisplayModes.List ? 1 : 3,
showDownloadBadges: false,
showUnreadBadges: false,
}}
>
<NovelCover
item={novel}
theme={theme}
isSelected={false}
hasSelection={hasSelection}
onPress={onPress}
onLongPress={jest.fn()}
onContinueReading={canContinue ? onContinueReading : undefined}
libraryStatus={false}
/>
</NovelCoverLayoutProvider>
);
};
const LibraryItem = ({
showContinueReadingButton,
}: {
showContinueReadingButton: boolean;
}) => {
const theme = useTheme();
return (
<NovelCoverLayoutProvider
value={{
coverHeight: 160,
displayMode: DisplayModes.Comfortable,
numColumns: 3,
showDownloadBadges: false,
showUnreadBadges: false,
}}
>
<LibraryNovelItem
item={novel}
theme={theme}
isSelected={false}
hasSelection={false}
onSelect={jest.fn()}
onNavigate={jest.fn()}
onContinueReading={onContinueReading}
lastReadChapter={lastReadChapter}
showContinueReadingButton={showContinueReadingButton}
imageRequestInit={undefined}
/>
</NovelCoverLayoutProvider>
);
};
describe('NovelCover continue reading', () => {
beforeEach(() => {
onPress.mockClear();
onContinueReading.mockClear();
});
it.each([DisplayModes.Compact, DisplayModes.Comfortable, DisplayModes.List])(
'opens the saved chapter from %s layout',
displayMode => {
renderCover(<Cover displayMode={displayMode} />);
fireEvent.press(
screen.getByRole('button', {
name: getString('novelScreen.continueReading'),
}),
{ stopPropagation: jest.fn() },
);
expect(onContinueReading).toHaveBeenCalledTimes(1);
expect(onPress).not.toHaveBeenCalled();
},
);
it('hides the shortcut during selection and when there is no reading history', () => {
const { rerender } = renderCover(
<Cover displayMode={DisplayModes.Comfortable} hasSelection />,
);
expect(
screen.queryByRole('button', {
name: getString('novelScreen.continueReading'),
}),
).toBeNull();
rerender(
<Cover displayMode={DisplayModes.Comfortable} canContinue={false} />,
);
expect(
screen.queryByRole('button', {
name: getString('novelScreen.continueReading'),
}),
).toBeNull();
});
it('respects the library display setting', () => {
const { rerender } = renderCover(
<LibraryItem showContinueReadingButton={false} />,
);
expect(
screen.queryByRole('button', {
name: getString('novelScreen.continueReading'),
}),
).toBeNull();
rerender(<LibraryItem showContinueReadingButton />);
fireEvent.press(
screen.getByRole('button', {
name: getString('novelScreen.continueReading'),
}),
{ stopPropagation: jest.fn() },
);
expect(onContinueReading).toHaveBeenCalledWith(novel, lastReadChapter);
});
});
+21
View File
@@ -289,6 +289,27 @@ describe('runDatabaseBootstrap', () => {
sqlite.close();
}
});
it('repairs only an empty default category name', () => {
const sqlite = open({ name: ':memory:' });
try {
createSchema(sqlite);
sqlite.executeSync(
"INSERT INTO Category (id, name, sort) VALUES (1, ' ', 1), (2, 'Legacy user category', 2)",
);
runDatabaseBootstrap(createExecutor(sqlite));
expect(
sqlite.executeSync('SELECT id, name FROM Category ORDER BY id').rows,
).toEqual([
{ id: 1, name: 'categories.default' },
{ id: 2, name: 'Legacy user category' },
]);
} finally {
sqlite.close();
}
});
});
describe('production migrations', () => {
+5 -1
View File
@@ -8,7 +8,10 @@ import { migrate } from 'drizzle-orm/op-sqlite/migrator';
import migrations from '../../drizzle/migrations';
import { createDbManager } from './manager/manager';
import { open } from '@op-engineering/op-sqlite';
import { createCategoryDefaultQuery } from './queryStrings/populate';
import {
createCategoryDefaultQuery,
repairDefaultCategoryNameQuery,
} from './queryStrings/populate';
import {
createCategoryTriggerQuery,
createNovelTriggerQueryDelete,
@@ -332,6 +335,7 @@ const setPragmas = (executor: SqlExecutor) => {
const populateDatabase = (executor: SqlExecutor) => {
console.log('Populating database');
executor.executeSync(createCategoryDefaultQuery);
executor.executeSync(repairDefaultCategoryNameQuery);
};
const createDbTriggers = (executor: SqlExecutor) => {
+66 -24
View File
@@ -8,6 +8,17 @@ import {
novelCategorySchema,
type CategoryRow,
} from '@database/schema';
import { BUILT_IN_CATEGORY_IDS } from '@database/constants';
const DEFAULT_CATEGORY_ID = BUILT_IN_CATEGORY_IDS.default;
const normalizeCategoryName = (categoryName: string): string => {
const normalizedName = categoryName.trim();
if (!normalizedName) {
throw new Error('Category name cannot be empty');
}
return normalizedName;
};
/**
* Get all categories with their novel IDs using Drizzle ORM
@@ -90,11 +101,13 @@ export const getCategoriesWithCount = async (
export const createCategory = async (
categoryName: string,
): Promise<CategoryRow> => {
const normalizedName = normalizeCategoryName(categoryName);
return await dbManager.write(async tx => {
const categoryCount = await tx.$count(categorySchema);
const row = await tx
.insert(categorySchema)
.values({ name: categoryName, sort: categoryCount + 1 })
.values({ name: normalizedName, sort: categoryCount + 1 })
.returning()
.get();
return row;
@@ -152,10 +165,16 @@ export const updateCategory = async (
categoryId: number,
categoryName: string,
): Promise<void> => {
if (categoryId === DEFAULT_CATEGORY_ID) {
return;
}
const normalizedName = normalizeCategoryName(categoryName);
await dbManager.write(async tx => {
await tx
.update(categorySchema)
.set({ name: categoryName })
.set({ name: normalizedName })
.where(eq(categorySchema.id, categoryId))
.run();
});
@@ -165,11 +184,16 @@ export const updateCategory = async (
* Check if a category name already exists using Drizzle ORM
*/
export const isCategoryNameDuplicate = (categoryName: string): boolean => {
const normalizedName = categoryName.trim();
if (!normalizedName) {
return false;
}
const result = dbManager.getSync(
dbManager
.select({ id: categorySchema.id })
.from(categorySchema)
.where(eq(categorySchema.name, categoryName)),
.where(eq(categorySchema.name, normalizedName)),
);
return !!result;
@@ -209,39 +233,57 @@ export const getAllNovelCategories = async (): Promise<NovelCategory[]> => {
*/
export const _restoreCategory = async (
category: BackupCategory,
): Promise<void> => {
await dbManager.write(async tx => {
// Delete existing category with same id or sort
await tx
.delete(categorySchema)
.where(
sql`${categorySchema.id} = ${category.id} OR ${categorySchema.sort} = ${category.sort}`,
)
.run();
novelIdMap?: ReadonlyMap<number, number>,
): Promise<number> => {
return dbManager.write(async tx => {
const isDefaultCategory = category.id === DEFAULT_CATEGORY_ID;
const normalizedName = isDefaultCategory
? undefined
: normalizeCategoryName(category.name);
const existingByName = normalizedName
? await tx
.select({ id: categorySchema.id })
.from(categorySchema)
.where(eq(categorySchema.name, normalizedName))
.get()
: undefined;
const existingDefault =
existingByName || !isDefaultCategory
? undefined
: await tx
.select({ id: categorySchema.id })
.from(categorySchema)
.where(eq(categorySchema.id, DEFAULT_CATEGORY_ID))
.get();
// Insert the category
await tx
.insert(categorySchema)
.values({
id: category.id,
name: category.name,
sort: category.sort,
})
.onConflictDoNothing()
.run();
let categoryId = existingByName?.id ?? existingDefault?.id;
if (!categoryId) {
const restoredCategory = await tx
.insert(categorySchema)
.values({
name: normalizedName ?? getString('categories.default'),
sort: category.sort,
})
.returning({ id: categorySchema.id })
.get();
categoryId = restoredCategory.id;
}
// Insert novel-category associations
if (category.novelIds && category.novelIds.length > 0) {
for (const novelId of category.novelIds) {
for (const backupNovelId of category.novelIds) {
const novelId = novelIdMap?.get(backupNovelId) ?? backupNovelId;
await tx
.insert(novelCategorySchema)
.values({
categoryId: category.id,
categoryId,
novelId: novelId,
})
.onConflictDoNothing()
.run();
}
}
return categoryId;
});
};
+4
View File
@@ -785,7 +785,9 @@ export const getPrevChapter = async (
.orderBy(
desc(castInt(chapterSchema.page)),
desc(castInt(chapterSchema.position)),
desc(chapterSchema.id),
)
.limit(1)
.get();
};
@@ -814,7 +816,9 @@ export const getNextChapter = async (
.orderBy(
asc(castInt(chapterSchema.page)),
asc(castInt(chapterSchema.position)),
asc(chapterSchema.id),
)
.limit(1)
.get();
};
+123 -34
View File
@@ -6,7 +6,12 @@ import { insertChapters } from './ChapterQueries';
import { showToast } from '@utils/showToast';
import { getString } from '@i18n/translations';
import { BackupNovel, DBNovelInfo, NovelInfo } from '../types';
import {
BackupNovel,
DBNovelInfo,
NovelInfo,
type RestoredNovelMapping,
} from '../types';
import { SourceNovel } from '@plugins/types';
import { NOVEL_STORAGE } from '@utils/Storages';
import { downloadFile } from '@plugins/helpers/fetch';
@@ -23,7 +28,36 @@ import { getLibraryDefaultCategoryId } from '@hooks/persisted/useSettings';
import NativeFile from '@modules/native-file';
import { BUILT_IN_CATEGORY_IDS } from '@database/constants';
const getCategoryForNewNovel = async (tx: TransactionParameter) => {
const getBuiltInDefaultCategory = async (tx: TransactionParameter) => {
const defaultCategory = await tx
.select({ id: categorySchema.id })
.from(categorySchema)
.where(eq(categorySchema.id, BUILT_IN_CATEGORY_IDS.default))
.get();
return defaultCategory ? [defaultCategory] : [];
};
const getCategoriesForNewNovel = async (
tx: TransactionParameter,
categoryIds?: number[],
) => {
if (categoryIds !== undefined) {
if (categoryIds.length) {
const selectedCategories = await tx
.select({ id: categorySchema.id })
.from(categorySchema)
.where(inArray(categorySchema.id, categoryIds))
.all();
if (selectedCategories.length) {
return selectedCategories;
}
}
return getBuiltInDefaultCategory(tx);
}
const preferredCategoryId = getLibraryDefaultCategoryId();
if (preferredCategoryId) {
@@ -34,17 +68,16 @@ const getCategoryForNewNovel = async (tx: TransactionParameter) => {
.get();
if (preferredCategory) {
return preferredCategory;
return [preferredCategory];
}
}
return tx
.select({ id: categorySchema.id })
.from(categorySchema)
.where(eq(categorySchema.id, BUILT_IN_CATEGORY_IDS.default))
.get();
return getBuiltInDefaultCategory(tx);
};
const getCategoryForNewNovel = async (tx: TransactionParameter) =>
(await getCategoriesForNewNovel(tx))[0];
/**
* Inserts a novel and its chapters into the database using Drizzle ORM.
* Also handles downloading the novel cover if available.
@@ -138,6 +171,7 @@ export const getNovelByPath = (
export const switchNovelToLibraryQuery = async (
novelPath: string,
pluginId: string,
categoryIds?: number[],
): Promise<NovelInfo | undefined> => {
const novel = await getNovelByPath(novelPath, pluginId);
if (novel) {
@@ -157,16 +191,18 @@ export const switchNovelToLibraryQuery = async (
.run();
showToast(getString('browseScreen.removeFromLibrary'));
} else {
// Add to library: add to default category
const defaultCategory = await getCategoryForNewNovel(tx);
// Add to library with the selected or configured default categories.
const categories = await getCategoriesForNewNovel(tx, categoryIds);
if (defaultCategory) {
if (categories.length) {
await tx
.insert(novelCategorySchema)
.values({
novelId: novel.id,
categoryId: defaultCategory.id,
})
.values(
categories.map((category: { id: number }) => ({
novelId: novel.id,
categoryId: category.id,
})),
)
.run();
}
@@ -195,15 +231,17 @@ export const switchNovelToLibraryQuery = async (
.where(eq(novelSchema.id, novelId))
.run();
const defaultCategory = await getCategoryForNewNovel(tx);
const categories = await getCategoriesForNewNovel(tx, categoryIds);
if (defaultCategory) {
if (categories.length) {
await tx
.insert(novelCategorySchema)
.values({
novelId: novelId,
categoryId: defaultCategory.id,
})
.values(
categories.map((category: { id: number }) => ({
novelId: novelId,
categoryId: category.id,
})),
)
.run();
}
});
@@ -440,18 +478,13 @@ export const updateNovelCategories = async (
/**
* Restores novel and chapters from a backup object.
*/
export const _restoreNovelAndChapters = async (backupNovel: BackupNovel) => {
const { chapters, ...novel } = backupNovel;
await dbManager.write(async tx => {
// Delete existing novel data
await tx.delete(novelSchema).where(eq(novelSchema.id, novel.id)).run();
await tx
.delete(chapterSchema)
.where(eq(chapterSchema.novelId, novel.id))
.run();
// Restore novel
await tx
export const _restoreNovelAndChapters = async (
backupNovel: BackupNovel,
): Promise<RestoredNovelMapping> => {
const { chapters, id: backupNovelId, ...novel } = backupNovel;
return dbManager.write(async tx => {
// Match novels by their stable source identity, not the database-local ID.
const restoredNovel = await tx
.insert(novelSchema)
.values({
...novel,
@@ -459,15 +492,71 @@ export const _restoreNovelAndChapters = async (backupNovel: BackupNovel) => {
chaptersDownloaded: 0,
chaptersUnread: 0,
})
.onConflictDoUpdate({
target: [novelSchema.path, novelSchema.pluginId],
set: {
...novel,
totalChapters: 0,
chaptersDownloaded: 0,
chaptersUnread: 0,
},
})
.returning({ id: novelSchema.id })
.get();
if (novel.cover?.startsWith(`file://${NOVEL_STORAGE}/`)) {
const cacheSuffix = novel.cover.match(/[?#].*$/)?.[0] ?? '';
await tx
.update(novelSchema)
.set({
cover: `file://${NOVEL_STORAGE}/${novel.pluginId}/${restoredNovel.id}/cover.png${cacheSuffix}`,
})
.where(eq(novelSchema.id, restoredNovel.id))
.run();
}
await tx
.delete(chapterSchema)
.where(eq(chapterSchema.novelId, restoredNovel.id))
.run();
const chapterMappings: RestoredNovelMapping['chapters'] = [];
// Restore chapters in batches
if (chapters.length > 0) {
const BATCH_SIZE = 100;
for (let i = 0; i < chapters.length; i += BATCH_SIZE) {
const batch = chapters.slice(i, i + BATCH_SIZE);
await tx.insert(chapterSchema).values(batch).run();
const restoredChapters = await tx
.insert(chapterSchema)
.values(
batch.map(({ id: _chapterId, novelId: _novelId, ...chapter }) => ({
...chapter,
novelId: restoredNovel.id,
})),
)
.returning({ id: chapterSchema.id, path: chapterSchema.path })
.all();
const restoredIdsByPath = new Map(
restoredChapters.map(chapter => [chapter.path, chapter.id]),
);
for (const chapter of batch) {
const restoredChapterId = restoredIdsByPath.get(chapter.path);
if (restoredChapterId !== undefined) {
chapterMappings.push({
backupChapterId: chapter.id,
restoredChapterId,
});
}
}
}
}
return {
pluginId: novel.pluginId,
backupNovelId,
restoredNovelId: restoredNovel.id,
chapters: chapterMappings,
};
});
};
@@ -24,6 +24,7 @@ import {
deleteCategoryById,
updateCategoryOrderInDb,
getAllNovelCategories,
_restoreCategory,
} from '../CategoryQueries';
import { showToast } from '@utils/showToast';
@@ -56,6 +57,89 @@ describe('CategoryQueries', () => {
});
});
describe('_restoreCategory', () => {
it('preserves the default name and adds restored memberships', async () => {
const testDb = getTestDb();
const existingNovelId = await insertTestNovel(testDb, {
inLibrary: true,
});
const restoredNovelId = await insertTestNovel(testDb, {
inLibrary: true,
});
await insertTestNovelCategory(testDb, existingNovelId, 1);
await _restoreCategory({
id: 1,
name: ' ',
sort: 1,
novelIds: [restoredNovelId],
});
const categories = await getCategoriesFromDb();
expect(categories.find(category => category.id === 1)?.name).toBe(
'Default',
);
const memberships = await getAllNovelCategories();
expect(
memberships
.filter(membership => membership.categoryId === 1)
.map(membership => membership.novelId),
).toEqual(expect.arrayContaining([existingNovelId, restoredNovelId]));
});
it('does not overwrite a custom category with a colliding backup ID', async () => {
const testDb = getTestDb();
const existingCategoryId = await insertTestCategory(testDb, {
name: 'Existing Category',
});
const restoredCategoryId = await _restoreCategory({
id: existingCategoryId,
name: 'Restored Category',
sort: 3,
novelIds: [],
});
expect(restoredCategoryId).not.toBe(existingCategoryId);
const categories = await getCategoriesFromDb();
expect(
categories.find(category => category.id === existingCategoryId)?.name,
).toBe('Existing Category');
expect(
categories.find(category => category.id === restoredCategoryId)?.name,
).toBe('Restored Category');
});
it('does not treat a legacy category with ID 2 as the Local category', async () => {
const restoredCategoryId = await _restoreCategory({
id: 2,
name: 'Legacy user category',
sort: 2,
novelIds: [],
});
expect(restoredCategoryId).not.toBe(2);
const categories = await getCategoriesFromDb();
expect(categories.find(category => category.id === 2)?.name).toBe(
'Local',
);
expect(
categories.find(category => category.id === restoredCategoryId)?.name,
).toBe('Legacy user category');
});
it('rejects a restored user category with an empty name', async () => {
await expect(
_restoreCategory({
id: 3,
name: ' ',
sort: 3,
novelIds: [],
}),
).rejects.toThrow('Category name cannot be empty');
});
});
describe('createCategory', () => {
it('should create a new category', async () => {
const result = await createCategory('New Category');
@@ -71,6 +155,18 @@ describe('CategoryQueries', () => {
expect(cat2.sort).toBeGreaterThan(cat1.sort!);
});
it('trims category names', async () => {
const category = await createCategory(' New Category ');
expect(category.name).toBe('New Category');
});
it('rejects an empty category name', async () => {
await expect(createCategory(' ')).rejects.toThrow(
'Category name cannot be empty',
);
});
});
describe('isCategoryNameDuplicate', () => {
@@ -103,6 +199,38 @@ describe('CategoryQueries', () => {
const updated = categories.find(c => c.id === categoryId);
expect(updated?.name).toBe('New Name');
});
it('trims the updated category name', async () => {
const categoryId = await insertTestCategory(getTestDb(), {
name: 'Old Name',
});
await updateCategory(categoryId, ' New Name ');
const categories = await getCategoriesFromDb();
expect(
categories.find(category => category.id === categoryId)?.name,
).toBe('New Name');
});
it('rejects an empty updated category name', async () => {
const categoryId = await insertTestCategory(getTestDb(), {
name: 'Old Name',
});
await expect(updateCategory(categoryId, ' ')).rejects.toThrow(
'Category name cannot be empty',
);
});
it('does not rename the default category', async () => {
await updateCategory(1, 'Renamed Default');
const categories = await getCategoriesFromDb();
expect(categories.find(category => category.id === 1)?.name).toBe(
'Default',
);
});
});
describe('getCategoriesWithCount', () => {
@@ -1008,6 +1008,114 @@ describe('ChapterQueries', () => {
});
});
describe('chapter neighbors', () => {
it('limits rows transferred by both neighbor lookups to one', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb);
for (let position = 0; position < 5; position++) {
await insertTestChapter(testDb, novelId, { position });
}
const client = testDb.sqlite as typeof testDb.sqlite & {
executeRawAsync: typeof testDb.sqlite.executeRaw;
};
const executeRaw = jest.spyOn(client, 'executeRawAsync');
try {
expect((await getNextChapter(novelId, 0, '1'))?.position).toBe(1);
expect((await getPrevChapter(novelId, 4, '1'))?.position).toBe(3);
expect(executeRaw).toHaveBeenCalledTimes(2);
for (const call of executeRaw.mock.calls) {
expect(call[0]).toMatch(/limit \?/i);
expect(call[1]?.at(-1)).toBe(1);
}
for (const result of executeRaw.mock.results) {
expect(await result.value).toHaveLength(1);
}
} finally {
executeRaw.mockRestore();
}
});
it('returns no neighbor before the first or after the last chapter', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb);
await insertTestChapter(testDb, novelId, { page: '1', position: 0 });
await insertTestChapter(testDb, novelId, { page: '2', position: 1 });
expect(await getPrevChapter(novelId, 0, '1')).toBeUndefined();
expect(await getNextChapter(novelId, 1, '2')).toBeUndefined();
});
it('breaks duplicate position ties by id in the navigation direction', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb);
const firstId = await insertTestChapter(testDb, novelId, { position: 1 });
const secondId = await insertTestChapter(testDb, novelId, {
position: 1,
});
expect((await getNextChapter(novelId, 0, '1'))?.id).toBe(firstId);
expect((await getPrevChapter(novelId, 2, '1'))?.id).toBe(secondId);
expect(await getNextChapter(novelId, 1, '1')).toBeUndefined();
expect(await getPrevChapter(novelId, 1, '1')).toBeUndefined();
});
it('preserves numeric-looking page comparisons and integer page ordering', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb);
const page1 = await insertTestChapter(testDb, novelId, { page: '1' });
const page02 = await insertTestChapter(testDb, novelId, { page: '02' });
const page2 = await insertTestChapter(testDb, novelId, { page: '2' });
const page10 = await insertTestChapter(testDb, novelId, { page: '10' });
// The cast parameter gives comparisons numeric affinity: '02' and '2'
// compare equally, and page 10 sorts after page 2.
expect((await getPrevChapter(novelId, 0, '2'))?.id).toBe(page1);
expect((await getPrevChapter(novelId, 0, '10'))?.id).toBe(page2);
expect((await getNextChapter(novelId, 0, '02'))?.id).toBe(page10);
expect((await getPrevChapter(novelId, 0, '02'))?.id).toBe(page1);
expect((await getNextChapter(novelId, 0, '1'))?.id).toBe(page02);
});
it('preserves integer-cast behavior for source-defined page labels', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb);
await insertTestChapter(testDb, novelId, {
page: 'Volume A',
position: 2,
});
const chapterId = await insertTestChapter(testDb, novelId, {
page: 'Volume B',
position: 0,
});
// Labels cast to zero, so existing eligibility treats both as later
// pages and sorting resolves their equal page casts by position.
expect(await getPrevChapter(novelId, 1, 'Volume A')).toBeUndefined();
expect((await getNextChapter(novelId, 1, 'Volume A'))?.id).toBe(
chapterId,
);
});
it('skips excluded chapters at the previous page boundary', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb);
const chapterId = await insertTestChapter(testDb, novelId, {
page: '1',
position: 0,
scanlator: null,
});
await insertTestChapter(testDb, novelId, {
page: '1',
position: 1,
scanlator: 'Excluded',
});
expect((await getPrevChapter(novelId, 0, '2', ['Excluded']))?.id).toBe(
chapterId,
);
});
});
describe('getPrevChapter', () => {
it('should return previous chapter in same page', async () => {
const testDb = getTestDb();
@@ -14,6 +14,7 @@ import {
} from './testData';
import {
categorySchema,
chapterSchema,
novelCategorySchema,
novelSchema,
} from '@database/schema';
@@ -34,6 +35,7 @@ import {
pickCustomNovelCover,
updateNovelCategoryById,
updateNovelCategories,
_restoreNovelAndChapters,
} from '../NovelQueries';
const mockGetLibraryDefaultCategoryId = jest.fn<number | undefined, []>();
@@ -252,6 +254,69 @@ describe('NovelQueries', () => {
).toBe(true);
});
it('should assign the categories selected while adding', async () => {
const testDb = getTestDb();
const firstCategoryId = await insertTestCategory(testDb, {
name: 'First selected category',
});
const secondCategoryId = await insertTestCategory(testDb, {
name: 'Second selected category',
});
const novelId = await insertTestNovel(testDb, {
inLibrary: false,
path: '/test/selected-categories',
pluginId: 'test-plugin',
});
await switchNovelToLibraryQuery(
'/test/selected-categories',
'test-plugin',
[firstCategoryId, secondCategoryId],
);
const associations = await testDb.drizzleDb
.select()
.from(novelCategorySchema)
.where(eq(novelCategorySchema.novelId, novelId))
.all();
expect(associations.map(association => association.categoryId)).toEqual([
firstCategoryId,
secondCategoryId,
]);
});
it('should use the built-in default when no categories are selected', async () => {
const testDb = getTestDb();
const previousDefaultCategoryId = await insertTestCategory(testDb, {
name: 'Previous default category',
});
const novelId = await insertTestNovel(testDb, {
inLibrary: false,
path: '/test/no-selected-categories',
pluginId: 'test-plugin',
});
mockGetLibraryDefaultCategoryId.mockReturnValue(
previousDefaultCategoryId,
);
await switchNovelToLibraryQuery(
'/test/no-selected-categories',
'test-plugin',
[],
);
const associations = await testDb.drizzleDb
.select()
.from(novelCategorySchema)
.where(eq(novelCategorySchema.novelId, novelId))
.all();
expect(associations.map(association => association.categoryId)).toEqual([
BUILT_IN_CATEGORY_IDS.default,
]);
});
it('should fall back when the selected category no longer exists', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb, {
@@ -395,6 +460,73 @@ describe('NovelQueries', () => {
});
});
describe('_restoreNovelAndChapters', () => {
it('does not replace an unrelated novel when backup IDs collide', async () => {
const testDb = getTestDb();
await insertTestNovel(testDb, {
path: '/existing/novel',
pluginId: 'existing-plugin',
name: 'Existing Novel',
inLibrary: true,
});
const mapping = await _restoreNovelAndChapters({
id: 1,
path: '/restored/novel',
pluginId: 'restored-plugin',
name: 'Restored Novel',
cover: null,
summary: null,
author: null,
artist: null,
status: 'Ongoing',
genres: null,
inLibrary: true,
isLocal: false,
totalPages: 0,
chapters: [
{
id: 10,
novelId: 1,
path: '/restored/chapter-1',
name: 'Chapter 1',
releaseTime: null,
readTime: null,
bookmark: false,
unread: true,
isDownloaded: true,
updatedTime: null,
chapterNumber: 1,
page: '1',
progress: null,
position: 0,
scanlator: null,
timeSpent: 0,
},
],
});
expect(mapping.restoredNovelId).not.toBe(1);
expect(
(await getNovelByPath('/existing/novel', 'existing-plugin'))?.name,
).toBe('Existing Novel');
expect(
(await getNovelByPath('/restored/novel', 'restored-plugin'))?.id,
).toBe(mapping.restoredNovelId);
const restoredChapters = await testDb.drizzleDb
.select()
.from(chapterSchema)
.where(eq(chapterSchema.novelId, mapping.restoredNovelId))
.all();
expect(mapping.chapters).toEqual([
{
backupChapterId: 10,
restoredChapterId: restoredChapters[0].id,
},
]);
});
});
describe('updateNovelInfo', () => {
it('should update novel information', async () => {
const testDb = getTestDb();
+6
View File
@@ -8,3 +8,9 @@ INSERT OR IGNORE INTO Category (id, name, sort) VALUES
(1, '${escapeSqlString(getString('categories.default'))}', 1),
(2, '${escapeSqlString(getString('categories.local'))}', 2)
`;
export const repairDefaultCategoryNameQuery = `
UPDATE Category
SET name = '${escapeSqlString(getString('categories.default'))}'
WHERE id = 1 AND TRIM(name) = ''
`;
+10
View File
@@ -121,6 +121,16 @@ export interface BackupNovel extends NovelInfo {
chapters: ChapterInfo[];
}
export interface RestoredNovelMapping {
pluginId: string;
backupNovelId: number;
restoredNovelId: number;
chapters: {
backupChapterId: number;
restoredChapterId: number;
}[];
}
export interface BackupCategory extends Category {
novelIds: number[];
}
+2 -2
View File
@@ -23,7 +23,7 @@ const mockNovel: NovelInfo = {
const createNovelStore = (
overrides: Partial<NovelInfo> = {},
switchNovelToLibrary = jest.fn().mockResolvedValue(undefined),
switchNovelToLibrary = jest.fn().mockResolvedValue(true),
) =>
createStore({
pluginId: 'plugin-id',
@@ -123,7 +123,7 @@ describe('useNovel store', () => {
});
it('toggles follow state after followNovel action', async () => {
const switchNovelToLibrary = jest.fn().mockResolvedValue(undefined);
const switchNovelToLibrary = jest.fn().mockResolvedValue(true);
const store = createNovelStore({ inLibrary: false }, switchNovelToLibrary);
await store.getState().actions.followNovel();
@@ -44,6 +44,7 @@ export const defaultLibrarySettings = {
displayMode: 'comfortable',
showDownloadBadges: true,
showUnreadBadges: true,
showContinueReadingButton: false,
novelsPerRow: 3,
sortOrder: 'DateAdded_DESC',
};
@@ -65,6 +66,8 @@ export const initialChapterGeneralSettings = {
bionicReading: false,
tapToScroll: false,
TTSEnable: true,
pageReaderInvertVolumeButtons: false,
pageReaderDisableAnimation: false,
};
export const initialChapterReaderSettings = {
+8 -3
View File
@@ -38,9 +38,14 @@ export default function useDownload() {
const downloadingNovelIds = useMemo(
() =>
new Set(
downloadQueue.flatMap(item =>
item.task.data.novelId === undefined ? [] : [item.task.data.novelId],
),
downloadQueue.flatMap(item => [
...(item.task.data.novelId === undefined
? []
: [item.task.data.novelId]),
...item.task.data.chapters.flatMap(chapter =>
chapter.novelId === undefined ? [] : [chapter.novelId],
),
]),
),
[downloadQueue],
);
@@ -0,0 +1,171 @@
import { NovelInfo } from '@database/types';
import { getString } from '@i18n/translations';
import { createNovelStoreActions } from '../store/novelStore.actions';
import { createNovelSlice } from '../store/novelStore';
interface BootstrapServiceSlice {
bootstrapNovelAsync: jest.Mock;
bootstrapNovelSync: jest.Mock;
getNextChapterBatch: jest.Mock;
loadUpToBatch: jest.Mock;
}
const mockNovel: NovelInfo = {
id: 1,
path: '/novels/x',
name: 'X',
pluginId: 'plugin.test',
};
interface TestState {
novel: NovelInfo | undefined;
novelPath: string;
pluginId: string;
pageIndex: number;
pages: string[];
novelSettings: { filter: never[]; excludedScanlators: never[] };
loading: boolean;
fetching: boolean;
error: string | undefined;
}
const createHarness = (overrides: Partial<TestState> = {}) => {
let state: TestState = {
novel: undefined,
novelPath: '/x',
pluginId: 'plugin.test',
pageIndex: 0,
pages: [],
novelSettings: { filter: [], excludedScanlators: [] },
loading: false,
fetching: false,
error: undefined,
...overrides,
};
const set = jest.fn(
(partial: Partial<TestState> | ((s: TestState) => Partial<TestState>)) => {
const patch = typeof partial === 'function' ? partial(state) : partial;
state = { ...state, ...patch };
},
);
const get = () => state;
const bootstrapService: jest.Mocked<BootstrapServiceSlice> = {
bootstrapNovelAsync: jest.fn(),
bootstrapNovelSync: jest.fn(),
getNextChapterBatch: jest.fn(),
loadUpToBatch: jest.fn(),
};
let requestVersion = 0;
const chapterRequestCoordinator = {
current: jest.fn(() => requestVersion),
invalidate: jest.fn(() => ++requestVersion),
};
const transformChapters = jest.fn(chs => chs);
const actions = createNovelStoreActions({
//@ts-expect-error partial state/actions for testing
set, //@ts-expect-error
get,
deps: {
bootstrapService,
chapterActionsDependencies: {} as never,
chapterRequestCoordinator,
transformChapters,
} as never,
defaultChapterSort: 'positionAsc',
});
return {
actions,
getState: () => state,
bootstrapService,
chapterRequestCoordinator,
};
};
describe('novelStore.bootstrap', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('starts in a loading state until the initial bootstrap completes', () => {
const state = createNovelSlice({
pluginId: 'plugin.test',
novelPath: '/x',
initialNovelSettings: {
sort: 'positionAsc',
filter: [],
showChapterTitles: true,
},
});
expect(state.loading).toBe(true);
expect(state.fetching).toBe(true);
});
it('maps a missing novel to the not-found error', async () => {
const harness = createHarness();
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
ok: false,
reason: 'missing-novel',
});
const ok = await harness.actions.bootstrapNovel();
expect(ok).toBe(false);
expect(harness.getState().error).toBe(getString('novelScreen.notFound'));
expect(harness.getState().loading).toBe(false);
});
it('maps a bootstrap error to the load-failed message, never the raw error', async () => {
const harness = createHarness();
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
ok: false,
reason: 'error',
error: new Error('boom'),
});
const ok = await harness.actions.bootstrapNovel();
expect(ok).toBe(false);
expect(harness.getState().error).toBe(getString('novelScreen.loadFailed'));
expect(harness.getState().error).not.toBe('boom');
});
it('maps a missing-chapters failure to the unable-to-get-novel message', async () => {
const harness = createHarness();
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
ok: false,
reason: 'missing-chapters',
});
const ok = await harness.actions.bootstrapNovel();
expect(ok).toBe(false);
expect(harness.getState().error).toBe(
getString('updatesScreen.unableToGetNovel'),
);
expect(harness.getState().loading).toBe(false);
});
it('clears the error on a successful bootstrap', async () => {
const harness = createHarness({ error: 'previous error' });
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
ok: true,
novel: mockNovel,
pages: ['1'],
pageIndex: 0,
scanlators: [],
chapters: [],
batchInformation: { batch: 0, total: 0, totalChapters: 0 },
firstUnreadChapter: undefined,
});
const ok = await harness.actions.bootstrapNovel();
expect(ok).toBe(true);
expect(harness.getState().error).toBeUndefined();
expect(harness.getState().novel).toBe(mockNovel);
});
});
@@ -20,7 +20,10 @@ interface Props {
path: string;
novel?: NovelInfo;
defaultChapterSort: ChapterOrderKey;
switchNovelToLibrary: (novelPath: string, pluginId: string) => Promise<void>;
switchNovelToLibrary: (
novelPath: string,
pluginId: string,
) => Promise<boolean>;
}
export function createStore({
@@ -1,5 +1,7 @@
import { ChapterFilterKey, ChapterOrderKey } from '@database/constants';
import { getString } from '@i18n/translations';
import { NovelSettings } from '../types';
import { BootstrapFailureResult } from '../store-helper/bootstrapService';
import {
GetState,
NovelStoreDependencies,
@@ -14,6 +16,17 @@ interface CreateNovelStoreActionsParams {
defaultChapterSort: ChapterOrderKey;
}
const resolveBootstrapError = (result: BootstrapFailureResult): string => {
if (result.reason === 'missing-chapters') {
return getString('updatesScreen.unableToGetNovel');
}
if (result.reason === 'error') {
// A real fetch/parse failure — don't claim the novel is missing.
return getString('novelScreen.loadFailed');
}
return getString('novelScreen.notFound');
};
export const createNovelStoreActions = ({
set,
get,
@@ -42,7 +55,7 @@ export const createNovelStoreActions = ({
const requestId = deps.chapterRequestCoordinator.invalidate();
inflightBootstrap = (async () => {
set({ loading: true, fetching: true });
set({ loading: true, fetching: true, error: undefined });
const state = get();
const result = await deps.bootstrapService.bootstrapNovelAsync({
@@ -63,6 +76,7 @@ export const createNovelStoreActions = ({
set({
loading: false,
fetching: false,
error: resolveBootstrapError(result),
});
return false;
}
@@ -248,7 +262,11 @@ export const createNovelStoreActions = ({
return;
}
await deps.switchNovelToLibrary(state.novelPath, state.pluginId);
const completed = await deps.switchNovelToLibrary(
state.novelPath,
state.pluginId,
);
if (!completed) return;
set(inner => {
if (!inner.novel) {
return {};
@@ -21,8 +21,9 @@ export const createNovelSlice = ({
initialLastRead,
}: CreateNovelStoreParams) => {
return {
loading: false,
fetching: false,
loading: true,
fetching: true,
error: undefined,
pluginId,
novelPath,
novel,
@@ -23,6 +23,7 @@ export interface ChapterSliceState {
export interface NovelStoreData extends ChapterSliceState {
loading: boolean;
fetching: boolean;
error: string | undefined;
pluginId: string;
novelPath: string;
@@ -95,7 +96,10 @@ export interface NovelStoreDependencies {
persistPageIndex?: (value: number) => void;
persistNovelSettings?: (value: NovelSettings) => void;
persistLastRead?: (value: ChapterInfo) => void;
switchNovelToLibrary?: (novelPath: string, pluginId: string) => Promise<void>;
switchNovelToLibrary?: (
novelPath: string,
pluginId: string,
) => Promise<boolean>;
}
export type SetState = {
+9
View File
@@ -114,6 +114,8 @@ export interface BrowseSettings {
export interface LibrarySettings {
/** User-selected category for newly added novels. */
defaultCategoryId?: number;
/** Prompt for categories whenever a novel is added to the library. */
promptForCategoryOnAdd?: boolean;
/** Last category viewed in the library. */
lastUsedCategoryId?: number;
globalUpdateExcludeCategoryIds?: number[];
@@ -122,6 +124,7 @@ export interface LibrarySettings {
filter?: LibraryFilter;
showDownloadBadges?: boolean;
showUnreadBadges?: boolean;
showContinueReadingButton?: boolean;
showNumberOfNovels?: boolean;
displayMode?: DisplayModes;
novelsPerRow?: number;
@@ -191,6 +194,8 @@ export interface ChapterGeneralSettings {
bionicReading: boolean;
tapToScroll: boolean;
TTSEnable: boolean;
pageReaderInvertVolumeButtons: boolean;
pageReaderDisableAnimation: boolean;
}
export interface ReaderTheme {
@@ -316,6 +321,8 @@ export const initialChapterGeneralSettings: ChapterGeneralSettings = {
bionicReading: false,
tapToScroll: false,
TTSEnable: true,
pageReaderInvertVolumeButtons: false,
pageReaderDisableAnimation: false,
};
export const initialChapterReaderSettings: ChapterReaderSettings = {
@@ -376,12 +383,14 @@ export const useBrowseSettings = () => {
};
const defaultLibrarySettings: LibrarySettings = {
promptForCategoryOnAdd: false,
showNumberOfNovels: false,
downloadedOnlyMode: false,
incognitoMode: false,
displayMode: DisplayModes.Comfortable,
showDownloadBadges: true,
showUnreadBadges: true,
showContinueReadingButton: false,
novelsPerRow: 3,
sortOrder: LibrarySortOrder.DateAdded_DESC,
};
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -2,7 +2,7 @@
"aboutScreen": {
"website": "الموقع",
"discord": "ديس كورد",
"github": "كيت هاب",
"github": "Arabic",
"helpTranslate": "المساعدة في الترجَمة",
"plugins": "Plugins",
"version": "آلإصدار",
@@ -22,6 +22,7 @@
"dataManagement": "إدارة البيانات",
"deleteReadChapters": "حذف الفصول المقروءة",
"deleteReadChaptersDialogTitle": "حذف جميع الفصول التي تم قراءتها",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "تمت إعادة إنشاء فهارس قاعدة البيانات",
"recreateDBIndexesDialogTitle": "سيتم إعادة إنشاء جميع فهارس قاعدة البيانات.\nقد يستغرق هذا بعض الوقت.",
"recreateDBIndexesDesc": "يعيد إنشاء فهارس قاعدة البيانات. قد يؤدي هذا إلى تحسين الأداء على الأجهزة الأبطأ.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "مظهر",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "تحضير البيانات",
"preparingSelectedFiles": "Preparing selected files",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "لا يمكنك حذف التصنيف الافتراضي",
"default": "افتراضي",
"defaultCategory": "التصنيف الافتراضي",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "هل ترغب في حذف التصنيف",
"header": "حذف التصنيف"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "ليس لديك تصنيفات. اضغط على زر تعديل لإنشاء واحد لتنظيم مكتبتك"
},
"repositories": {
"emptyMsg": "ليس لديك مستودعات. أضف مستودع الإضافات الأول لتبدأ."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "ليس لديك مستودعات. أضف مستودع الإضافات الأول لتبدأ.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "حول",
@@ -340,7 +349,12 @@
"submit": "إرسال",
"loading": "جارٍ التحميل...",
"moreOptions": "More options",
"warning": "تحذير"
"warning": "تحذير",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "تحديث",
@@ -374,10 +388,19 @@
"pluginNotFound": "لم يتم العثور على الإضافة!",
"removeDownloadsWarning": "هل أنت متأكد؟ سيتم حذف جميع الفصول المحملة."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "العامة",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "جميع المصادر",
"searchIn": "البحث عن رواية في"
"searchIn": "البحث عن رواية في",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "السجل",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "نقل",
"noSummary": "لا يوجد ملخص",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "التمرير إلى الفصل الحالي",
"scrollToTop": "التمرير إلى الأعلى"
},
"emptyChapterMessage": "<h2>تعذر تحميل الفصل</h2><p>لم يتم العثور على محتوى قابل للقراءة. تحقق من الفصل في WebView؛ إذا تم تحميله هناك، <a href='%{reportUrl}'>فأبلغ عن مشكلة الإضافة</a>.</p><p>الإضافة: %{pluginId}<br>الرواية: %{novelName}<br>الفصل: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "انتهى",
"nextChapter": "التالي: %{name}",
"noNextChapter": "لا يوجد فصل تالي",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "ডিফল্ট শ্ৰেণী",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "আপুনি শ্ৰেণী মচিব বিচাৰে নেকি?",
"header": "শ্ৰেণী মচক"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "আপোনাৰ কোনো শ্ৰেণী নাই। সম্পাদনা বুটামত টেপ কৰি এটি সৃষ্টি কৰক"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "জমা দিয়ক",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "সকলো উৎস",
"searchIn": "উৎসত উপন্যাস অনুসন্ধান কৰক"
"searchIn": "উৎসত উপন্যাস অনুসন্ধান কৰক",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "ইতিহাস",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "স্থানান্তৰিত কৰক",
"noSummary": "কোনো সংক্ষিপ্তসাৰ নাই",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "বৰ্তমান অধ্যায়লৈ স্ক্ৰল কৰক",
"scrollToTop": "শীৰ্ষলৈ স্ক্ৰল কৰক"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "শেষ",
"nextChapter": "Next: %{name}",
"noNextChapter": "পৰবর্তী অধ্যায় নাই",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Výchozí kategorie",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Přejete si odstranit kategorii",
"header": "Smazat kategorii"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "Nemáte žádné kategorie. Klepněte na tlačítko editovat pro vytvoření nové kategorie"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Odeslat",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "všechny zdroje",
"searchIn": "Hledat novelu v"
"searchIn": "Hledat novelu v",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Historie",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrovat",
"noSummary": "Žádné shrnutí",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Přejít na aktuální kapitolu",
"scrollToTop": "Přejít na začátek"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Dokončeno",
"nextChapter": "Next: %{name}",
"noNextChapter": "Nejsou žádné další kapitoly",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Gelesene Kapitel löschen",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Standard-Kategorie",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Möchten Sie diese Kategorie löschen?",
"header": "Kategorie löschen"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "Sie haben keine Kategorien. Tippen Sie auf die Plus-Schaltfläche, um eine für die Organisation Ihrer Bibliothek zu erstellen"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Bestätigen",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "Allgemein",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "Alle Quellen",
"searchIn": "Suche ein Buch in"
"searchIn": "Suche ein Buch in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Verlauf",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrieren",
"noSummary": "Keine Beschreibung",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Zum aktuellen Kapitel springen",
"scrollToTop": "Zum Anfang springen"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Beendet",
"nextChapter": "Next: %{name}",
"noNextChapter": "Es gibt kein weiteres Kapitel",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -51,7 +55,7 @@
"appLanguageDefault": "Default",
"appTheme": "App theme",
"darkTheme": "Dark Theme",
"dateFormat": "Date format",
"dateFormat": "Μορφοποίησή ημερομηνίας",
"dateFormatDefault": "Default (%{date})",
"dynamicColors": "Dynamic",
"hideBackdrop": "Hide backdrop",
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Υποβολή",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Ιστορικό",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Κύλιση στο τρέχον κεφάλαιο",
"scrollToTop": "Κύλιση προς τα πάνω"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "Δεν υπάρχει επόμενο κεφάλαιο",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+69 -10
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -383,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -429,6 +443,7 @@
"jumpToLastReadChapter": "Jump to last read chapter in list",
"novel": "Novel",
"novelBadges": "Novel Badges",
"showContinueReadingButton": "Show continue reading button",
"novelSort": "Novel Sort",
"refreshMetadata": "Automatically refresh metadata",
"refreshMetadataDescription": "Check for new cover and details when updating library",
@@ -440,7 +455,8 @@
"updateLibrary": "Update library on launch",
"updateLibraryDesc": "Not recommended for low-end devices",
"updateTime": "Show last update time",
"useFAB": "Use FAB in Library",
"useFAB": "Show library resume button",
"useFABDescription": "Show a floating button that opens the most recently read chapter.",
"timeTracking": "Time Tracking",
"enableTimeTracking": "Enable time tracking",
"enableTimeTrackingDesc": "Tracks time spent reading and displays it in the statistics screen",
@@ -450,7 +466,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -469,12 +488,14 @@
"badges": "Badges",
"comfortable": "Comfortable grid",
"compact": "Compact grid",
"continueReadingButton": "Continue reading button",
"displayMode": "Display mode",
"download": "Download",
"downloadBadges": "Download badges",
"list": "List",
"noTitle": "Cover only grid",
"numberOfItems": "Number of Items",
"overlays": "Overlays",
"showNoOfItems": "Show number of items",
"unread": "Unread",
"unreadBadges": "Unread badges"
@@ -600,9 +621,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -645,6 +668,10 @@
"padding": "Padding",
"pageReader": "Paged reading (Experimental)",
"pageReaderDescription": "Split the chapter into pages instead of scrolling. Experimental — may break formatting in some novels",
"invertVolumeButtons": "Invert volume buttons",
"invertVolumeButtonsDescription": "Swap volume up and down buttons for previous and next page",
"disablePageTransitions": "Disable page transition animation",
"disablePageTransitionsDescription": "Turn pages instantly without slide animation",
"removeExtraSpacing": "Remove extra spacing",
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
"scrollAmount": "Scroll amount (screen height by default)",
@@ -713,9 +740,14 @@
"genreDistribution": "Genre distribution",
"novels": "novels",
"readChapters": "Read chapters",
"readLabel": "Read",
"downloadedLabel": "Downloaded",
"sources": "Sources",
"statusDistribution": "Status distribution",
"title": "Statistics",
"readingTime": "Reading time",
"chaptersInLibrary": "chapters in library",
"customizeGenres": "Customize",
"titlesInLibrary": "Titles in library",
"totalChapters": "Total chapters",
"unreadChapters": "Unread chapters",
@@ -789,7 +821,20 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"customCodeSettings": {
"description": "Rules and snippets run on every chapter you read, in the order shown.",
"textRules": "Text Rules",
"ruleCount": {
"one": "%{count} rule",
"other": "%{count} rules"
},
"activeSnippetCount": {
"one": "%{count} of %{total} active",
"other": "%{count} of %{total} active"
},
"addReplaceRule": "Add replacement",
"addRemoveRule": "Add removal",
"renameHint": "Long press to rename",
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
@@ -804,11 +849,16 @@
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"regexHint": "Plain text affects all matches. For regex, use /pattern/flags and add g to affect all matches.",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"beautifyCode": "Beautify code",
"beautifyFailed": "Couldn't format the code. Check its syntax and try again.",
"syntaxHighlighting": "Syntax highlighting",
"importCode": "Import code",
"imported": "Imported"
},
"genreStats": {
@@ -828,12 +878,21 @@
"one": "%{count} subgenre",
"other": "%{count} subgenres"
},
"novelsIn": "Novels in %{genre}",
"novels": "Novels",
"noNovels": "No novels found for this category",
"seeAllNovels": "See all",
"editTaxonomy": "Edit genre taxonomy",
"taxonomyTitle": "Genre taxonomy",
"parentCategories": "Parent categories",
"parentCategories": "Genre groups",
"genreGroups": "Genre groups",
"taxonomyDescription": "Statistics group each novel's genres under these groups. Genres outside a group are counted individually.",
"newGroup": "New group",
"editGenreGroup": "Edit genre group",
"genresInGroup": "Genres in this group",
"noGenresInGroup": "No genres yet",
"foundInLibrary": "Found in your library",
"ungroupedGenresDescription": "Genres your sources use that aren't in any group yet",
"deleteGroupConfirm": "Delete this genre group? Its genres will be counted individually.",
"normalization": "Normalization rules",
"addCategory": "Add category",
"addChild": "Add subgenre",
@@ -842,11 +901,11 @@
"deleteCategoryConfirm": "Delete this category and its subgenres?",
"deleteChildConfirm": "Delete this subgenre?",
"deleteNormalizationConfirm": "Delete this normalization rule?",
"parentNamePlaceholder": "Category name",
"childNamePlaceholder": "Add Subgenre",
"parentNamePlaceholder": "Group name",
"childNamePlaceholder": "Add genre",
"normalizationKeyPlaceholder": "e.g. sci-fi",
"normalizationValuePlaceholder": "e.g. Sci-Fi",
"noCategories": "No parent categories configured",
"noCategories": "No genre groups yet",
"noNormalization": "No normalization rules configured"
}
}
+105 -55
View File
@@ -22,6 +22,7 @@
"dataManagement": "Administración de datos",
"deleteReadChapters": "Eliminar capítulos leídos",
"deleteReadChaptersDialogTitle": "Todos los capítulos marcados como leídos serán borrados.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Índices de la Base de Datos Recreados",
"recreateDBIndexesDialogTitle": "Todos los índices de la base de datos serán Recreados. Esto puede tomar un tiempo.",
"recreateDBIndexesDesc": "Recrea los índices de la Base de Datos. Esto puede mejorar el rendimiento en dispositivos más lentos.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Apariencia",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparando datos",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "No se puede eliminar la categoría predeterminada",
"default": "Predeterminado",
"defaultCategory": "Categoría por defecto",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "¿Deseas eliminar la categoría?",
"header": "Eliminar categoría"
@@ -287,13 +292,17 @@
"setModalEmptyMsg": "No tienes categorías. Pulsa el botón Editar para crear una para organizar tu biblioteca"
},
"repositories": {
"emptyMsg": "No tienes repositorios. Añade tu primer repositorio de plugins para empezar."
"disable": "Deshabilitar",
"disableTitle": "¿Desactivar repositorio?",
"disableWarning": "LNReader dejará de buscar actualizaciones en %{name}. Sus plugins se ocultarán a menos que otro repositorio activo los proporcione. Los plugins instalados permanecerán instalados.",
"emptyMsg": "No tienes repositorios. Añade tu primer repositorio de plugins para empezar.",
"toggle": "Alternar repositorio %{name}"
},
"common": {
"about": "Acerca de",
"add": "Añadir",
"all": "Todos",
"back": "Back",
"back": "Volver",
"backup": "Copia de seguridad",
"cancel": "Cancelar",
"categories": "Categorías",
@@ -311,14 +320,14 @@
"filter": "Filtrar",
"globally": "globalmente",
"install": "Instalar",
"imported": "Imported",
"later": "Later",
"imported": "Importado",
"later": "Más tarde",
"logout": "Cerrar sesión",
"name": "Nombre",
"newUpdateAvailable": "Nueva actualización disponible",
"none": "None",
"none": "Ninguno",
"ok": "Ok",
"close": "Close",
"close": "Cerrar",
"pause": "Pausa",
"preparing": "Preparando",
"remove": "Eliminar",
@@ -327,75 +336,89 @@
"resume": "Reanudar",
"retry": "Reintentar",
"save": "Guardar",
"saved": "Saved",
"saved": "Guardar",
"search": "Buscar",
"skipVersion": "Skip version",
"skipVersion": "Saltar versión",
"searchFor": "Buscar por",
"searchResults": "Resultados de la búsqueda",
"settings": "Configuración",
"show": "Mostrar",
"signIn": "Sign in",
"signOut": "Sign out",
"signIn": "Entrar",
"signOut": "Salir",
"sort": "Ordenar",
"submit": "Enviar",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"loading": "Cargando ...",
"moreOptions": "Más opciones",
"warning": "Aviso",
"code": "Código",
"custom_code": "Cód. Propio",
"textToReplace": "Texto a buscar",
"replaceWith": "Remplazar con",
"replaceText": "Remplazar texto"
},
"webview": {
"refresh": "Refresh",
"share": "Share",
"openInWebView": "Open in WebView",
"openInBrowser": "Open in browser",
"clearCookies": "Clear cookies",
"cookiesCleared": "Cookies cleared",
"clearData": "Clear WebView data",
"dataDeleted": "WebView data cleared"
"refresh": "Recargar",
"share": "Compartir",
"openInWebView": "Abrir en vista web",
"openInBrowser": "Abrir en el navegador",
"clearCookies": "Eliminar cookies",
"cookiesCleared": "Cookies borradas",
"clearData": "Borrar datos de vista web",
"dataDeleted": "Datos de vista web borrados"
},
"date": {
"calendar": {
"lastDay": "[Yesterday]",
"lastWeek": "[Last] dddd",
"nextDay": "[Tomorrow]",
"sameDay": "[Today]"
"lastDay": "[Ayer]",
"lastWeek": "[El] dddd [pasado]",
"nextDay": "[Mañana]",
"sameDay": "[Hoy]"
}
},
"downloadScreen": {
"cancelDownloads": "Cancel downloads",
"cancelled": "Downloads cancelled.",
"chapterEmptyOrScrapeError": "Either chapter is empty or the app couldn't scrape it",
"chapterName": "Chapter: %{name}",
"completed": "Download completed",
"storageInfo": "Downloaded chapters are stored in the app's private storage on this device.",
"downloading": "Downloading",
"downloadingNovel": "Downloading: %{name}",
"cancelDownloads": "Cancelar descargas",
"cancelled": "Descarga cancelada.",
"chapterEmptyOrScrapeError": "El capítulo está vacío o la app no pudo extraer el contenido",
"chapterName": "Capítulo: %{name}",
"completed": "Descarta completada",
"storageInfo": "Los capítulos descargados se guardan en el almacenamiento privado de la app en este dispositivo.",
"downloading": "Descargando",
"downloadingNovel": "Descargando:%{name}",
"downloadsLower": "descargas",
"noDownloads": "No hay descargas",
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
"pluginNotFound": "¡Plugin no funciona!",
"removeDownloadsWarning": "¿Estás seguro? Se eliminarán todos los capítulos descargados."
},
"taskQueue": {
"cancelTaskAction": "Cancelar tarea",
"cancelTaskTitle": "¿Cancelar tarea?",
"cancelTaskConfirmation": "¿Estás seguro de que quieres cancelar %{task}?",
"keepTaskAction": "Conservar tarea"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"restart": "Restart the application"
"title": "Ocurrió un error inesperado",
"description": "La aplicación experimentó un error inesperado. Por favor, comparte los registros de fallo en nuestro canal de soporte de Discord.",
"copyStackTrace": "Copiar seguimiento del error",
"shareCrashLogs": "Compartir registro de fallos",
"shareCrashLogsFailed": "No se pudieron compartir los registros de fallo. En su lugar, se copió el seguimiento del error.",
"shareCrashLogsFailedWithoutCopy": "No se pudieron compartir los registros de fallo ni copiar el seguimiento del error.",
"restart": "Reiniciar aplicación"
},
"generalSettings": "General",
"generalSettingsScreen": {
"asc": "(Ascendente)",
"autoDownload": "Descarga automática",
"automaticUpdates": "Automatic updates",
"automaticUpdatesDaily": "Daily",
"automaticUpdatesDescription": "Runs when a network connection is available. Android may delay background work to save battery.",
"automaticUpdatesEvery12Hours": "Every 12 hours",
"automaticUpdatesEvery2Days": "Every 2 days",
"automaticUpdatesEvery3Days": "Every 3 days",
"automaticUpdatesOff": "Off",
"automaticUpdatesWeekly": "Weekly",
"automaticUpdates": "Actualizaciones automáticas",
"automaticUpdatesDaily": "Diariamente",
"automaticUpdatesDescription": "Ejecuta cuando hay disponible una conexión de red. Android puede retrasar el trabajo en segundo plano para ahorrar batería.",
"automaticUpdatesEvery12Hours": "Cada 12 horas",
"automaticUpdatesEvery2Days": "Cada 2 días",
"automaticUpdatesEvery3Days": "Cada 3 días",
"automaticUpdatesOff": "Desactivar",
"automaticUpdatesWeekly": "Semanal",
"bySource": "Por fuente",
"chapterSort": "Orden de capítulo por defecto",
"desc": "(Descendente)",
"disableLoadingAnimations": "Disable loading animations",
"disableLoadingAnimations": "Desactivar animación de carga",
"disableLoadingAnimationsDesc": "May improve performance on slower devices",
"disableHapticFeedback": "Deshabilitar la respuesta háptica",
"disableHapticFeedbackDescription": "Turn off vibrations for touch interactions.",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "Todas las fuentes",
"searchIn": "Buscar una novela en"
"searchIn": "Buscar una novela en",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Historial",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrar",
"noSummary": "Sin resumen",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Desplazar al capítulo actual",
"scrollToTop": "Desplazar a arriba"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finalizado",
"nextChapter": "Next: %{name}",
"noNextChapter": "No hay próximo capítulo",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -11,7 +11,7 @@
"advancedSettings": "Avancé",
"advancedSettingsScreen": {
"cachedNovelsDeletedToast": "Romans en cache supprimés",
"chapterInsertFailed": "Échec de l'insertion du chapitre",
"chapterInsertFailed": "Insertion de chapitre échoué",
"clearCachedNovels": "Vider les romans en cache",
"clearCachedNovelsDesc": "Supprimer les romans en cache qui ne sont pas dans votre bibliothèque",
"clearDatabaseWarning": "Les progrès et les chapitres lus et téléchargés hors de la bibliothèque, seront perdus.",
@@ -22,6 +22,7 @@
"dataManagement": "Gestion de données",
"deleteReadChapters": "Supprimer les chapitres lus",
"deleteReadChaptersDialogTitle": "Tous les chapitres marqués comme lus seront supprimés.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Index de la base de données recréés",
"recreateDBIndexesDialogTitle": "Tous les index de la base de données seront recréés.\nCela peut prendre un certain temps.",
"recreateDBIndexesDesc": "Recrée les index de la base de données. Cela peut améliorer les performances sur les appareils plus lents.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Apparence",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "Vous ne pouvez pas supprimer la catégorie par défaut",
"default": "Défaut",
"defaultCategory": "Catégorie par défaut",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Voulez-vous supprimer cette catégorie",
"header": "Supprimer la catégorie"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "Vous n'avez pas de catégories. Appuyez sur le bouton Modifier pour en créer une pour organiser votre bibliothèque"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "À propos",
@@ -340,7 +349,12 @@
"submit": "Soumettre",
"loading": "Chargement ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Actualiser",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin introuvable!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "Général",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "toutes les sources",
"searchIn": "Rechercher un novel dans"
"searchIn": "Rechercher un novel dans",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Historique",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrer",
"noSummary": "Aucun résumé",
"noCoverFound": "Aucune couverture trouvée",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Commencer à lire %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Défiler vers le chapitre actuel",
"scrollToTop": "Défiler jusqu'en haut"
},
"emptyChapterMessage": "<h2>Impossible de charger le chapitre</h2><p>Aucun contenu lisible n'a été trouvé. Vérifiez le chapitre dans la WebView ; s'il s'y charge, <a href='%{reportUrl}'>signalez le problème du plugin</a>.</p><p>Plugin : %{pluginId}<br>Roman : %{novelName}<br>Chapitre : %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Terminé",
"nextChapter": "Suivant: %{name}",
"noNextChapter": "Il n'y a pas de prochain chapitre",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "ניהול מידע",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "תצוגה",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Olvasott fejezetek törlése",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Alapértelmezett kategória",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Kategória törlése"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Küldés",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "Általános",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "összes forrás",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Előzmények",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Áttelepítés",
"noSummary": "Nincs összefoglaló",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Aktuális fejezethez ugrás",
"scrollToTop": "Elejére ugrás"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Befejezett",
"nextChapter": "Next: %{name}",
"noNextChapter": "Nincs következő fejezet",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
File diff suppressed because one or more lines are too long
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Elimina capitoli letti",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Categoria predefinita",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Vuoi eliminare la categoria",
"header": "Elimina categoria"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "Non hai categorie. Tocca il pulsante Modifica per crearne una per organizzare la tua libreria"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -308,7 +317,7 @@
"downloads": "Downloads",
"edit": "Modifica",
"example": "Example",
"filter": "Filtra",
"filter": "Filtra",
"globally": "globale",
"install": "Installa",
"imported": "Imported",
@@ -340,7 +349,12 @@
"submit": "Invia",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "Generale",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "Tutte le fonti",
"searchIn": "Cerca una novel in"
"searchIn": "Cerca una novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Cronologia",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migra",
"noSummary": "Nessuna trama",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scorri al capitolo corrente",
"scrollToTop": "Scorri in cima"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finito",
"nextChapter": "Next: %{name}",
"noNextChapter": "Non c'è un capitolo successivo",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "デフォルトカテゴリー",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "カテゴリーを削除しますか?",
"header": "カテゴリーを削除"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "カテゴリーがありません。ライブラリを整理したい場合、プラスボタンをタップしてカテゴリーを作成してください。"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "履歴",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "移行",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "次の章はありません",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+71 -21
View File
@@ -1,14 +1,14 @@
{
"aboutScreen": {
"website": "Website",
"discord": "Discord",
"github": "GitHub",
"helpTranslate": "Help translate",
"plugins": "Plugins",
"version": "Version",
"whatsNew": "What's new"
"website": "웹사이트",
"discord": "디스코드",
"github": "깃허브",
"helpTranslate": "번역에 도움을 주세요.",
"plugins": "플러그인",
"version": "버전 정보",
"whatsNew": "새로운 소식"
},
"advancedSettings": "Advanced",
"advancedSettings": "고급 설정",
"advancedSettingsScreen": {
"cachedNovelsDeletedToast": "Cached novels deleted",
"chapterInsertFailed": "chapter insert failed",
@@ -19,10 +19,11 @@
"clearUpdatesTab": "Clear updates tab",
"clearUpdatesWarning": "Updates tab will be cleared.",
"clearupdatesTabDesc": "Clears chapter entries in updates tab",
"dataManagement": "Data Management",
"dataManagement": "데이터 관리",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"disabled": "Disabled",
"diagnostics": "Diagnostics",
"disabled": "비활성화",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
"importEpub": "Import EPUB",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+60 -10
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Verwijder gelezen hoofdstukken",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,7 +242,7 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Standaard categorie",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Wilt u deze categorie verwijderen",
"header": "Categorie verwijderen"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "Je hebt geen categorieën. Tik op de plus-knop om er een te maken om je bibliotheek te organiseren"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "Algemeen",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "alle bronnen",
"searchIn": "Zoek een roman in"
"searchIn": "Zoek een roman in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Geschiedenis",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migreer",
"noSummary": "Geen samenvatting",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Voltooid",
"nextChapter": "Next: %{name}",
"noNextChapter": "Er is geen volgend hoofdstuk",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+65 -15
View File
@@ -22,6 +22,7 @@
"dataManagement": "Manejo de informação",
"deleteReadChapters": "Excluir capítulos lidos",
"deleteReadChaptersDialogTitle": "Você tem certeza? Todos os capítulos marcados como lidos serão excluídos.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -31,13 +32,16 @@
"importStaticFiles": "Importar arquivo estático",
"novelInsertFailed": "falha ao inserir livro",
"networking": "Networking",
"useFAB": "Utilize FAB em vez do botão",
"useFAB": "Utilize FAV invez do botão",
"userAgent": "Agente usuário",
"recreateDBIndexes": "Recriar índices do BD",
"recreateDBIndexesToast": "Índices do BD foram recriados",
"recreateDBIndexesDialogTitle": "Todos os índices do BD serão recriados.\nIsso pode demorar um pouco.",
"recreateDBIndexesToast": "Índices do BD foi recriado",
"recreateDBIndexesDialogTitle": "Todos os índices do BD vai ser Recriado.\nIsso pode demorar um pouco.",
"recreateDBIndexesDesc": "Recrie os índices do BD. Isso pode melhorar o desempenho em dispositivos mais lentos.",
"restartRequired": "Reinicie o LNReader para aplicar esta alteração."
"restartRequired": "Reinicie o LNReader para aplicar esta alteração.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Aparência",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparando os dados",
"preparingSelectedFiles": "Preparando arquivos selecionados",
@@ -189,7 +193,7 @@
},
"remoteBackup": "Backup remoto",
"restoreBackup": "Restaurar backup",
"restoreBackupDesc": "Restaure a biblioteca a partir de um arquivo de backup",
"restoreBackupDesc": "Restaure a biblioteca a partir de um arquivo de backup ",
"restorinBackup": "Restaurando cópia de segurança",
"restoringData": "Restaurando dados",
"restoringSelectedFiles": "Restaurando arquivos selecionados",
@@ -245,7 +249,7 @@
"plugins": "Plugins",
"removeFromLibrary": "Removido da biblioteca",
"repositories": "Repositories",
"searchbar": "Pesquisar fontes e plugins",
"searchbar": "Pesquisar nas fontes",
"searchResults": "Resultados da pesquisa",
"selectNovel": "Selecionar novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "Categoria padrão não pode ser deletada",
"default": "Padrão",
"defaultCategory": "Categoria padrão",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Você quer deletar esta categoria",
"header": "Deletar categoria"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "Você não possui categorias. Clique no botão de editar para criar uma para organizar sua biblioteca"
},
"repositories": {
"emptyMsg": "Você não tem fontes. Adicione sua primeira para iniciar."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "Você não tem fontes. Adicione sua primeira para iniciar.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "Sobre",
@@ -304,7 +313,7 @@
"deleted": "Deletado %{name}",
"deprecated": "Depreciado",
"display": "Exibir",
"done": "Concluído",
"done": "Depreciado",
"downloads": "Baixados",
"edit": "Editar",
"example": "Exemplo",
@@ -340,7 +349,12 @@
"submit": "Aplicar",
"loading": "Carregando...",
"moreOptions": "More options",
"warning": "Aviso"
"warning": "Aviso",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Rever",
@@ -374,10 +388,19 @@
"pluginNotFound": "Extensão não encontrada!",
"removeDownloadsWarning": "Tem certeza? Todos os capítulos transferidos serão deletados"
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "Geral",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "todas as fontes",
"searchIn": "Procurar uma novel em"
"searchIn": "Procurar uma novel em",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "Histórico",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrar",
"noSummary": "Nenhum resumo",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Rolar para o capítulo atual",
"scrollToTop": "Rolar para o topo"
},
"emptyChapterMessage": "<h2>Não foi possível carregar o capítulo</h2><p>Nenhum conteúdo legível foi encontrado. Verifique o capítulo no WebView; se ele carregar lá, <a href='%{reportUrl}'>relate o problema do plugin</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Capítulo: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finalizado",
"nextChapter": "Next: %{name}",
"noNextChapter": "Não há próximo capítulo",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+62 -12
View File
@@ -13,7 +13,7 @@
"cachedNovelsDeletedToast": "Кэшированные новеллы удалены",
"chapterInsertFailed": "не удалось вставить главу",
"clearCachedNovels": "Очистить кэш новелл",
"clearCachedNovelsDesc": "Удалить кэшированные новеллы, которые не находятся в вашей библиотеке",
"clearCachedNovelsDesc": "Удалить кэш новелл, которых нет в вашей библиотеке",
"clearDatabaseWarning": "Вы уверены? Прочитанные и загруженные главы и прогресс не библиотечных новелл будут потеряны.",
"clearUpdatesMessage": "Обновления очищены.",
"clearUpdatesTab": "Очистить вкладку обновления",
@@ -22,8 +22,9 @@
"dataManagement": "Управление данными",
"deleteReadChapters": "Удалять прочитанные главы",
"deleteReadChaptersDialogTitle": "Вы уверены? Все главы, помеченные как прочитанные, будут удалены.",
"diagnostics": "Диагностика",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttps": "DNS через HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
"importEpub": "Импорт EPUB",
"importFailed": "Error importing EPUB:",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Индексы базы данных пересозданы",
"recreateDBIndexesDialogTitle": "Все индексы базы данных будут пересозданы.\nЭто может занять некоторое время.",
"recreateDBIndexesDesc": "Пересоздаёт индексы базы данных. Это может улучшить производительность на слабых устройствах.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Внешний вид",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Подготовка данных",
"preparingSelectedFiles": "Preparing selected files",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "Вы не можете удалить категорию по умолчанию",
"default": "По умолчанию",
"defaultCategory": "Категория по умолчанию",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Вы хотите удалить эту категорию",
"header": "Удалить категорию"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "У вас нет категорий. Нажмите кнопку Изменить, чтобы создать ее для организации вашей библиотеки"
},
"repositories": {
"emptyMsg": "У вас нет репозиториев. Добавьте ваш первый репозиторий плагинов, чтобы начать работу."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "У вас нет репозиториев. Добавьте ваш первый репозиторий плагинов, чтобы начать работу.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "О программе",
@@ -340,7 +349,12 @@
"submit": "Продолжить",
"loading": "Загрузка...",
"moreOptions": "More options",
"warning": "Внимание"
"warning": "Внимание",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Обновить",
@@ -374,10 +388,19 @@
"pluginNotFound": "Плагин не найден!",
"removeDownloadsWarning": "Вы уверены? Все загруженные главы будут удалены."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "Основные",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "все источники",
"searchIn": "Поиск новеллы в"
"searchIn": "Поиск новеллы в",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "История",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Перенос",
"noSummary": "Нет сводки",
"noCoverFound": "Обложка не найдена",
"notFound": "Novel not found on the source",
"progress": "Прогресс %{progress}%",
"readChaptersDeleted": "Прочитанные главы удалены",
"startReadingChapters": "Начать %{name}",
@@ -662,9 +690,9 @@
"scrollToCurrentChapter": "Прокрутка к текущей главе",
"scrollToTop": "Прокрутить вверх"
},
"emptyChapterMessage": "<h2>Не удалось загрузить главу</h2><p>Читаемый контент не найден. Проверьте главу в WebView; если она там загружается, <a href='%{reportUrl}'>сообщите о проблеме плагина</a>.</p><p>Плагин: %{pluginId}<br>Новелла: %{novelName}<br>Глава: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Завершено",
"nextChapter": "Следующая глава: %{name}",
"nextChapter": "%{name}Следующий: %{name}",
"noNextChapter": "Следующей главы нет",
"noPreviousChapter": "Предыдущей главы нет",
"searchMinLength": "Enter at least %{count} characters to search",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
+61 -11
View File
@@ -22,6 +22,7 @@
"dataManagement": "Data Management",
"deleteReadChapters": "Delete read chapters",
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
"diagnostics": "Diagnostics",
"disabled": "Disabled",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
@@ -37,7 +38,10 @@
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"restartRequired": "Restart LNReader to apply this change."
"restartRequired": "Restart LNReader to apply this change.",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
"shareCrashLogsFailed": "Failed to create crash logs."
},
"appearance": "Appearance",
"appearanceScreen": {
@@ -172,13 +176,13 @@
"description": "Choose what to include in this backup.",
"selectAll": "Select all",
"library": "Library data",
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
"settings": "Settings",
"settingsDescription": "App, reader, and update preferences.",
"plugins": "Plugins",
"pluginsDescription": "Installed source plugin files.",
"downloadedFiles": "Downloaded chapters and covers",
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
"downloadedFiles": "Downloaded chapters",
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
},
"preparingData": "Preparing Data",
"preparingSelectedFiles": "Preparing selected files",
@@ -238,14 +242,14 @@
"openPluginSettings": "Open settings for %{name}",
"openWebsite": "Open the website for %{name}",
"pinSource": "Pin %{name}",
"pinnedPlugins": "Pinned",
"pinnedPlugins": "Pinned plugins",
"pluginDetails": "Plugin details",
"pluginIcon": "plugin icon",
"pluginNotInstalled": "This plugin is not installed",
"plugins": "Plugins",
"removeFromLibrary": "Removed from library",
"repositories": "Repositories",
"searchbar": "Search sources and plugins",
"searchbar": "Search sources",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"sources": "Sources",
@@ -274,6 +278,7 @@
"cantDeleteDefault": "You can't delete the default category",
"default": "Default",
"defaultCategory": "Default category",
"alwaysAsk": "Always ask",
"deleteModal": {
"desc": "Do you wish to delete category",
"header": "Delete category"
@@ -287,7 +292,11 @@
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"disable": "Disable",
"disableTitle": "Disable repository?",
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
"toggle": "Toggle %{name} repository"
},
"common": {
"about": "About",
@@ -340,7 +349,12 @@
"submit": "Submit",
"loading": "Loading ...",
"moreOptions": "More options",
"warning": "Warning"
"warning": "Warning",
"code": "Code",
"custom_code": "Custom Code",
"textToReplace": "Text to replace",
"replaceWith": "Replace with",
"replaceText": "Replace text"
},
"webview": {
"refresh": "Refresh",
@@ -374,10 +388,19 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"taskQueue": {
"cancelTaskAction": "Cancel task",
"cancelTaskTitle": "Cancel task?",
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
"keepTaskAction": "Keep task"
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"shareCrashLogs": "Share crash logs",
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
"restart": "Restart the application"
},
"generalSettings": "General",
@@ -441,7 +464,10 @@
},
"globalSearch": {
"allSources": "all sources",
"searchIn": "Search a novel in"
"searchIn": "Search a novel in",
"openNovel": "Open novel",
"openCopiedNovel": "Open copied novel",
"noSourceForUrl": "No source found for this URL"
},
"history": "History",
"historyScreen": {
@@ -591,9 +617,11 @@
"searchByName": "Search by Chapter Name",
"searchByNameDescription": "Turn off to search by chapter number."
},
"loadFailed": "Unable to load novel",
"migrate": "Migrate",
"noSummary": "No summary",
"noCoverFound": "No cover found",
"notFound": "Novel not found on the source",
"progress": "Progress %{progress} %",
"readChaptersDeleted": "Read chapters deleted",
"startReadingChapters": "Start reading %{name}",
@@ -662,7 +690,7 @@
"scrollToCurrentChapter": "Scroll to current chapter",
"scrollToTop": "Scroll to top"
},
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
"finished": "Finished",
"nextChapter": "Next: %{name}",
"noNextChapter": "There's no next chapter",
@@ -780,6 +808,28 @@
"other": "%{count} days"
}
},
"customCodeSettings": {
"textManipulation": "Text Manipulation",
"codeSnippets": "Code Snippets",
"cssSnippets": "CSS Snippets",
"javascriptSnippets": "JavaScript Snippets",
"createCSSSnippet": "Create CSS Snippet",
"addCssCode": "Add custom CSS code that applies to all chapters",
"createJSSnippet": "Create JS Snippet",
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
"noCodeSnippets": "No code snippets",
"renameSnippet": "Rename Snippet",
"yourCodeHere": "Your code here",
"editReplace": "Edit Replace/Remove",
"enterAMatch": "Enter a match pattern",
"enterAReplace": "Enter a replace value",
"itemAlreadyExists": "Item already exists",
"replace": "Replace",
"removeText": "Remove Text",
"snippetSaved": "Snippet saved",
"snippetUpdated": "Snippet updated",
"imported": "Imported"
},
"genreStats": {
"title": "Genre statistics",
"totalNovels": {
File diff suppressed because it is too large Load Diff

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