Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0c20e209c | |||
| 648477931b | |||
| e42e8f66f2 | |||
| de5358727a | |||
| 706459783f | |||
| 1ca9a679cf | |||
| 920f88aee4 | |||
| cef3ae5bdf | |||
| 39c788e576 | |||
| d40713ac99 | |||
| f53bed7190 | |||
| 0b9c415681 | |||
| 38003c59e0 | |||
| f1119d8c30 | |||
| a5e2b4d4f0 | |||
| a12cba030d | |||
| 56d1140aad | |||
| 0379141304 | |||
| b6e9f7157d | |||
| edb3df719f | |||
| 9bde3df74d | |||
| 2426e6fe3e | |||
| 93209abebd | |||
| 158786ccd5 | |||
| 77be5624f2 | |||
| 4b580c90ef |
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -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,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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+2
@@ -29,6 +29,8 @@ export interface Reader {
|
||||
finished: string;
|
||||
nextChapter: string;
|
||||
noNextChapter: string;
|
||||
removeText: string;
|
||||
replaceText: string;
|
||||
};
|
||||
|
||||
setAdjacentChapters: (adjacent: {
|
||||
|
||||
@@ -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,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
@@ -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 = {
|
||||
|
||||
+68
-9
@@ -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)
|
||||
|
||||
+12
-1
@@ -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(
|
||||
|
||||
+102
-41
@@ -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)
|
||||
|
||||
+42
-25
@@ -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
|
||||
|
||||
|
||||
+20
-10
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
+57
@@ -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())
|
||||
}
|
||||
}
|
||||
+34
@@ -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
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import androidx.core.content.FileProvider
|
||||
|
||||
class CrashLogFileProvider : FileProvider(R.xml.crash_log_paths)
|
||||
+15
@@ -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]")
|
||||
}
|
||||
+48
@@ -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)
|
||||
}
|
||||
+73
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+163
@@ -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>
|
||||
+26
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import NativeCrashLogs from './src/NativeCrashLogsModule';
|
||||
|
||||
export default NativeCrashLogs;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+37
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+3
-3
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) = ''
|
||||
`;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
@@ -376,12 +379,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,
|
||||
};
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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}",
|
||||
@@ -713,9 +736,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 +817,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 +845,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 +874,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 +897,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Veri Yönetimi",
|
||||
"deleteReadChapters": "Okunmuş bölümleri sil",
|
||||
"deleteReadChaptersDialogTitle": "Okunmuş olarak işaretlenmiş bütün bölümler silinecek.",
|
||||
"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": "Veritabanı indeksleri yenilendi",
|
||||
"recreateDBIndexesDialogTitle": "Tüm veritabanı indeksleri yenilenecek. Bu biraz vakit alabilir.",
|
||||
"recreateDBIndexesDesc": "Veritabanı indekslerini yeniler. Yavaş cihazlarda hız artışı sağlayabilir.",
|
||||
"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": "Görünüş",
|
||||
"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": "Veri hazırlanıyor",
|
||||
"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,20 +278,25 @@
|
||||
"cantDeleteDefault": "Varsayılan kategorisi silinemez",
|
||||
"default": "Varsayılan",
|
||||
"defaultCategory": "Varsayılan kategori",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Şu kategoriyi silmek istiyor musunuz:",
|
||||
"header": "Kategoriyi sil"
|
||||
},
|
||||
"duplicateError": "Bu ada sahip bir kategori zaten var!",
|
||||
"editCategories": "Kategoriyi yeniden adlandır",
|
||||
"emptyMsg": "Kategoriniz bulunmuyor. Kitaplığınızı düzenlemek için bir tane oluşturmak için artı butonuna basın",
|
||||
"emptyMsg": "Kategoriniz bulunmuyor. Kitaplığınızı düzenlemek için bir tane oluşturmak için artı butonuna basın",
|
||||
"header": "Kategorileri düzenle",
|
||||
"local": "Yerel",
|
||||
"setCategories": "Kategorileri ayarla",
|
||||
"setModalEmptyMsg": "Hiç kategoriniz yok. Kitaplığınızı düzenlemek üzere bir tane oluşturmak için Düzenle düğmesine dokunun."
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "Uzantı deponuz yok. Başlamak için ilk uzantı deponuzu ekleyin."
|
||||
"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": "Uzantı deponuz yok. Başlamak için ilk uzantı deponuzu ekleyin.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "Hakkında",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Gönder",
|
||||
"loading": "Yükleniyor ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Dikkat"
|
||||
"warning": "Dikkat",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Yenile",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Uzantı bulunamadı!",
|
||||
"removeDownloadsWarning": "Emin misiniz? İndirilen bütün bölümler silinecek."
|
||||
},
|
||||
"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": "Genel",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "tüm kaynaklarda",
|
||||
"searchIn": "Şurada bir novel arayın"
|
||||
"searchIn": "Şurada bir novel arayın",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Geçmiş",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Taşın",
|
||||
"noSummary": "Özet bilgisi yok",
|
||||
"noCoverFound": "Kapak bulunamadı",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "İlerleme %{progress}%",
|
||||
"readChaptersDeleted": "Okunmuş bölümler silindi",
|
||||
"startReadingChapters": "%{name} okumaya başla",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Mevcut bölüme kaydır",
|
||||
"scrollToTop": "Yukarı kaydır"
|
||||
},
|
||||
"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": "Biten",
|
||||
"nextChapter": "Sonraki: %{name}",
|
||||
"noNextChapter": "Başka bölüm yok",
|
||||
@@ -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": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Керування даними",
|
||||
"deleteReadChapters": "Видалити прочитані розділи",
|
||||
"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": "Підготовка даних",
|
||||
"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": "Не можна видалити категорію за замовчуванням",
|
||||
"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": "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": "Оновити",
|
||||
@@ -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} %",
|
||||
"readChaptersDeleted": "Прочитані розділи видалено",
|
||||
"startReadingChapters": "Почати читання %{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": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Quản lý dữ liệu",
|
||||
"deleteReadChapters": "Xóa chương đã đọc",
|
||||
"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": "Giao diện",
|
||||
"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": "Chuẩn bị dữ liệu",
|
||||
"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": "Bạn không thể xoá danh mục mặc định",
|
||||
"default": "Mặc định",
|
||||
"defaultCategory": "Danh mục mặc định",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Bạn có muốn xóa danh mục này",
|
||||
"header": "Xóa danh mục"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "Bạn không có danh mục nào. Hãy ấn vào nút chỉnh sửa để tạo danh mục mới"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "Bạn không có nguồn lưu trữ. Thêm nguồn lưu trữ để bắt đầu."
|
||||
"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": "Bạn không có nguồn lưu trữ. Thêm nguồn lưu trữ để bắt đầu.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "Thông tin",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Gửi",
|
||||
"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": "Làm mới",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Không tìm thấy nguồn!",
|
||||
"removeDownloadsWarning": "Bạn chắc chứ? Tất cả chương đã tải xuống sẽ bị xoá."
|
||||
},
|
||||
"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": "Chung",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "toàn bộ nguồn",
|
||||
"searchIn": "Tìm truyện ở"
|
||||
"searchIn": "Tìm truyện ở",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Lịch sử",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Đồng bộ",
|
||||
"noSummary": "Không có tóm tắt",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Tiến trình %{progress} %",
|
||||
"readChaptersDeleted": "Đã xoá chương đã đọc",
|
||||
"startReadingChapters": "Bắt đầu đọc %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Chương hiện tại",
|
||||
"scrollToTop": "Lên đầu"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Không thể tải chương</h2><p>Không tìm thấy nội dung có thể đọc. Hãy kiểm tra chương trong WebView; nếu chương tải được ở đó, <a href='%{reportUrl}'>hãy báo cáo sự cố của plugin</a>.</p><p>Plugin: %{pluginId}<br>Truyện: %{novelName}<br>Chương: %{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": "Đã xong",
|
||||
"nextChapter": "Tiếp: %{name}",
|
||||
"noNextChapter": "Không có chương kế tiếp",
|
||||
@@ -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": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user