diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index f74134041..428ae6ebf 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -196,6 +196,7 @@ export default { requestBatchQuickEmbed() { const payload = { message: this.$strings.MessageConfirmQuickEmbed, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 4bf8cfbbf..a8e8ae469 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -300,6 +300,8 @@ export default { }) }, userUpdated(user) { + if (user.id !== this.$store.state.user.user.id) return + if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) { this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening) } diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index 62a9b8037..4834a1a25 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -338,6 +338,18 @@ export default { const series = this.series.find((se) => se.id == decoded) if (series) filterValue = series.name } + } else if (parts[0] === 'progress') { + const item = this.progress.find((p) => p.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'tracks') { + const item = this.tracks.find((t) => t.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'ebooks') { + const item = this.ebooks.find((e) => e.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'missing') { + const item = this.missing.find((m) => m.id == decoded) + if (item) filterValue = item.name } else { filterValue = decoded } diff --git a/client/components/modals/collections/AddCreateModal.vue b/client/components/modals/collections/AddCreateModal.vue index 24e8695d6..f0d43c14f 100644 --- a/client/components/modals/collections/AddCreateModal.vue +++ b/client/components/modals/collections/AddCreateModal.vue @@ -227,7 +227,7 @@ export default { .catch((error) => { console.error('Failed to create collection', error) var errMsg = error.response ? error.response.data || '' : '' - this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg) + this.$toast.error(errMsg) this.processing = false }) } diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 4b92f6cd8..a1fc41091 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -78,7 +78,7 @@

- {{ $strings.LabelCurrently }} {{ isPodcast ? mediaMetadata.author : mediaMetadata.authorName }} + {{ $strings.LabelCurrently }} {{ isPodcast ? mediaMetadata.author : mediaMetadata.authorName }}

@@ -87,7 +87,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName }}

@@ -96,7 +96,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}

@@ -105,7 +105,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.publisher }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.publisher }}

@@ -114,7 +114,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear }}

diff --git a/client/components/modals/item/tabs/Schedule.vue b/client/components/modals/item/tabs/Schedule.vue index 2cd3af514..0faa29630 100644 --- a/client/components/modals/item/tabs/Schedule.vue +++ b/client/components/modals/item/tabs/Schedule.vue @@ -158,6 +158,8 @@ export default { this.isProcessing = true var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatePayload).catch((error) => { console.error('Failed to update', error) + const errorMessage = typeof error?.response?.data === 'string' ? error?.response?.data : null + this.$toast.error(errorMessage || this.$strings.ToastFailedToUpdate) return false }) this.isProcessing = false diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index d96550887..8e40ca3c0 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -107,6 +107,7 @@ export default { quickEmbed() { const payload = { message: this.$strings.MessageConfirmQuickEmbed, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/components/player/PlayerPlaybackControls.vue b/client/components/player/PlayerPlaybackControls.vue index 368e3f35b..c7cbba63c 100644 --- a/client/components/player/PlayerPlaybackControls.vue +++ b/client/components/player/PlayerPlaybackControls.vue @@ -8,7 +8,7 @@ - diff --git a/client/components/prompt/Confirm.vue b/client/components/prompt/Confirm.vue index 361765e2d..d0828c7d1 100644 --- a/client/components/prompt/Confirm.vue +++ b/client/components/prompt/Confirm.vue @@ -3,7 +3,8 @@
-

+

+

{{ message }}

@@ -52,6 +53,17 @@ export default { message() { return this.confirmPromptOptions.message || '' }, + allowHtmlMessage() { + return !!this.confirmPromptOptions.allowHtml + }, + sanitizedMessage() { + if (!this.allowHtmlMessage) return this.message + + return this.escapeHtml(this.message) + .replace(/<br\s*\/?>/gi, '
') + .replace(/<code>/gi, '') + .replace(/<\/code>/gi, '') + }, callback() { return this.confirmPromptOptions.callback }, @@ -103,6 +115,14 @@ export default { if (this.callback) this.callback(true, this.checkboxValue) this.show = false }, + escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + }, setShow() { this.checkboxValue = this.checkboxDefaultValue this.$eventBus.$emit('showing-prompt', true) diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index dd0eaa738..cd3bc6d2d 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -1,6 +1,6 @@ @@ -26,7 +26,8 @@ export default { disabled: Boolean, inputClass: String, showCopy: Boolean, - trimWhitespace: Boolean + trimWhitespace: Boolean, + autocomplete: String }, data() { return {} diff --git a/client/package-lock.json b/client/package-lock.json index 1e2d52c11..79ac53250 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.34.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 0eaffb106..dd0f3a0c9 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.34.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index b8cf3cff2..7fb84d468 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -390,8 +390,8 @@ export default { }, purgeItemsCache() { const payload = { - // message: `This will delete the entire folder at /metadata/cache/items.
Are you sure you want to purge items cache?`, message: this.$strings.MessageConfirmPurgeItemsCache, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.sendPurgeItemsCache() diff --git a/client/pages/config/item-metadata-utils/genres.vue b/client/pages/config/item-metadata-utils/genres.vue index 87e14bbd8..8abe34eef 100644 --- a/client/pages/config/item-metadata-utils/genres.vue +++ b/client/pages/config/item-metadata-utils/genres.vue @@ -90,9 +90,9 @@ export default { let message = this.$getString('MessageConfirmRenameGenre', [this.editingGenre, this.newGenreName]) if (genreNameExists) { - message += `
${this.$strings.MessageConfirmRenameGenreMergeNote}` + message += ` ${this.$strings.MessageConfirmRenameGenreMergeNote}` } else if (genreNameExistsOfDifferentCase) { - message += `
${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}` + message += ` ${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}` } const payload = { diff --git a/client/pages/config/item-metadata-utils/tags.vue b/client/pages/config/item-metadata-utils/tags.vue index 2fe5c0f1a..4dfc39424 100644 --- a/client/pages/config/item-metadata-utils/tags.vue +++ b/client/pages/config/item-metadata-utils/tags.vue @@ -86,9 +86,9 @@ export default { let message = this.$getString('MessageConfirmRenameTag', [this.editingTag, this.newTagName]) if (tagNameExists) { - message += `
${this.$strings.MessageConfirmRenameTagMergeNote}` + message += ` ${this.$strings.MessageConfirmRenameTagMergeNote}` } else if (tagNameExistsOfDifferentCase) { - message += `
${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}` + message += ` ${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}` } const payload = { diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index 86ec7eec6..4341ea07d 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -66,7 +66,11 @@

{{ getPlayMethodName(session.playMethod) }}

-

+

+ +

{{ $elapsedPrettyLocalized(session.timeListening) }}

@@ -130,7 +134,11 @@

{{ getPlayMethodName(session.playMethod) }}

-

+

+ +

{{ $elapsedPretty(session.timeListening) }}

@@ -172,7 +180,11 @@

{{ getPlayMethodName(session.playMethod) }}

-

+

+ +

{{ $secondsToTimestamp(session.currentTime) }}

@@ -433,16 +445,16 @@ export default { this.selectedSession = session this.showSessionModal = true }, - getDeviceInfoString(deviceInfo) { - if (!deviceInfo) return '' - var lines = [] + getDeviceInfoLines(deviceInfo) { + if (!deviceInfo) return [] + const lines = [] if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.browserName) lines.push(deviceInfo.browserName) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) - return lines.join('
') + return lines }, getPlayMethodName(playMethod) { if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue index 060f34be8..cb18f5ee3 100644 --- a/client/pages/config/users/_id/sessions.vue +++ b/client/pages/config/users/_id/sessions.vue @@ -38,8 +38,12 @@

{{ getPlayMethodName(session.playMethod) }}

-

- +

+ +

+

{{ $elapsedPrettyLocalized(session.timeListening) }}

@@ -193,16 +197,16 @@ export default { this.selectedSession = session this.showSessionModal = true }, - getDeviceInfoString(deviceInfo) { - if (!deviceInfo) return '' - var lines = [] + getDeviceInfoLines(deviceInfo) { + if (!deviceInfo) return [] + const lines = [] if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`) if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) if (deviceInfo.browserName) lines.push(deviceInfo.browserName) if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) - return lines.join('
') + return lines }, getPlayMethodName(playMethod) { if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' diff --git a/client/pages/login.vue b/client/pages/login.vue index 8e5cde098..cec150e71 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -17,9 +17,9 @@

Create Root User

- - - + + +

Directory Paths

@@ -51,10 +51,10 @@ - + - +
{{ processing ? 'Checking...' : $strings.ButtonSubmit }}
diff --git a/client/pages/share/_slug.vue b/client/pages/share/_slug.vue index 64c099632..fdcbbe500 100644 --- a/client/pages/share/_slug.vue +++ b/client/pages/share/_slug.vue @@ -364,6 +364,7 @@ export default { } const startTime = this.playbackSession.currentTime || 0 + this.localAudioPlayer.set(null, this.audioTracks, false, startTime, false) this.localAudioPlayer.on('stateChange', this.playerStateChange.bind(this)) this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this)) diff --git a/client/players/LocalAudioPlayer.js b/client/players/LocalAudioPlayer.js index 7fc17e7aa..a0384d54d 100644 --- a/client/players/LocalAudioPlayer.js +++ b/client/players/LocalAudioPlayer.js @@ -46,7 +46,20 @@ export default class LocalAudioPlayer extends EventEmitter { this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this)) this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this)) - var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm'] + var mimeTypes = [ + 'audio/flac', + 'audio/mpeg', + 'audio/mp4', + 'audio/ogg', + 'audio/aac', + 'audio/x-ms-wma', + 'audio/x-aiff', + 'audio/webm', + // `audio/matroska` is the correct mimetype, but the server still uses `audio/x-matroska` + // ref: https://www.iana.org/assignments/media-types/media-types.xhtml + 'audio/matroska', + 'audio/x-matroska' + ] var mimeTypeCanPlayMap = {} mimeTypes.forEach((mt) => { var canPlay = this.player.canPlayType(mt) diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 83e748f25..a56fbd267 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -6,6 +6,7 @@ const defaultCode = 'en-us' const languageCodeMap = { ar: { label: 'عربي', dateFnsLocale: 'ar' }, + be: { label: 'Беларуская', dateFnsLocale: 'be' }, bg: { label: 'Български', dateFnsLocale: 'bg' }, bn: { label: 'বাংলা', dateFnsLocale: 'bn' }, ca: { label: 'Català', dateFnsLocale: 'ca' }, @@ -20,6 +21,7 @@ const languageCodeMap = { he: { label: 'עברית', dateFnsLocale: 'he' }, hr: { label: 'Hrvatski', dateFnsLocale: 'hr' }, it: { label: 'Italiano', dateFnsLocale: 'it' }, + ja: { label: '日本語', dateFnsLocale: 'ja' }, lt: { label: 'Lietuvių', dateFnsLocale: 'lt' }, hu: { label: 'Magyar', dateFnsLocale: 'hu' }, ko: { label: '한국어', dateFnsLocale: 'ko' }, @@ -28,6 +30,7 @@ const languageCodeMap = { pl: { label: 'Polski', dateFnsLocale: 'pl' }, 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, ru: { label: 'Русский', dateFnsLocale: 'ru' }, + sk: { label: 'Slovenčina', dateFnsLocale: 'sk' }, sl: { label: 'Slovenščina', dateFnsLocale: 'sl' }, sv: { label: 'Svenska', dateFnsLocale: 'sv' }, tr: { label: 'Türkçe', dateFnsLocale: 'tr' }, @@ -48,6 +51,7 @@ const podcastSearchRegionMap = { au: { label: 'Australia' }, br: { label: 'Brasil' }, be: { label: 'België / Belgique / Belgien' }, + by: { label: 'Беларусь' }, cz: { label: 'Česko' }, dk: { label: 'Danmark' }, de: { label: 'Deutschland' }, @@ -57,6 +61,7 @@ const podcastSearchRegionMap = { hr: { label: 'Hrvatska' }, il: { label: 'ישראל / إسرائيل' }, it: { label: 'Italia' }, + jp: { label: '日本' }, lu: { label: 'Luxembourg / Luxemburg / Lëtezebuerg' }, hu: { label: 'Magyarország' }, nl: { label: 'Nederland' }, @@ -67,6 +72,7 @@ const podcastSearchRegionMap = { pt: { label: 'Portugal' }, ru: { label: 'Россия' }, ch: { label: 'Schweiz / Suisse / Svizzera' }, + sk: { label: 'Slovensko' }, se: { label: 'Sverige' }, vn: { label: 'Việt Nam' }, ua: { label: 'Україна' }, diff --git a/client/strings/be.json b/client/strings/be.json index f812a13c9..b4e4df86a 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -1,24 +1,24 @@ { "ButtonAdd": "Дадаць", - "ButtonAddApiKey": "Дадаць API-ключ", + "ButtonAddApiKey": "Дадаць ключ API", "ButtonAddChapters": "Дадаць раздзелы", "ButtonAddDevice": "Дадаць прыладу", "ButtonAddLibrary": "Дадаць бібліятэку", "ButtonAddPodcasts": "Дадаць падкасты", "ButtonAddUser": "Дадаць карыстальніка", "ButtonAddYourFirstLibrary": "Дадайце сваю першую бібліятэку", - "ButtonApply": "Ужыць", - "ButtonApplyChapters": "Ужыць раздзелы", + "ButtonApply": "Прымяніць", + "ButtonApplyChapters": "Прымяніць раздзелы", "ButtonAuthors": "Аўтары", "ButtonBack": "Назад", "ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага", "ButtonBatchEditPopulateMapDetails": "Запоўніць падрабязнасці карты", - "ButtonBrowseForFolder": "Знайсці тэчку", + "ButtonBrowseForFolder": "Агляд папак", "ButtonCancel": "Скасаваць", "ButtonCancelEncode": "Скасаваць кадзіраванне", - "ButtonChangeRootPassword": "Зменіце Root пароль", - "ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды", - "ButtonChooseAFolder": "Выбраць тэчку", + "ButtonChangeRootPassword": "Змяніць пароль root", + "ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя выпускі", + "ButtonChooseAFolder": "Выбраць папку", "ButtonChooseFiles": "Выбраць файлы", "ButtonClearFilter": "Ачысціць фільтр", "ButtonClose": "Закрыць", @@ -26,7 +26,7 @@ "ButtonCloseSession": "Закрыць адкрыты сеанс", "ButtonCollections": "Калекцыі", "ButtonConfigureScanner": "Наладзіць сканер", - "ButtonCreate": "Ствараць", + "ButtonCreate": "Стварыць", "ButtonCreateBackup": "Стварыць рэзервовую копію", "ButtonDelete": "Выдаліць", "ButtonDownloadQueue": "Чарга", @@ -36,7 +36,7 @@ "ButtonEnable": "Уключыць", "ButtonFireAndFail": "Агонь і няўдача", "ButtonFireOnTest": "Тэст на вогнеўстойлівасць", - "ButtonForceReScan": "Прымусовае паўторнае сканаванне", + "ButtonForceReScan": "Прымусова паўторна сканіраваць", "ButtonFullPath": "Поўны шлях", "ButtonHide": "Схаваць", "ButtonHome": "Галоўная", @@ -47,22 +47,22 @@ "ButtonLibrary": "Бібліятэка", "ButtonLogout": "Выйсці", "ButtonLookup": "Пошук", - "ButtonManageTracks": "Кіраванне дарожкамі", - "ButtonMapChapterTitles": "Супаставіць назвы раздзелаў", + "ButtonManageTracks": "Кіраванне трэкамі", + "ButtonMapChapterTitles": "Супаставіць загалоўкі раздзелаў", "ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў", - "ButtonMatchBooks": "Падбор кніг", + "ButtonMatchBooks": "Параўнаць кнігі", "ButtonNevermind": "Няважна", "ButtonNext": "Далей", "ButtonNextChapter": "Наступны раздзел", "ButtonNextItemInQueue": "Наступны элемент у чарзе", - "ButtonOk": "Добра", + "ButtonOk": "ОК", "ButtonOpenFeed": "Адкрыць стужку", "ButtonOpenManager": "Адкрыць менеджар", - "ButtonPause": "Паўза", + "ButtonPause": "Прыпыніць", "ButtonPlay": "Прайграць", "ButtonPlayAll": "Прайграць усё", "ButtonPlaying": "Прайграваецца", - "ButtonPlaylists": "Спісы прайгравання", + "ButtonPlaylists": "Плэй-лісты", "ButtonPrevious": "Папярэдні", "ButtonPreviousChapter": "Папярэдні раздзел", "ButtonProbeAudioFile": "Праверыць аўдыяфайл", @@ -71,9 +71,9 @@ "ButtonQueueAddItem": "Дадаць у чаргу", "ButtonQueueRemoveItem": "Выдаліць з чаргі", "ButtonQuickEmbed": "Хуткае ўбудаванне", - "ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метададзеных", + "ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метаданых", "ButtonQuickMatch": "Хуткі пошук", - "ButtonReScan": "Паўторнае сканаванне", + "ButtonReScan": "Паўторна сканіраваць", "ButtonRead": "Чытаць", "ButtonReadLess": "Чытаць менш", "ButtonReadMore": "Чытаць больш", @@ -81,11 +81,11 @@ "ButtonRemove": "Выдаліць", "ButtonRemoveAll": "Выдаліць усе", "ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі", - "ButtonRemoveFromContinueListening": "Выдаліць з Працягваць слухаць", + "ButtonRemoveFromContinueListening": "Выдаліць з Працяг праслухоўвання", "ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне", "ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю", "ButtonReset": "Скінуць", - "ButtonResetToDefault": "Скінуць па змаўчанні", + "ButtonResetToDefault": "Скінуць да прадвызначаных", "ButtonRestore": "Аднавіць", "ButtonSave": "Захаваць", "ButtonSaveAndClose": "Захаваць і зачыніць", @@ -95,62 +95,63 @@ "ButtonScrollLeft": "Пракруціць улева", "ButtonScrollRight": "Пракруціць направа", "ButtonSearch": "Пошук", - "ButtonSelectFolderPath": "Выбраць шлях да тэчкі", + "ButtonSelectFolderPath": "Выбраць шлях да папкі", "ButtonSeries": "Серыі", - "ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў", + "ButtonSetChaptersFromTracks": "Задаць раздзелы з трэкаў", "ButtonShare": "Падзяліцца", "ButtonShiftTimes": "Карэкцыя часу", "ButtonShow": "Паказаць", "ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B", - "ButtonStartMetadataEmbed": "Пачаць убудаванне метададзеных", + "ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых", "ButtonStats": "Статыстыка", "ButtonSubmit": "Адправіць", "ButtonTest": "Тэст", "ButtonUnlinkOpenId": "Адвязаць OpenID", - "ButtonUpload": "Загрузіць", - "ButtonUploadBackup": "Загрузіць рэзервовую копію", - "ButtonUploadCover": "Загрузіць вокладку", - "ButtonUploadOPMLFile": "Загрузіць файл OPML", + "ButtonUpload": "Запампаваць", + "ButtonUploadBackup": "Запампаваць рэзервовую копію", + "ButtonUploadCover": "Запампаваць вокладку", + "ButtonUploadOPMLFile": "Запампаваць файл OPML", "ButtonUserDelete": "Выдаліць карыстальніка {0}", "ButtonUserEdit": "Рэдагаваць карыстальніка {0}", "ButtonViewAll": "Прагледзець усе", "ButtonYes": "Так", - "ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метададзеных", - "ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара", - "ErrorUploadLacksTitle": "Павінна быць назва", + "ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метаданых", + "ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метаданыя – паспрабуйце абнавіць загаловак і/або аўтара", + "ErrorUploadLacksTitle": "Павінен быць загаловак", "HeaderAccount": "Уліковы запіс", - "HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метададзенных", + "HeaderAddCustomMetadataProvider": "Дадаванне карыстальніцкага пастаўшчыка метаданых", "HeaderAdvanced": "Дадаткова", - "HeaderApiKeys": "API-ключы", + "HeaderApiKeys": "Ключы API", "HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise", - "HeaderAudioTracks": "Аўдыядарожкі", + "HeaderAudioTracks": "Аўдыятрэкі", "HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг", "HeaderAuthentication": "Аўтэнтыфікацыя", "HeaderBackups": "Рэзервовыя копіі", + "HeaderBulkChapterModal": "Дадаць некалькі раздзелаў", "HeaderChangePassword": "Змяніць пароль", "HeaderChapters": "Раздзелы", - "HeaderChooseAFolder": "Выбраць тэчку", + "HeaderChooseAFolder": "Выберыце папку", "HeaderCollection": "Калекцыя", "HeaderCollectionItems": "Элементы калекцыі", "HeaderCover": "Вокладка", - "HeaderCurrentDownloads": "Бягучыя спампоўкі", + "HeaderCurrentDownloads": "Бягучыя спампоўванні", "HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе", - "HeaderCustomMetadataProviders": "Карыстальніцкія крыніцы метададзеных", + "HeaderCustomMetadataProviders": "Карыстальніцкія пастаўшчыкі метаданых", "HeaderDetails": "Падрабязнасці", "HeaderDownloadQueue": "Чарга спамповак", "HeaderEbookFiles": "Файлы электронных кніг", "HeaderEmail": "Электронная пошта", "HeaderEmailSettings": "Налады электроннай пошты", - "HeaderEpisodes": "Эпізоды", + "HeaderEpisodes": "Выпускі", "HeaderEreaderDevices": "Прылады для чытання", "HeaderEreaderSettings": "Налады прылады для чытання", "HeaderFiles": "Файлы", "HeaderFindChapters": "Знайсці раздзелы", "HeaderIgnoredFiles": "Ігнараваныя файлы", "HeaderItemFiles": "Файлы элементаў", - "HeaderItemMetadataUtils": "Утыліты для метададзеных элементаў", + "HeaderItemMetadataUtils": "Утыліты для метаданых", "HeaderLastListeningSession": "Апошні сеанс праслухоўвання", - "HeaderLatestEpisodes": "Апошнія эпізоды", + "HeaderLatestEpisodes": "Апошнія выпускі", "HeaderLibraries": "Бібліятэкі", "HeaderLibraryFiles": "Файлы бібліятэкі", "HeaderLibraryStats": "Статыстыка бібліятэкі", @@ -162,10 +163,10 @@ "HeaderManageTags": "Кіраванне тэгамі", "HeaderMapDetails": "Падрабязнасці адлюстравання", "HeaderMatch": "Супадзенне", - "HeaderMetadataOrderOfPrecedence": "Парадак прыярытэтнасці метададзеных", - "HeaderMetadataToEmbed": "Метададзеныя для ўбудавання", + "HeaderMetadataOrderOfPrecedence": "Парадак прыярытэту метаданых", + "HeaderMetadataToEmbed": "Метаданыя для ўбудавання", "HeaderNewAccount": "Новы ўліковы запіс", - "HeaderNewApiKey": "Новы API-ключ", + "HeaderNewApiKey": "Новы ключ API", "HeaderNewLibrary": "Новая бібліятэка", "HeaderNotificationCreate": "Стварыць апавяшчэнне", "HeaderNotificationUpdate": "Абнавіць апавяшчэнне", @@ -178,39 +179,40 @@ "HeaderPermissions": "Дазволы", "HeaderPlayerQueue": "Чарга прайгравання", "HeaderPlayerSettings": "Налады прайгравальніка", - "HeaderPlaylist": "Спіс прайгравання", - "HeaderPlaylistItems": "Элементы спіса прайгравання", + "HeaderPlaylist": "Плэй-ліст", + "HeaderPlaylistItems": "Элементы плэй-ліста", "HeaderPodcastsToAdd": "Падкасты для дадання", "HeaderPresets": "Прадустаноўкі", "HeaderPreviewCover": "Прадпрагляд вокладкі", "HeaderRSSFeedGeneral": "Падрабязнасці RSS", "HeaderRSSFeedIsOpen": "RSS-стужка адкрытая", "HeaderRSSFeeds": "RSS-стужкі", - "HeaderRemoveEpisode": "Выдаліць эпізод", - "HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў", + "HeaderRemoveEpisode": "Выдаліць выпуск", + "HeaderRemoveEpisodes": "Выдаліць {0} выпускаў", "HeaderSavedMediaProgress": "Захаваны прагрэс медыя", "HeaderSchedule": "Расклад", - "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў", + "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спампоўванняў выпускаў", "HeaderScheduleLibraryScans": "Расклад аўтаматычнага сканавання бібліятэкі", "HeaderSession": "Сеанс", "HeaderSetBackupSchedule": "Наладзіць расклад рэзервовага капіравання", "HeaderSettings": "Налады", - "HeaderSettingsDisplay": "Дысплей", + "HeaderSettingsDisplay": "Выгляд", "HeaderSettingsExperimental": "Эксперыментальныя функцыі", "HeaderSettingsGeneral": "Агульныя", "HeaderSettingsScanner": "Сканер", + "HeaderSettingsSecurity": "Бяспека", "HeaderSettingsWebClient": "Вэб-кліент", "HeaderSleepTimer": "Таймер сну", "HeaderStatsLargestItems": "Найбуйнейшыя элементы", "HeaderStatsLongestItems": "Найдаўжэйшыя элементы (гадзіны)", "HeaderStatsMinutesListeningChart": "Хвілін праслухоўвання (апошнія 7 дзён)", "HeaderStatsRecentSessions": "Апошнія сеансы", - "HeaderStatsTop10Authors": "10 лепшых аўтараў", - "HeaderStatsTop5Genres": "5 лепшых жанраў", + "HeaderStatsTop10Authors": "Топ 10 аўтараў", + "HeaderStatsTop5Genres": "Топ 5 жанраў", "HeaderTableOfContents": "Змест", "HeaderTools": "Інструменты", "HeaderUpdateAccount": "Абнавіць уліковы запіс", - "HeaderUpdateApiKey": "Абнавіць API-ключ", + "HeaderUpdateApiKey": "Абнавіць ключ API", "HeaderUpdateAuthor": "Абнавіць аўтара", "HeaderUpdateDetails": "Абнавіць падрабязнасці", "HeaderUpdateLibrary": "Абнавіць бібліятэку", @@ -229,40 +231,40 @@ "LabelActivity": "Дзеянне", "LabelAddToCollection": "Дадаць у калекцыю", "LabelAddToCollectionBatch": "Дадаць {0} кніг у калекцыю", - "LabelAddToPlaylist": "Дадаць у спіс прайгравання", - "LabelAddToPlaylistBatch": "Дадаць {0} элементаў у спіс прайгравання", + "LabelAddToPlaylist": "Дадаць у плэй-ліст", + "LabelAddToPlaylistBatch": "Дадаць {0} элементаў у плэй-ліст", "LabelAddedAt": "Дата дабаўлення", "LabelAddedDate": "Дададзена {0}", "LabelAdminUsersOnly": "Толькі для адміністратараў", "LabelAll": "Усе", - "LabelAllEpisodesDownloaded": "Усе эпізоды спампаваныя", + "LabelAllEpisodesDownloaded": "Усе выпускі спампаваныя", "LabelAllUsers": "Усе карыстальнікі", - "LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей", - "LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей", + "LabelAllUsersExcludingGuests": "Усіх карыстальнікаў, акрамя гасцей", + "LabelAllUsersIncludingGuests": "Усіх карыстальнікаў, уключаючы гасцей", "LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы", - "LabelApiKeyCreated": "API-ключ \"{0}\" паспяхова створаны.", - "LabelApiKeyCreatedDescription": "Пераканайцеся, што вы скапіявалі API-ключ зараз, бо паўторна яго ўбачыць не атрымаецца.", + "LabelApiKeyCreated": "Ключ API \"{0}\" паспяхова створаны.", + "LabelApiKeyCreatedDescription": "Абавязкова скапіюйце ключ API зараз, бо паўторна яго ўбачыць не атрымаецца.", "LabelApiKeyUser": "Дзейнічаць ад імя карыстальніка", - "LabelApiKeyUserDescription": "Гэты API-ключ будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.", + "LabelApiKeyUserDescription": "Гэты ключ API будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.", "LabelApiToken": "Токен API", "LabelAppend": "Дадаць", - "LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)", - "LabelAudioChannels": "Аўдыёканалы (1 або 2)", - "LabelAudioCodec": "Аўдыёкодэк", + "LabelAudioBitrate": "Бітрэйт аўдыя (напрыклад, 128к)", + "LabelAudioChannels": "Аўдыяканалы (1 або 2)", + "LabelAudioCodec": "Аўдыякодэк", "LabelAuthor": "Аўтар", - "LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)", - "LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)", + "LabelAuthorFirstLast": "Аўтар (імя, прозвішча)", + "LabelAuthorLastFirst": "Аўтар (прозвішча, імя)", "LabelAuthors": "Аўтары", - "LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", - "LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метададзеных", - "LabelAutoFetchMetadataHelp": "Атрыманне звестак пра назву, аўтара і серыю для падыходнага фарматавання перад загрузкай. Далей можа быць неабходна дапоўніць метададзеныя.", + "LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі", + "LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых", + "LabelAutoFetchMetadataHelp": "Атрыманне звестак пра загаловак, аўтара і серыю для спрашчэння запампоўвання. Пасля запампоўвання, магчыма, спатрэбіцца супаставіць дадатковыя метаданыя.", "LabelAutoLaunch": "Аўтазапуск", - "LabelAutoLaunchDescription": "Аўтаматычна перанакіроўваць да пастаўшчыка аўтэнтыфікацыі пры переходзе на старонку ўваходу (ручное пераключэнне праз шлях /login?autoLaunch=0)", + "LabelAutoLaunchDescription": "Аўтаматычна перанакіроўваць да пастаўшчыка аўтэнтыфікацыі пры пераходзе на старонку ўваходу (ручное пераключэнне праз шлях /login?autoLaunch=0)", "LabelAutoRegister": "Аўтарэгістрацыя", "LabelAutoRegisterDescription": "Аўтаматычна ствараць новых карыстальнікаў пасля ўваходу ў сістэму", "LabelBackToUser": "Вярнуцца да карыстальніка", - "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў", - "LabelBackupLocation": "Месцазнаходжанне рэзервовых копій", + "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыяфайлаў", + "LabelBackupLocation": "Размяшчэнне рэзервовых копій", "LabelBackupsEnableAutomaticBackups": "Аўтаматычнае рэзервовае капіраванне", "LabelBackupsEnableAutomaticBackupsHelp": "Рэзервовыя копіі захаваныя ў /metadata/backups", "LabelBackupsMaxBackupSize": "Максімальны памер рэзервовай копіі (у ГБ) (0 — неабмежавана)", @@ -273,11 +275,11 @@ "LabelBonus": "Бонус", "LabelBooks": "Кнігі", "LabelButtonText": "Тэкст кнопкі", - "LabelByAuthor": "ад {0}", + "LabelByAuthor": "– {0}", "LabelChangePassword": "Змяніць пароль", "LabelChannels": "Каналы", "LabelChapterCount": "{0} раздзелаў", - "LabelChapterTitle": "Назва раздзела", + "LabelChapterTitle": "Загаловак раздзела", "LabelChapters": "Раздзелы", "LabelChaptersFound": "раздзелаў знойдзена", "LabelClickForMoreInfo": "Націсніце для больш падрабязнай інфармацыі", @@ -288,14 +290,15 @@ "LabelCollapseSubSeries": "Згарнуць падсерыі", "LabelCollection": "Калекцыя", "LabelCollections": "Калекцыі", - "LabelComplete": "Завершана", + "LabelComplete": "Завяршыць", "LabelConfirmPassword": "Пацвердзіце пароль", - "LabelContinueListening": "Працягваць слухаць", + "LabelContinueListening": "Працяг праслухоўвання", "LabelContinueReading": "Працягнуць чытанне", "LabelContinueSeries": "Працягнуць серыі", + "LabelCorsAllowed": "Дазволеныя крыніцы CORS", "LabelCover": "Вокладка", - "LabelCoverImageURL": "URL выявы вокладкі", - "LabelCoverProvider": "Крыніца вокладак", + "LabelCoverImageURL": "URL-адрас відарыса вокладкі", + "LabelCoverProvider": "Пастаўшчык вокладак", "LabelCreatedAt": "Дата стварэння", "LabelCronExpression": "Запіс Cron", "LabelCurrent": "Бягучы", @@ -306,15 +309,16 @@ "LabelDeleteFromFileSystemCheckbox": "Выдаліць з файлавай сістэмы (зніміце галачку, каб выдаліць толькі з базы даных)", "LabelDescription": "Апісанне", "LabelDeselectAll": "Скасаваць выбар усяго", + "LabelDetectedPattern": "Выяўлены ўзор:", "LabelDevice": "Прылада", "LabelDeviceInfo": "Інфармацыя пра прыладу", "LabelDeviceIsAvailableTo": "Прылада даступная для...", "LabelDirectory": "Каталог", - "LabelDiscFromFilename": "Дыск з імя файла", - "LabelDiscFromMetadata": "Дыск па метададзеных", - "LabelDiscover": "Знайсці", + "LabelDiscFromFilename": "Дыск з файла", + "LabelDiscFromMetadata": "Дыск з метаданых", + "LabelDiscover": "Знаходкі", "LabelDownload": "Спампаваць", - "LabelDownloadNEpisodes": "Спампована {0} эпізодаў", + "LabelDownloadNEpisodes": "Спампавана {0} выпускаў", "LabelDownloadable": "Спампоўваецца", "LabelDuration": "Працягласць", "LabelDurationComparisonExactMatch": "(дакладнае супадзенне)", @@ -328,23 +332,29 @@ "LabelEmailSettingsFromAddress": "Адрас адпраўніка", "LabelEmailSettingsRejectUnauthorized": "Адхіляць неаўтарызаваныя сертыфікаты", "LabelEmailSettingsRejectUnauthorizedHelp": "Адключэнне праверкі SSL-сертыфіката можа зрабіць ваша злучэнне ўразлівым перад пагрозамі бяспекі, такімі як атакі \"чалавек пасярэдзіне\". Адключайце гэтую опцыю толькі калі цалкам разумееце наступствы і ўпэўнены ў надзейнасці паштовага сервера.", - "LabelEmailSettingsSecure": "Бяспечныя", + "LabelEmailSettingsSecure": "Бяспечна", "LabelEmailSettingsSecureHelp": "Калі ўключана, злучэнне будзе выкарыстоўваць TLS пры падключэнні да сервера. Калі выключана, TLS будзе выкарыстоўвацца толькі ў выпадку падтрымкі пашырэння STARTTLS на серверы. У большасці выпадкаў усталюйце значэнне true пры падключэнні да порта 465. Для партоў 587 або 25 не ўключайце яго. (інфармацыя з nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Тэставы адрас", "LabelEmbeddedCover": "Убудаваная вокладка", "LabelEnable": "Уключыць", - "LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:", - "LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.", + "LabelEncodingBackupLocation": "Рэзервовая копія арыгінальных аўдыяфайлаў будзе захавана ў:", + "LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудоўваюцца ў аўдыякнігі з некалькімі трэкамі.", "LabelEncodingClearItemCache": "Пераканайцеся, што перыядычна ачышчаеце кэш элементаў.", - "LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:", - "LabelEncodingInfoEmbedded": "Метададзеныя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.", + "LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу папку з аўдыякнігамі ў:", + "LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыятрэкі ўнутры папкі з аўдыякнігамі.", "LabelEncodingStartedNavigation": "Пасля запуску задачы вы можаце перайсці на іншую старонку.", "LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.", + "LabelEncodingWarningAdvancedSettings": "Увага: Не абнаўляйце гэтыя налады, калі вы не знаёмыя з параметрамі кадавання ffmpeg.", + "LabelEncodingWatcherDisabled": "Калі ў вас адключана адсочванне змен у папцы, пасля трэба будзе паўторна сканіраваць гэту аўдыякнігу.", "LabelEnd": "Канец", "LabelEndOfChapter": "Канец раздзела", - "LabelEpisode": "Эпізод", - "LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай", - "LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі", + "LabelEpisode": "Выпуск", + "LabelEpisodeNotLinkedToRssFeed": "Выпуск не звязаны з RSS-стужкай", + "LabelEpisodeNumber": "Выпуск №{0}", + "LabelEpisodeTitle": "Загаловак выпуску", + "LabelEpisodeType": "Тып выпуску", + "LabelEpisodeUrlFromRssFeed": "URL-адрас выпуску з RSS-стужкі", + "LabelEpisodes": "Выпускі", "LabelEpisodic": "Эпізадычны", "LabelExample": "Прыклад", "LabelExpandSeries": "Разгарнуць серыю", @@ -353,27 +363,48 @@ "LabelExpiresAt": "Тэрмін дзеяння заканчваецца ў", "LabelExpiresInSeconds": "Тэрмін дзеяння заканчваецца праз (секунд)", "LabelExpiresNever": "Ніколі", - "LabelExplicit": "Відверты", + "LabelExplicit": "Непрыстойнае", + "LabelExplicitChecked": "Непрыстойнае (пазначана)", + "LabelExplicitUnchecked": "Прыстойнае (не пазначана)", "LabelExportOPML": "Экспарт OPML", "LabelFeedURL": "URL стужкі", - "LabelFetchingMetadata": "Атрыманне метададзеных", + "LabelFetchingMetadata": "Атрыманне метаданых", "LabelFile": "Файл", "LabelFileBirthtime": "Час стварэння файла", + "LabelFileBornDate": "Створаны {0}", "LabelFileModified": "Час змянення файла", - "LabelFilename": "Імя файла", - "LabelFinished": "Скончана", - "LabelFolder": "Тэчка", + "LabelFileModifiedDate": "Зменены {0}", + "LabelFilename": "Назва файла", + "LabelFilterByUser": "Фільтраваць па карыстальніку", + "LabelFindEpisodes": "Знайсці выпускі", + "LabelFinished": "Завершана", + "LabelFinishedDate": "Завершана {0}", + "LabelFolder": "Папка", + "LabelFolders": "Папкі", + "LabelFontBold": "Тоўсты", "LabelFontBoldness": "Таўшчыня шрыфта", + "LabelFontFamily": "Сямейства шрыфтоў", + "LabelFontItalic": "Курсіў", "LabelFontScale": "Памер шрыфту", + "LabelFontStrikethrough": "Перакрэслены", + "LabelFormat": "Фармат", + "LabelFull": "Поўны", "LabelGenre": "Жанр", "LabelGenres": "Жанры", + "LabelHardDeleteFile": "Жорстка выдаляць файл", "LabelHasEbook": "Мае электронную кнігу", "LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу", "LabelHideSubtitles": "Схаваць падзагалоўкі", + "LabelHighestPriority": "Найвышэйшы прыярытэт", "LabelHost": "Хост", - "LabelImageURLFromTheWeb": "URL выявы з інтэрнэту", + "LabelHour": "Гадзіна", + "LabelHours": "Гадзіны", + "LabelIcon": "Значок", + "LabelImageURLFromTheWeb": "URL-адрас відарыса з інтэрнэту", "LabelInProgress": "У працэсе", + "LabelIncludeInTracklist": "Уключыць у спіс трэкаў", "LabelIncomplete": "Незавершана", + "LabelInterval": "Інтэрвал", "LabelIntervalCustomDailyWeekly": "Карыстальніцкі штодзённы/штотыднёвы", "LabelIntervalEvery12Hours": "Кожныя 12 гадзін", "LabelIntervalEvery15Minutes": "Кожныя 15 хвілін", @@ -385,12 +416,15 @@ "LabelIntervalEveryMinute": "Кожную хвіліну", "LabelInvert": "Інвертаваць", "LabelItem": "Элемент", + "LabelJumpBackwardAmount": "Час пераходу назад", + "LabelJumpForwardAmount": "Час пераходу наперад", "LabelLanguage": "Мова", - "LabelLanguageDefaultServer": "Мова сервера па змаўчанні", + "LabelLanguageDefaultServer": "Прадвызначаная мова сервера", "LabelLanguages": "Мовы", "LabelLastBookAdded": "Апошняя дададзеная кніга", "LabelLastBookUpdated": "Апошняя абноўленая кніга", - "LabelLastSeen": "Апошні прагляд", + "LabelLastProgressDate": "Апошні прагрэс: {0}", + "LabelLastSeen": "Апошняя актыўнасць", "LabelLastTime": "Апошні раз", "LabelLastUpdate": "Апошняе абнаўленне", "LabelLayout": "Знешні выгляд", @@ -401,43 +435,109 @@ "LabelLibrary": "Бібліятэка", "LabelLibraryFilterSublistEmpty": "Не {0}", "LabelLibraryItem": "Элемент бібліятэкі", - "LabelLibraryName": "Імя бібліятэкі", - "LabelLibrarySortByProgress": "Прагрэс абноўлены", + "LabelLibraryName": "Назва бібліятэкі", + "LabelLibrarySortByProgress": "Прагрэс: апошняе абнаўленне", + "LabelLibrarySortByProgressFinished": "Прагрэс: завершана", + "LabelLibrarySortByProgressStarted": "Прагрэс: пачата", "LabelLimit": "Абмежаванне", "LabelLineSpacing": "Міжрадковы інтэрвал", "LabelListenAgain": "Паслухаць зноў", - "LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.", - "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку", - "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.", + "LabelLogLevelDebug": "Debug", + "LabelLogLevelInfo": "Info", + "LabelLogLevelWarn": "Warn", + "LabelLookForNewEpisodesAfterDate": "Шукаць новыя выпускі пасля гэтай даты", + "LabelLowestPriority": "Найніжэйшы прыярытэт", + "LabelMatchConfidence": "Упэўненасць", + "LabelMatchExistingUsersBy": "Параўноўваць існуючых карыстальнікаў па", + "LabelMatchExistingUsersByDescription": "Выкарыстоўваецца для падключэння існуючых карыстальнікаў. Пасля падключэння карыстальнікі будуць супастаўляцца з дапамогай унікальнага ідэнтыфікатара ад пастаўшчыка SSO", + "LabelMaxEpisodesToDownload": "Максімальная колькасць выпускаў для спампоўвання. 0 – неабмежаваная колькасць.", + "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых выпускаў для спампоўвання за праверку", + "LabelMaxEpisodesToKeep": "Максімальная колькасць выпускаў, якія будуць захоўвацца", + "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не задае максімальнага абмежавання. Пасля аўтаматычнага спампоўвання новага выпуску будзе выдалены самы стары выпуск, калі ў вас больш за X выпускаў. Пры кожным новым спампоўванні будзе выдаляцца толькі 1 выпуск.", "LabelMediaPlayer": "Медыяпрайгравальнік", "LabelMediaType": "Тып медыя", - "LabelMetadataOrderOfPrecedenceDescription": "Крыніцы метададзеных з вышэйшым прыярытэтам будуць замяняць крыніцы з ніжэйшым прыярытэтам", - "LabelMetadataProvider": "Пастаўшчык метададзеных", + "LabelMetaTag": "Метатэг", + "LabelMetaTags": "Метатэгі", + "LabelMetadataOrderOfPrecedenceDescription": "Крыніцы метаданых з вышэйшым прыярытэтам будуць замяняць крыніцы з ніжэйшым прыярытэтам", + "LabelMetadataProvider": "Пастаўшчык метаданых", + "LabelMinute": "Хвіліна", + "LabelMinutes": "Хвіліны", "LabelMissing": "Адсутнічае", + "LabelMissingEbook": "Няма электроннай кнігі", + "LabelMissingSupplementaryEbook": "Няма дадатковай электроннай кнігі", + "LabelMobileRedirectURIs": "Дазволеныя URI перанакіравання для мабільных прылад", + "LabelMobileRedirectURIsDescription": "Гэта белы спіс дапушчальных URI перанакіравання для мабільных праграм. Стандартным з'яўляецца audiobookshelf://oauth, які вы можаце выдаліць або дапоўніць дадатковымі URI для інтэграцыі са староннімі праграмамі. Выкарыстанне зорачкі (*) у якасці адзінага запісу дазваляе любы URI.", "LabelMore": "Больш", "LabelMoreInfo": "Больш інфармацыі", - "LabelName": "Імя", - "LabelNarrator": "Чытальнік", - "LabelNarrators": "Чытальнікі", + "LabelName": "Назва", + "LabelNarrator": "Дыктар", + "LabelNarrators": "Дыктары", + "LabelNew": "Новы", + "LabelNewPassword": "Новы пароль", "LabelNewestAuthors": "Новыя аўтары", - "LabelNewestEpisodes": "Новыя эпізоды", - "LabelNoCustomMetadataProviders": "Няма карыстацкіх пастаўшчыкоў метададзеных", - "LabelNotFinished": "Не скончана", + "LabelNewestEpisodes": "Найноўшыя выпускі", + "LabelNextBackupDate": "Дата наступнага рэзервовага капіравання", + "LabelNextChapters": "Наступныя раздзелы:", + "LabelNextScheduledRun": "Наступны запланаваны запуск", + "LabelNoApiKeys": "Няма ключоў API", + "LabelNoCustomMetadataProviders": "Няма карыстальніцкіх пастаўшчыкоў метаданых", + "LabelNoEpisodesSelected": "Не выбрана ніводнага выпуску", + "LabelNotFinished": "Незавершана", "LabelNotStarted": "Не пачата", + "LabelNotes": "Заўвагі", + "LabelNotificationAppriseURL": "URL-адрасы Apprise", + "LabelNotificationAvailableVariables": "Даступныя пераменныя", + "LabelNotificationBodyTemplate": "Шаблон зместу", + "LabelNotificationEvent": "Падзея апавяшчэння", + "LabelNotificationTitleTemplate": "Шаблон загалоўка", + "LabelNotificationsMaxFailedAttempts": "Максімальная колькасць няўдалых спроб", "LabelNotificationsMaxFailedAttemptsHelp": "Апавяшчэнні адключаюцца пасля таго, як не ўдаецца іх адправіць гэтулькі разоў", - "LabelNumberOfEpisodes": "# з эпізодаў", + "LabelNotificationsMaxQueueSize": "Максімальны памер чаргі для падзей апавяшчэнняў", + "LabelNotificationsMaxQueueSizeHelp": "Падзеі могуць спрацоўваць толькі адзін раз у секунду. Падзеі будуць ігнаравацца пры дасягненні максімальнага памеру чаргі. Гэта прадухіляе рассылку спаму.", + "LabelNumberOfBooks": "Колькасць кніг", + "LabelNumberOfChapters": "Колькасць раздзелаў:", + "LabelNumberOfEpisodes": "Колькасць выпускаў", + "LabelOpenIDAdvancedPermsClaimDescription": "Назва прэтэнзіі OpenID, якая змяшчае пашыраныя дазволы для дзеянняў карыстальніка ў праграме, якія будуць прымяняцца да роляў, якія не з'яўляюцца адміністратарамі (калі наладжана). Калі прэтэнзія адсутнічае ў адказе, доступ да ABS будзе забаронены. Калі адсутнічае адзін параметр, ён будзе разглядацца як false. Пераканайцеся, што прэтэнзія пастаўшчыка ідэнтыфікацыі адпавядае чаканай структуры:", + "LabelOpenIDClaims": "Пакіньце наступныя параметры пустымі, каб адключыць пашыранае прызначэнне груп і дазволаў, аўтаматычна прызначаючы групу \"Карыстальнік\".", + "LabelOpenIDGroupClaimDescription": "Назва прэтэнзіі OpenID, якая змяшчае спіс груп карыстальніка. Звычайна іх называюць групамі. Калі наладжана, праграма будзе аўтаматычна прызначаць ролі на аснове членства карыстальніка ў групах, пры ўмове, што ў прэтэнзіі гэтыя групы названы без уліку рэгістра: \"адміністратар\", \"карыстальнік або \"госць\". Прэтэнзія павінна ўтрымліваць спіс, і калі карыстальнік належыць да некалькіх груп, праграма прызначыць ролю, якая адпавядае найвышэйшаму ўзроўню доступу. Калі ніводная група не супадае, доступ будзе забаронены.", "LabelOpenRSSFeed": "Адкрыць RSS-стужку", + "LabelOverwrite": "Перазапісаць", + "LabelPaginationPageXOfY": "Старонка {0} з {1}", "LabelPassword": "Пароль", "LabelPath": "Шлях", - "LabelPermissionsDownload": "Можна спампаваць", - "LabelPlaylists": "Cпісs прайгравання", + "LabelPermanent": "Пастаянны", + "LabelPermissionsAccessAllLibraries": "Мае доступ да ўсіх бібліятэк", + "LabelPermissionsAccessAllTags": "Мае доступ да ўсіх тэгаў", + "LabelPermissionsAccessExplicitContent": "Мае доступ да непрыстойнага змесціва", + "LabelPermissionsCreateEreader": "Можа ствараць прыладу для чытання", + "LabelPermissionsDelete": "Можа выдаляць", + "LabelPermissionsDownload": "Можа спампоўваць", + "LabelPermissionsUpdate": "Можа абнаўляць", + "LabelPermissionsUpload": "Можа запампоўваць", + "LabelPersonalYearReview": "Вынікі года ({0})", + "LabelPhotoPathURL": "Шлях/URL-адрас фота", + "LabelPlayMethod": "Метад прайгравання", + "LabelPlaybackRateIncrementDecrement": "Павелічэнне/памяншэнне хуткасці прайгравання", + "LabelPlayerChapterNumberMarker": "{0} з {1}", + "LabelPlaylists": "Плэй-лісты", "LabelPodcast": "Падкаст", + "LabelPodcastSearchRegion": "Рэгіён пошуку падкастаў", + "LabelPodcastType": "Тып падкаста", "LabelPodcasts": "Падкасты", + "LabelPort": "Порт", + "LabelPrefixesToIgnore": "Прэфіксы, якія трэба ігнараваць (без уліку рэгістра)", "LabelPreventIndexing": "Прадухіліць індэксацыю вашай стужкі каталогамі падкастаў iTunes і Google", + "LabelPrimaryEbook": "Асноўная электронная кніга", "LabelProgress": "Прагрэс", + "LabelProvider": "Пастаўшчык", + "LabelProviderAuthorizationValue": "Значэнне загалоўка аўтарызацыі", "LabelPubDate": "Дата публікацыі", "LabelPublishYear": "Год публікацыі", "LabelPublishedDate": "Апублікавана {0}", + "LabelPublishedDecade": "Дзесяцігоддзе публікацыі", + "LabelPublishedDecades": "Дзесяцігоддзі публікацыі", + "LabelPublisher": "Выдавец", + "LabelPublishers": "Выдаўцы", "LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка", "LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка", "LabelRSSFeedOpen": "RSS-стужка адкрыта", @@ -445,16 +545,22 @@ "LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі", "LabelRSSFeedURL": "URL RSS-стужкі", "LabelRandomly": "Выпадкова", - "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць", + "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працяг праслухоўвання", "LabelRead": "Чытаць", "LabelReadAgain": "Чытаць зноў", + "LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу", "LabelRecentSeries": "Апошнія серыі", "LabelRecentlyAdded": "Нядаўна дададзеныя", + "LabelRecommended": "Рэкамендаваныя", + "LabelRedo": "Узнавіць", + "LabelRegion": "Рэгіён", + "LabelReleaseDate": "Дата выпуску", "LabelRemoveAllMetadataAbs": "Выдаліць усе файлы metadata.abs", "LabelRemoveAllMetadataJson": "Выдаліць усе файлы metadata.json", + "LabelRemoveAudibleBranding": "Выдаляць уступленне і завяршэнне Audible з раздзелаў", "LabelRemoveCover": "Выдаліць вокладку", - "LabelRemoveMetadataFile": "Выдаліць файлы метададзеных у тэчках элементаў бібліятэкі", - "LabelRemoveMetadataFileHelp": "Выдаліць усе файлы metadata.json і metadata.abs у вашых {0} тэчках.", + "LabelRemoveMetadataFile": "Выдаліць файлы метаданых у папках элементаў бібліятэкі", + "LabelRemoveMetadataFileHelp": "Выдаліць усе файлы metadata.json і metadata.abs у {0} папках.", "LabelRowsPerPage": "Радкоў на старонку", "LabelSearchTerm": "Пошукавы запыт", "LabelSearchTitle": "Пошук па загалоўку", @@ -462,8 +568,9 @@ "LabelSeason": "Сезон", "LabelSeasonNumber": "Сезон #{0}", "LabelSelectAll": "Выбраць усё", - "LabelSelectAllEpisodes": "Выбраць усе эпізоды", - "LabelSelectEpisodesShowing": "Выбраць {0} эпізодаў для паказу", + "LabelSelectAllEpisodes": "Выбраць усе выпускі", + "LabelSelectEpisodesShowing": "Выбраць {0} выпускаў для паказу", + "LabelSelectUser": "Выберыце карыстальніка", "LabelSelectUsers": "Выбраць карыстальнікаў", "LabelSendEbookToDevice": "Адправіць электронную кнігу на...", "LabelSequence": "Паслядоўнасць", @@ -477,48 +584,70 @@ "LabelSetEbookAsSupplementary": "Зрабіць дадатковым", "LabelSettingsAllowIframe": "Дазволіць убудоўванне ў iframe", "LabelSettingsAudiobooksOnly": "Толькі аўдыякнігі", - "LabelSettingsAudiobooksOnlyHelp": "Уключэнне гэтай налады будзе ігнараваць файлы электронных кніг, калі толькі яны не знаходзяцца ў тэчцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.", + "LabelSettingsAudiobooksOnlyHelp": "Пры ўключэнні гэтай налады файлы электронных кніг будуць ігнаравацца, калі толькі яны не знаходзяцца ў папцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.", "LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі", - "LabelSettingsEnableWatcherForLibrary": "Аўтаматычна правяраць бібліятэку на змены", + "LabelSettingsChromecastSupport": "Падтрымка Chromecast", + "LabelSettingsDateFormat": "Фарматы даты", + "LabelSettingsEnableWatcher": "Аўтаматычна сачыць за зменамі ў бібліятэках", + "LabelSettingsEnableWatcherForLibrary": "Аўтаматычна сачыць за зменамі ў бібліятэцы", "LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера", - "LabelSettingsEpubsAllowScriptedContent": "Дазволіць скрыптавы кантэнт у EPUB", - "LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць EPUB-файлам выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы EPUB-файлаў.", + "LabelSettingsEpubsAllowScriptedContent": "Дазваляць скрыпты ў файлах EPUB", + "LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць файлам EPUB выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы файлаў EPUB.", "LabelSettingsExperimentalFeatures": "Эксперыментальныя функцыі", "LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.", - "LabelSettingsFindCovers": "Знайсці вокладкі", - "LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або выявы вокладкі ў тэчцы, сканер паспрабуе знайсці вокладку.
Заўвага: гэта павялічыць час сканавання", + "LabelSettingsFindCovers": "Шукаць вокладкі", + "LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або відарыса вокладкі ў папцы, сканер паспрабуе знайсці вокладку.
Заўвага: гэта павялічыць час сканіравання", "LabelSettingsHideSingleBookSeries": "Схаваць серыі з адной кнігай", "LabelSettingsHideSingleBookSeriesHelp": "Серыі, якія змяшчаюць толькі адну кнігу, будуць схаваны са старонкі серый і паліц на галоўнай старонцы.", - "LabelSettingsHomePageBookshelfView": "Галоўная старонка выкарыстоўвае выгляд кніжнай паліцы", - "LabelSettingsLibraryBookshelfView": "Бібліятэка выкарыстоўвае выгляд кніжнай паліцы", - "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунды)", - "LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як скончаны, калі", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусціць папярэднія кнігі ў \"Працягнуць серыю\"", - "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Палка \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.", - "LabelSettingsParseSubtitles": "Разабраць падзагалоўкі", - "LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў тэчак аўдыякніг.
Падзагаловак павінен быць аддзелены знакам \" - \".
Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"", - "LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метададзеным", - "LabelSettingsPreferMatchedMetadataHelp": "Супадаючыя дадзеныя будуць замяняць дэталі элемента пры выкарыстанні функцыі Хуткі пошук. Па змаўчанні Хуткі пошук запаўняе толькі адсутныя дэталі.", - "LabelSettingsStoreCoversWithItemHelp": "Па змаўчанні вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у тэчцы элемента вашай бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"", - "LabelSettingsStoreMetadataWithItem": "Захоўваць метададзеныя разам з элементам", - "LabelSettingsStoreMetadataWithItemHelp": "Па змаўчанні метададзеныя захоўваюцца ў /metadata/items. Уключэнне гэтай опцыі забяспечыць захоўванне файлаў метададзеных у тэчках элементаў вашай бібліятэкі", + "LabelSettingsHomePageBookshelfView": "Кніжныя паліцы на галоўнай старонцы", + "LabelSettingsLibraryBookshelfView": "Кніжныя паліцы ў бібліятэцы", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Працэнт завяршэння большы за", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, меншы за (секунд)", + "LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як завершаны, калі", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Прапусціць папярэднія кнігі ў \"Працягнуць серыю\"", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Паліца \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.", + "LabelSettingsParseSubtitles": "Аналізаваць падзагалоўкі", + "LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў папак аўдыякніг.
Падзагаловак павінен быць аддзелены сімвалам \" - \".
Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"", + "LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метаданым", + "LabelSettingsPreferMatchedMetadataHelp": "Супадаючыя даныя будуць замяняць звесткі элемента пры выкарыстанні функцыі Хуткі пошук. Прадвызначана Хуткі пошук запаўняе толькі адсутныя звесткі.", + "LabelSettingsSkipMatchingBooksWithASIN": "Прапусціць параўнанне кніг, якія ўжо маюць ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Прапусціць параўнанне кніг, якія ўжо маюць ISBN", + "LabelSettingsSortingIgnorePrefixes": "Ігнараваць прэфіксы пры сартаванні", + "LabelSettingsSortingIgnorePrefixesHelp": "напрыклад, для прэфікса \"the\" загаловак кнігі \"The Book Title\" будзе сартавацца як \"Book Title, The\"", + "LabelSettingsSquareBookCovers": "Выкарыстоўваць квадратныя вокладкі кніг", + "LabelSettingsSquareBookCoversHelp": "Аддаваць перавагу квадратным вокладкам замест стандартных вокладак з суадносінамі бакоў 1.6:1", + "LabelSettingsStoreCoversWithItem": "Захоўваць вокладкі з элементам", + "LabelSettingsStoreCoversWithItemHelp": "Прадвызначана вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у папцы элемента бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"", + "LabelSettingsStoreMetadataWithItem": "Захоўваць метаданыя з элементам", + "LabelSettingsStoreMetadataWithItemHelp": "Прадвызначана метаданыя захоўваюцца ў /metadata/items. Пры ўключэнні гэтай опцыі файлаў метаданых будуць захоўвацца ў папках элементаў бібліятэкі", "LabelSettingsTimeFormat": "Фармат часу", - "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.", + "LabelShare": "Абагуліць", + "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку, спампоўваць ZIP-архіў элемента бібліятэкі.", + "LabelShareOpen": "Абагульванне адкрыта", + "LabelShareURL": "URL-адрас для абагульвання", "LabelShowAll": "Паказаць усё", + "LabelShowSeconds": "Паказваць секунды", "LabelShowSubtitles": "Паказаць падзагалоўкі", "LabelSize": "Памер", "LabelSleepTimer": "Таймер сну", - "LabelStart": "Пачаць", + "LabelSlug": "Ідэнтыфікатар", + "LabelSortAscending": "Па ўзрастанні", + "LabelSortDescending": "Па ўбыванні", + "LabelSortPubDate": "Сартаваць па даце публікацыі", + "LabelStart": "Пачатак", "LabelStartTime": "Час пачатку", - "LabelStatsAudioTracks": "Аўдыядарожкі", - "LabelStatsAuthors": "Аўтары", - "LabelStatsBestDay": "Лепшы дзень", + "LabelStarted": "Пачата", + "LabelStartedAt": "Пачата ў", + "LabelStartedDate": "Пачата {0}", + "LabelStatsAudioTracks": "Аўдыятрэкі", + "LabelStatsAuthors": "Аўтараў", + "LabelStatsBestDay": "Найлепшы дзень", "LabelStatsDailyAverage": "У сярэднім за дзень", "LabelStatsDays": "Дзён", "LabelStatsDaysListened": "Дзён праслухана", "LabelStatsHours": "Гадзін", "LabelStatsInARow": "без перапынку", - "LabelStatsItemsFinished": "Скончаныя элементы", + "LabelStatsItemsFinished": "Элементаў завершана", "LabelStatsItemsInLibrary": "Элементаў у бібліятэцы", "LabelStatsMinutes": "хвілін", "LabelStatsMinutesListening": "Хвілін праслухоўвання", @@ -526,7 +655,7 @@ "LabelStatsOverallHours": "Агульная колькасць гадзін", "LabelStatsWeekListening": "Праслухана за тыдзень", "LabelSubtitle": "Падзагаловак", - "LabelSupportedFileTypes": "Падтрымліваемыя тыпы файлаў", + "LabelSupportedFileTypes": "Падтрымліваюцца тыпы файлаў", "LabelTag": "Метка", "LabelTags": "Меткі", "LabelTagsAccessibleToUser": "Меткі, даступныя карыстальніку", @@ -539,6 +668,7 @@ "LabelTheme": "Тэма", "LabelThemeDark": "Цёмная", "LabelThemeLight": "Светлая", + "LabelThemeSepia": "Сепія", "LabelTimeBase": "Часавая база", "LabelTimeDurationXHours": "{0} гадзін", "LabelTimeDurationXMinutes": "{0} хвілін", @@ -549,21 +679,26 @@ "LabelTimeListenedToday": "Час праслухоўвання сёння", "LabelTimeRemaining": "Засталося {0}", "LabelTimeToShift": "Час зрушэння ў секундах", - "LabelTitle": "Назва", - "LabelToolsEmbedMetadata": "Убудаваць метададзеныя", - "LabelToolsEmbedMetadataDescription": "Убудаваць метададзеныя ў аўдыёфайлы, уключаючы выяву вокладкі і раздзелы.", - "LabelToolsMakeM4bDescription": "Стварыць аўдыёкнігу ў фармаце .M4B з убудаванымі метададзенымі, выявай вокладкі і раздзеламі.", - "LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метададзенымі, выявай вокладкі і раздзеламі.", + "LabelTitle": "Загаловак", + "LabelToolsEmbedMetadata": "Убудаваць метаданыя", + "LabelToolsEmbedMetadataDescription": "Убудаваць метаданыя ў аўдыяфайлы, уключаючы відарыс вокладкі і раздзелы.", + "LabelToolsM4bEncoder": "Кадавальнік M4B", + "LabelToolsMakeM4b": "Стварыць файл аўдыякнігі M4B", + "LabelToolsMakeM4bDescription": "Стварыць аўдыякнігу ў фармаце .M4B з убудаванымі метаданымі, відарысам вокладкі і раздзеламі.", + "LabelToolsSplitM4b": "Падзяліць M4B на MP3", + "LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метаданымі, відарысам вокладкі і раздзеламі.", "LabelTotalDuration": "Агульная працягласць", "LabelTotalTimeListened": "Агульны час праслухоўвання", - "LabelTrackFromFilename": "Дарожка з імя файла", - "LabelTrackFromMetadata": "Дарожка з метададзеных", - "LabelTracks": "Дарожкі", - "LabelTracksMultiTrack": "Шматдарожкавы", - "LabelTracksNone": "Няма дарожак", - "LabelTracksSingleTrack": "Аднадарожкавы", + "LabelTrackFromFilename": "Трэк з файла", + "LabelTrackFromMetadata": "Трэк з метаданых", + "LabelTracks": "Трэкі", + "LabelTracksMultiTrack": "Некалькі трэкаў", + "LabelTracksNone": "Няма трэкаў", + "LabelTracksSingleTrack": "Адзін трэк", + "LabelTrailer": "Трэйлер", "LabelType": "Тып", - "LabelUndo": "Адмяніць", + "LabelUnabridged": "Поўная версія", + "LabelUndo": "Адрабіць", "LabelUnknown": "Невядома", "LabelUnknownPublishDate": "Невядомая дата публікацыі", "LabelUpdateCover": "Абнавіць вокладку", @@ -571,13 +706,14 @@ "LabelUpdateDetails": "Абнавіць падрабязнасці", "LabelUpdateDetailsHelp": "Дазволіць замену існуючых падрабязнасцей для выбраных кніг пры выяўленні адпаведнасці", "LabelUpdatedAt": "Абноўлена ў", - "LabelUploaderDragAndDrop": "Перацягвайце і скідайце файлы або тэчкі", + "LabelUploaderDragAndDrop": "Перацягніце файлы або папкі", "LabelUploaderDragAndDropFilesOnly": "Перацягвайце і скідайце файлы", "LabelUploaderDropFiles": "Скідайце файлы", - "LabelUploaderItemFetchMetadataHelp": "Аўтаматычна атрымліваць назву, аўтара і серыю", + "LabelUploaderItemFetchMetadataHelp": "Аўтаматычна атрымліваць загаловак, аўтара і серыю", "LabelUseAdvancedOptions": "Выкарыстоўваць пашыраныя параметры", - "LabelUseChapterTrack": "Выкарыстоўваць дарожку раздзелаў", - "LabelUseFullTrack": "Выкарыстоўваць поўную дарожку", + "LabelUseChapterTrack": "Выкарыстоўваць трэк раздзела", + "LabelUseFullTrack": "Выкарыстоўваць увесь трэк", + "LabelUseZeroForUnlimited": "0 – неабмежавана", "LabelUser": "Карыстальнік", "LabelUsername": "Імя карыстальніка", "LabelValue": "Значэнне", @@ -587,89 +723,196 @@ "LabelViewPlayerSettings": "Праглядзець налады прайгравальніка", "LabelViewQueue": "Праглядзець чаргу прайгравальніка", "LabelVolume": "Гучнасць", - "LabelWebRedirectURLsDescription": "Аўтарызуйце гэтыя URL у вашым OAuth-правайдары для перанакіравання ў вэб-дадатак пасля ўваходу:", - "LabelWebRedirectURLsSubfolder": "Падтэчка для URL-перанакіраванняў", + "LabelWebRedirectURLsDescription": "Аўтарызуйце гэтыя URL-адрасы ў вашым пастаўшчыку OAuth для перанакіравання ў вэб-праграму пасля ўваходу:", + "LabelWebRedirectURLsSubfolder": "Падпапка для URL-адрасоў перанакіравання", "LabelWeekdaysToRun": "Дні тыдня для запуску", "LabelXBooks": "{0} кніг", "LabelXItems": "{0} элементаў", "LabelYearReviewHide": "Схаваць вынікі года", "LabelYearReviewShow": "Азнаёміцца з вынікамі года", - "LabelYourAudiobookDuration": "Працягласць вашай аўдыякнігі", + "LabelYourAudiobookDuration": "Працягласць аўдыякнігі", "LabelYourBookmarks": "Вашы закладкі", - "LabelYourPlaylists": "Вашы спісы прайгравання", + "LabelYourPlaylists": "Вашы плэй-лісты", "LabelYourProgress": "Ваш прагрэс", "MessageAddToPlayerQueue": "Дадаць у чаргу прайгравальніка", "MessageAppriseDescription": "Каб выкарыстоўваць гэтую функцыю, вам спатрэбіцца запусціць асобнік Apprise API або API, які будзе апрацоўваць тыя ж запыты.
URL Apprise API павінен быць поўным шляхам для адпраўкі апавяшчэння, напрыклад, калі ваш API працуе па адрасе http://192.168.1.1:8337, то вы павінны ўвесці http://192.168.1.1:8337/notify.", - "MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і выявы, якія захоўваюцца ў /metadata/items і /metadata/authors. Рэзервовыя копіі не ўключаюць файлы, якія захоўваюцца ў вашых тэчках бібліятэкі.", + "MessageAsinCheck": "Пераканайцеся, што выкарыстоўваеце ASIN з правільнага рэгіёна Audible, а не Amazon.", + "MessageAuthenticationLegacyTokenWarning": "Састарэлыя токены API будуць выдалены ў будучыні. Замест іх выкарыстоўвайце ключы API.", + "MessageAuthenticationOIDCChangesRestart": "Перазапусціце сервер пасля захавання, каб прымяніць змены OIDC.", + "MessageAuthenticationSecurityMessage": "Дзеля бяспекі была палепшана аўтэнтыфікацыя. Усім карыстальнікам трэба паўторна ўвайсці ў сістэму.", + "MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і відарысы, якія захоўваюцца ў /metadata/items і /metadata/authors. Рэзервовыя копіі не ўключаюць файлы, якія захоўваюцца ў папках бібліятэкі.", "MessageBackupsLocationEditNote": "Заўвага: Абнаўленне месцазнаходжання рэзервовых копій не перамяшчае і не змяняе існуючыя рэзервовыя копіі", - "MessageBackupsLocationNoEditNote": "Заўвага: Месцазнаходжанне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.", + "MessageBackupsLocationNoEditNote": "Заўвага: Размяшчэнне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.", "MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым", - "MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі дадзенымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны", - "MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты дадзенымі з гэтага элемента", - "MessageBatchQuickMatchDescription": "Хуткі пошук паспрабуе дадаць адсутныя вокладкі і метададзеныя для выбраных элементаў. Уключыце ніжэй выкладзеныя опцыі, каб дазволіць Хуткаму пошуку замяняць існуючыя вокладкі і/або метададзеныя.", + "MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі данымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны", + "MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты данымі з гэтага элемента", + "MessageBatchQuickMatchDescription": "Хуткі пошук паспрабуе дадаць адсутныя вокладкі і метаданыя для выбраных элементаў. Уключыце параметры ніжэй, каб дазволіць Хуткаму пошуку замяняць існуючыя вокладкі і/або метаданыя.", + "MessageBookshelfNoCollections": "Вы пакуль не стварылі ніводнай калекцый", + "MessageBookshelfNoCollectionsHelp": "Калекцыі публічныя. Усе карыстальнікі, якія маюць доступ да бібліятэкі, могуць іх бачыць.", "MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак", + "MessageBookshelfNoResultsForFilter": "Няма вынікаў для фільтра \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "Няма вынікаў па запыце", + "MessageBookshelfNoSeries": "У вас няма серый", + "MessageBulkChapterPattern": "Колькі раздзелаў вы хочаце дадаць з дапамогай гэтага ўзору нумарацыі?", + "MessageChapterEndIsAfter": "Канец раздзела ідзе пасля канца аўдыякнігі", + "MessageChapterErrorFirstNotZero": "Першы раздзел павінен пачынацца з 0", "MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі", "MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела", - "MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?", - "MessageConfirmDeleteMetadataProvider": "Ці ўпэўненыя вы, што жадаеце выдаліць карыстацкага пастаўшчыка метададзеных \"{0}\"?", - "MessageConfirmEmbedMetadataInAudioFiles": "Ці ўпэўненыя вы, што жадаеце ўбудаваць метададзеныя ў {0} аўдыёфайлаў?", - "MessageConfirmPurgeCache": "Ачышчэнне кэша выдаліць увесь каталог па адрасе /metadata/cache.

Ці сапраўды вы жадаеце выдаліць каталог кэша?", - "MessageConfirmPurgeItemsCache": "Ачышчэнне кэша элементаў выдаліць увесь каталог па адрасе /metadata/cache/items.
Вы ўпэўнены?", - "MessageConfirmQuickMatchEpisodes": "Хуткае супадзенне эпізодаў перазапіша дэталі, калі супадзенне будзе знойдзена. Будуць абноўлены толькі эпізоды, якія не супадаюць. Вы ўпэўнены?", - "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?", - "MessageConfirmRemoveMetadataFiles": "Ці ўпэўненыя вы, што жадаеце выдаліць усе файлы метададзеных{0} у тэчках элементаў вашай бібліятэкі?", - "MessageConfirmRemovePlaylist": "Вы ўпэўненыя, што жадаеце выдаліць свой спіс прайгравання \"{0}\"?", + "MessageChapterStartIsAfter": "Пачатак раздзела ідзе пасля канца аўдыякнігі", + "MessageChaptersNotFound": "Раздзелы не знойдзены", + "MessageCheckingCron": "Праверка cron...", + "MessageConfirmCloseFeed": "Вы ўпэўнены, што хочаце закрыць гэту стужку?", + "MessageConfirmDeleteApiKey": "Вы ўпэўнены, што хочаце выдаліць ключ API \"{0}\"?", + "MessageConfirmDeleteBackup": "Вы ўпэўнены, што хочаце выдаліць рэзервовую копію для {0}?", + "MessageConfirmDeleteDevice": "Вы ўпэўнены, што хочаце выдаліць прыладу для чытання \"{0}\"?", + "MessageConfirmDeleteFile": "Будзе выдалены файл з файлавай сістэмы. Вы ўпэўнены?", + "MessageConfirmDeleteLibrary": "Вы ўпэўнены, што хочаце назаўжды выдаліць бібліятэку \"{0}\"?", + "MessageConfirmDeleteLibraryItem": "Будзе выдалены элемент бібліятэкі з базы даных і файлавай сістэмы. Вы ўпэўнены?", + "MessageConfirmDeleteLibraryItems": "Будзе выдалена {0} элементаў бібліятэкі з базы даных і файлавай сістэмы. Вы ўпэўнены?", + "MessageConfirmDeleteMetadataProvider": "Вы ўпэўнены, што хочаце выдаліць карыстальніцкага пастаўшчыка метаданых \"{0}\"?", + "MessageConfirmDeleteNotification": "Вы ўпэўнены, што хочаце выдаліць гэта апавяшчэнне?", + "MessageConfirmDeleteSession": "Вы ўпэўнены, што хочаце выдаліць гэты сеанс?", + "MessageConfirmEmbedMetadataInAudioFiles": "Вы ўпэўнены, што хочаце ўбудаваць метаданыя ў {0} аўдыяфайлаў?", + "MessageConfirmForceReScan": "Вы ўпэўнены, што хочаце прымусова паўторна сканіраваць?", + "MessageConfirmMarkAllEpisodesFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як завершаныя?", + "MessageConfirmMarkAllEpisodesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе выпускі як незавершаныя?", + "MessageConfirmMarkItemFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як завершаны?", + "MessageConfirmMarkItemNotFinished": "Вы ўпэўнены, што хочаце пазначыць \"{0}\" як незавершаны?", + "MessageConfirmMarkSeriesFinished": "Вы ўпэўнены, што хочаце пазначыць усе кнігі ў гэтай серыі як завершаныя?", + "MessageConfirmMarkSeriesNotFinished": "Вы ўпэўнены, што хочаце пазначыць усе кнігі ў гэтай серыі як незавершаныя?", + "MessageConfirmNotificationTestTrigger": "Уключыць гэта апавяшчэнне з тэставымі данымі?", + "MessageConfirmPurgeCache": "Ачышчэнне кэшу выдаліць увесь каталог па адрасе /metadata/cache.

Вы ўпэўнены, што хочаце выдаліць каталог кэшу?", + "MessageConfirmPurgeItemsCache": "Ачышчэнне кэшу элементаў выдаліць увесь каталог па адрасе /metadata/cache/items.
Вы ўпэўнены?", + "MessageConfirmQuickEmbed": "Увага! Хуткае ўбудаванне не стварае рэзервовых копій аўдыяфайлаў. Пераканайцеся, што ў вас ёсць рэзервовая копія аўдыяфайлаў.

Хочаце працягнуць?", + "MessageConfirmQuickMatchEpisodes": "Хуткае супадзенне выпускаў перазапіша дэталі, калі супадзенне будзе знойдзена. Будуць абноўлены толькі выпускі, якія не супадаюць. Вы ўпэўнены?", + "MessageConfirmReScanLibraryItems": "Вы ўпэўнены, што хочаце паўторна сканіраваць {0} элементаў?", + "MessageConfirmRemoveAllChapters": "Вы ўпэўнены, што хочаце выдаліць усе раздзелы?", + "MessageConfirmRemoveAuthor": "Вы ўпэўнены, што хочаце выдаліць аўтара \"{0}\"?", + "MessageConfirmRemoveCollection": "Вы ўпэўнены, што хочаце выдаліць калекцыю \"{0}\"?", + "MessageConfirmRemoveEpisode": "Вы ўпэўнены, што хочаце выдаліць выпуск \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Заўвага: Аўдыяфайл не будзе выдалены, калі не ўключыць параметр \"Жорстка выдаляць файл\"", + "MessageConfirmRemoveEpisodes": "Вы ўпэўнены, што хочаце выдаліць {0} выпускаў?", + "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што хочаце выдаліць {0} сеансаў праслухоўвання?", + "MessageConfirmRemoveMetadataFiles": "Вы ўпэўнены, што хочаце выдаліць усе файлы metadata.{0} у папках элементаў бібліятэкі?", + "MessageConfirmRemoveNarrator": "Вы ўпэўнены, што хочаце выдаліць дыктара \"{0}\"?", + "MessageConfirmRemovePlaylist": "Вы ўпэўнены, што хочаце выдаліць плэй-ліст \"{0}\"?", + "MessageConfirmRenameGenre": "Вы ўпэўнены, што хочаце перайменаваць жанр \"{0}\" на \"{1}\" для ўсіх элементаў?", + "MessageConfirmRenameGenreMergeNote": "Заўвага: Гэты жанр ужо існуе, таму яны будуць аб'яднаны.", + "MessageConfirmRenameGenreWarning": "Увага! Падобны жанр з іншым рэгістрам літар ужо існуе — \"{0}\".", + "MessageConfirmRenameTag": "Вы ўпэўнены, што хочаце перайменаваць тэг \"{0}\" на \"{1}\" для ўсіх элементаў?", + "MessageConfirmRenameTagMergeNote": "Заўвага: Гэты тэг ужо існуе, таму яны будуць аб'яднаны.", + "MessageConfirmRenameTagWarning": "Увага! Падобны тэг з іншым рэгістрам ужо існуе: \"{0}\".", + "MessageConfirmResetProgress": "Вы ўпэўнены, што хочаце скінуць свой прагрэс?", "MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?", - "MessageDownloadingEpisode": "Спампоўка эпізоду", - "MessageEmbedQueue": "У чарзе на ўбудаванне метададзеных (у чарзе {0})", - "MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", + "MessageConfirmUnlinkOpenId": "Вы ўпэўнены, што хочаце адвязаць гэтага карыстальніка ад OpenID?", + "MessageDaysListenedInTheLastYear": "{0} дзён праслухоўвання за апошні год", + "MessageDownloadingEpisode": "Спампоўванне выпуску", + "MessageDragFilesIntoTrackOrder": "Перацягніце файлы ў правільным парадку трэкаў", + "MessageEmbedFailed": "Не ўдалося ўбудаваць!", + "MessageEmbedFinished": "Убудаванне завершана!", + "MessageEmbedQueue": "У чарзе на ўбудаванне метаданых (у чарзе {0})", + "MessageEpisodesQueuedForDownload": "{0} выпуск(-аў) у чарзе спампоўвання", "MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.", "MessageFeedURLWillBe": "URL стужкі будзе {0}", "MessageFetching": "Атрыманне...", + "MessageForceReScanDescription": "прасканіруе ўсе файлы зноў, як пры новым сканаванні. Тэгі ID3 аўдыёфайлаў, файлы OPF і тэкставыя файлы будуць сканіравацца як новыя.", + "MessageHeatmapListeningTimeTooltip": "{0} праслухана on {1}", + "MessageHeatmapNoListeningSessions": "Няма сеансаў праслухоўвання на {0}", + "MessageImportantNotice": "Важная заўвага!", + "MessageInsertChapterBelow": "Уставіць раздзел ніжэй", "MessageInvalidAsin": "Няправільны ASIN", + "MessageItemsSelected": "Выбрана элементаў: {0}", "MessageItemsUpdated": "{0} элементаў абноўлена", + "MessageJoinUsOn": "Далучайцеся да нас у", "MessageLoading": "Загрузка...", + "MessageLoadingFolders": "Загрузка папак...", "MessageLogsDescription": "Журналы захоўваюцца ў каталогу /metadata/logs у фармаце JSON. Журналы памылак захоўваюцца ў файле /metadata/logs/crashlogs.txt.", - "MessageMapChapterTitles": "Супаставіць назвы раздзелаў з вашымі існуючымі раздзеламі аўдыякнігі без змянення часовых метак", - "MessageMarkAsFinished": "Пазначыць як скончана", + "MessageM4BFailed": "Памылка M4B!", + "MessageM4BFinished": "M4B завершана!", + "MessageMapChapterTitles": "Супаставіць загалоўкі раздзелаў з існуючымі раздзеламі аўдыякнігі без змянення пазнак часу", + "MessageMarkAllEpisodesFinished": "Пазначыць усе выпускі як завершаныя", + "MessageMarkAllEpisodesNotFinished": "Пазначыць усе выпускі як незавершаныя", + "MessageMarkAsFinished": "Пазначыць як завершаную", + "MessageMarkAsNotFinished": "Пазначыць як незавершаную", + "MessageMatchBooksDescription": "паспрабуе параўнаць кнігі ў бібліятэцы з кнігай ад выбранай пошукавай сістэмы і запоўніць пустыя палі і вокладку. Не перазапісвае звесткі.", + "MessageNoAudioTracks": "Няма аўдыятрэкаў", + "MessageNoAuthors": "Няма аўтараў", + "MessageNoBackups": "Няма рэзервовых копій", "MessageNoBookmarks": "Няма закладак", "MessageNoChapters": "Няма раздзелаў", "MessageNoCollections": "Няма калекцый", - "MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак", + "MessageNoCoversFound": "Не знойдзена вокладак", + "MessageNoDescription": "Няма апісання", + "MessageNoDevices": "Няма прылад", + "MessageNoDownloadsInProgress": "Зараз няма актыўных спампованняў", "MessageNoDownloadsQueued": "Няма спамповак у чарзе", + "MessageNoEpisodeMatchesFound": "Адпаведных выпускаў не знойдзена", + "MessageNoEpisodes": "Няма выпускаў", + "MessageNoFoldersAvailable": "Няма даступных папак", + "MessageNoGenres": "Няма жанраў", + "MessageNoIssues": "Няма праблем", "MessageNoItems": "Няма элементаў", "MessageNoItemsFound": "Элементы не знойдзены", "MessageNoListeningSessions": "Няма сеансаў праслухоўвання", + "MessageNoLogs": "Няма журналаў", "MessageNoMediaProgress": "Няма прагрэсу медыя", + "MessageNoNotifications": "Няма апавяшчэнняў", "MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі", "MessageNoPodcastsFound": "Падкасты не знойдзены", + "MessageNoResults": "Няма вынікаў", + "MessageNoSearchResultsFor": "Няма вынікаў пошуку па запыце \"{0}\"", + "MessageNoSeries": "Няма серый", + "MessageNoTags": "Няма тэгаў", "MessageNoTasksRunning": "Няма запушчаных задач", "MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся", - "MessageNoUserPlaylists": "У вас няма спісаў прайгравання", - "MessageNoUserPlaylistsHelp": "Спісы прайгравання прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.", - "MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага файла OPML. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.", - "MessagePlaylistCreateFromCollection": "Стварыць спіс прайгравання з калекцыі", + "MessageNoUserPlaylists": "У вас няма плэй-лістоў", + "MessageNoUserPlaylistsHelp": "Плэй-лісты прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.", + "MessageNotYetImplemented": "Пакуль не рэалізавана", + "MessageOpmlPreviewNote": "Заўвага: гэта перадпрагляд прааналізаванага файла OPML. Фактычны загаловак падкаста будзе ўзяты з RSS-стужкі.", + "MessageOr": "або", + "MessagePauseChapter": "Прыпыніць прайграванне раздзела", + "MessagePlayChapter": "Паслухаць пачатак раздзела", + "MessagePlaylistCreateFromCollection": "Стварыць плэй-ліст з калекцыі", + "MessagePleaseWait": "Пачакайце...", "MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення", "MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі", - "MessageQuickMatchDescription": "Запоўніць пустыя дэталі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе дэталі, калі опцыя «Аддаваць перавагу супадаючым метададзеным» на серверы не ўключана.", - "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на", - "MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама выявы вокладкі ў /metadata/items і /metadata/authors.

Рэзервовыя копіі не змяняюць файлы ў вашых тэчках бібліятэкі. Калі вы ўключылі наладкі сервера для захоўвання воклак і метададзеных у тэчках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца.

Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.", + "MessageQuickEmbedInProgress": "Выконваецца хуткае ўбудаванне", + "MessageQuickEmbedQueue": "Пастаўлена ў чаргу для хуткага ўбудавання ({0} у чарзе)", + "MessageQuickMatchAllEpisodes": "Хуткае параўнанне ўсіх выпускаў", + "MessageQuickMatchDescription": "Запоўніць пустыя звесткі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе звесткіі, калі параметр \"Аддаваць перавагу супадаючым метаданым\" на серверы не ўключана.", + "MessageRemoveChapter": "Выдаліць раздзел", + "MessageRemoveEpisodes": "Выдаліць выпускі ({0})", + "MessageRemoveFromPlayerQueue": "Выдаліць з чаргі прагравання", + "MessageRemoveUserWarning": "Вы ўпэўнены, што хочаце назаўжды выдаліць карыстальніка \"{0}\"?", + "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце функцыі і ўносьце свой уклад на", + "MessageResetChaptersConfirm": "Вы ўпэўнены, што хочаце скінуць раздзелы і адрабіць зробленыя вамі змены?", + "MessageRestoreBackupConfirm": "Вы ўпэўнены, што хочаце аднавіць рэзервовую копію, створаную", + "MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама відарысы вокладкі ў /metadata/items і /metadata/authors.

Рэзервовыя копіі не змяняюць файлы ў папках бібліятэкі. Калі вы ўключылі налады сервера для захоўвання воклак і метаданых у папках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца.

Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.", + "MessageScheduleLibraryScanNote": "Большасці карыстальнікаў рэкамендуецца не выключаць гэтую функцыю і пакідаць уключанай наладу \"Аўтаматычна сачыць за зменамі ў бібліятэцы\" — яна будзе аўтаматычна выяўляць змены ў папках бібліятэкі. Уключыце гэту функцыю, калі \"Аўтаматычна сачыць за зменамі ў бібліятэцы\" не працуе для вашай файлавай сістэмы (напрыклад, NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}", + "MessageSearchResultsFor": "Вынікі пошуку для", + "MessageSelected": "Выбрана: {0}", + "MessageSeriesSequenceCannotContainSpaces": "Паслядоўнасць серый не можа ўтрымліваць прабелы", + "MessageServerCouldNotBeReached": "Сервер недаступны", + "MessageSetChaptersFromTracksDescription": "Задаць раздзелы, выкарыстоўваючы кожны аўдыяфайл у якасці раздзела і назву аўдыяфайла ў якасці загалоўка раздзела", + "MessageShareExpirationWillBe": "Тэрмін дзеяння будзе {0}", + "MessageShareExpiresIn": "Тэрмін дзеяння заканчваецца праз {0}", + "MessageShareURLWillBe": "URL-адрас для абагульвання будзе {0", "MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?", - "MessageTaskAudioFileNotWritable": "Аўдыёфайл \"{0}\" недаступны для запісу", + "MessageTaskAudioFileNotWritable": "Аўдыяфайл \"{0}\" недаступны для запісу", "MessageTaskCanceledByUser": "Задача скасавана карыстальнікам", - "MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"", - "MessageTaskEmbeddingMetadata": "Убудаванне метададзеных", - "MessageTaskEmbeddingMetadataDescription": "Убудаванне метададзеных у аўдыёкнігу \"{0}\"", + "MessageTaskDownloadingEpisodeDescription": "Спампоўванне выпуску \"{0}\"", + "MessageTaskEmbeddingMetadata": "Убудаванне метаданых", + "MessageTaskEmbeddingMetadataDescription": "Убудаванне метаданых у аўдыякнігу \"{0}\"", "MessageTaskEncodingM4b": "Кадаванне M4B", "MessageTaskEncodingM4bDescription": "Кадаванне аўдыякнігі \"{0}\" у адзін файл m4b", "MessageTaskFailed": "Не ўдалося", - "MessageTaskFailedToBackupAudioFile": "Не ўдалося зрабіць рэзервовую копію аўдыёфайла \"{0}\"", - "MessageTaskFailedToCreateCacheDirectory": "Не ўдалося стварыць каталог кэша", - "MessageTaskFailedToEmbedMetadataInFile": "Не ўдалося ўбудаваць метададзеныя ў файл \"{0}\"", - "MessageTaskFailedToMergeAudioFiles": "Не ўдалося аб’яднаць аўдыёфайлы", + "MessageTaskFailedToBackupAudioFile": "Не ўдалося зрабіць рэзервовую копію аўдыяфайла \"{0}\"", + "MessageTaskFailedToCreateCacheDirectory": "Не ўдалося стварыць каталог кэшу", + "MessageTaskFailedToEmbedMetadataInFile": "Не ўдалося ўбудаваць метаданыя ў файл \"{0}\"", + "MessageTaskFailedToMergeAudioFiles": "Не ўдалося аб’яднаць аўдыяфайлы", "MessageTaskFailedToMoveM4bFile": "Не ўдалося перамясціць файл m4b", - "MessageTaskFailedToWriteMetadataFile": "Не ўдалося захаваць файл метададзеных", + "MessageTaskFailedToWriteMetadataFile": "Не ўдалося захаваць файл метаданых", "MessageTaskMatchingBooksInLibrary": "Пошук супадзенняў кніг у бібліятэцы \"{0}\"", "MessageTaskNoFilesToScan": "Няма файлаў для сканавання", "MessageTaskOpmlImport": "Імпарт OPML", @@ -681,8 +924,8 @@ "MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху", "MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст", "MessageTaskOpmlImportFinished": "Дададзена {0} падкастаў", - "MessageTaskOpmlParseFailed": "Не ўдалося разабраць файл OPML", - "MessageTaskOpmlParseFastFail": "Неправільны файл OPML: тэг не знойдзены АБО тэг не знойдзены", + "MessageTaskOpmlParseFailed": "Не ўдалося прааналізаваць файл OPML", + "MessageTaskOpmlParseFastFail": "Памылковы файл OPML: тэг не знойдзены АБО тэг не знойдзены", "MessageTaskOpmlParseNoneFound": "У файле OPML не знойдзена стужак", "MessageTaskScanItemsAdded": "{0} дададзена", "MessageTaskScanItemsMissing": "{0} адсутнічае", @@ -691,35 +934,133 @@ "MessageTaskScanningFileChanges": "Сканіраванне змяненняў у файле \"{0}\"", "MessageTaskScanningLibrary": "Сканіраванне бібліятэкі \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "Мэтавы каталог недаступны для запісу", + "MessageThinking": "Думаю...", + "MessageUploaderItemFailed": "Не ўдалося запампаваць", + "MessageUploaderItemSuccess": "Паспяхова запампавана!", + "MessageUploading": "Запампоўванне...", + "MessageValidCronExpression": "Карэктны выраз cron", + "MessageWatcherIsDisabledGlobally": "Адсочванне змен у папках адключана глабальна ў наладах сервера", + "MessageXLibraryIsEmpty": "{0} Бібліятэка пустая!", + "MessageYourAudiobookDurationIsLonger": "Працягласць аўдыякнігі большая за знойдзеную працягласць", + "MessageYourAudiobookDurationIsShorter": "Працягласць аўдыякнігі карацейшая за знойдзеную працягласць", + "NoteChangeRootPassword": "Толькі карыстальнік root можа мець пусты пароль", "NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.", + "NoteFolderPicker": "Заўвага: ужо супастаўленыя папкі адлюстроўвацца не будуць", "NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS", - "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", - "NoteUploaderFoldersWithMediaFiles": "Тэчкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.", - "NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца", - "PlaceholderNewPlaylist": "Імя новага спіса прайгравання", - "StatsBooksFinished": "кнігі скончаны", - "StatsBooksFinishedThisYear": "Некаторыя кнігі скончаны ў гэтым годзе…", - "StatsBooksListenedTo": "кнігі, якія былі праслуханы", + "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш выпускаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", + "NoteUploaderFoldersWithMediaFiles": "Папкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.", + "NoteUploaderOnlyAudioFiles": "Пры запампоўванні толькі аўдыяфайлаў кожны аўдыяфайл будзе апрацоўвацца як асобная аўдыякніга.", + "NoteUploaderUnsupportedFiles": "Файлы, якія не падтрымліваюцца, ігнаруюцца. Пры выбары або выдаленні папкі іншыя файлы, якія не знаходзяцца ў папцы элемента, ігнаруюцца.", + "NotificationOnBackupCompletedDescription": "Спрацоўвае пасля завяршэння рэзервовага капіравання", + "NotificationOnBackupFailedDescription": "Спрацоўвае пры збоі рэзервовага капіравання", + "NotificationOnEpisodeDownloadedDescription": "Спрацоўвае, калі выпуск падкаста аўтаматычна спампоўваецца", + "NotificationOnRSSFeedDisabledDescription": "Спрацоўвае, калі аўтаматычнае спампоўванне выпускаў адключана з-за занадта вялікай колькасці няўдалых спроб", + "NotificationOnRSSFeedFailedDescription": "Спрацоўвае, пры памылцы запыту RSS-стужкі для аўтаматычнага спампоўвання выпуску", + "NotificationOnTestDescription": "Падзея для тэсціравання сістэмы апавяшчэнняў", + "PlaceholderBulkChapterInput": "Увядзіце загаловак раздзела або выкарыстоўвайце нумарацыю (напрыклад, «Выпуск 1», «Раздзел 10», «1.»)", + "PlaceholderNewCollection": "Назва новай калекцыі", + "PlaceholderNewFolderPath": "Шлях да новай папкі", + "PlaceholderNewPlaylist": "Назва новага плэй-ліста", + "PlaceholderSearch": "Пошук..", + "PlaceholderSearchEpisode": "Пошук выпуску...", + "StatsAuthorsAdded": "дададзена аўтараў", + "StatsBooksAdded": "дададзена кніг", + "StatsBooksAdditional": "Некаторыя дапаўненні ўключаюць…", + "StatsBooksFinished": "завершана кніг", + "StatsBooksFinishedThisYear": "Некаторыя кнігі завершаны ў гэтым годзе…", + "StatsBooksListenedTo": "кніг праслухана", "StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…", + "StatsSessions": "сеансаў", + "StatsSpentListening": "праслухана", + "StatsTopAuthor": "ТОП АЎТАР", + "StatsTopAuthors": "ТОП АЎТАРЫ", + "StatsTopGenre": "ТОП ЖАНР", + "StatsTopGenres": "ТОП ЖАНРЫ", + "StatsTopMonth": "ТОП МЕСЯЦ", + "StatsTopNarrator": "ТОП ДЫКТАР", + "StatsTopNarrators": "ТОП ДЫКТАРЫ", + "StatsTotalDuration": "З агульнай працягласцю…", + "StatsYearInReview": "ВЫНІКІ ГОДА", "ToastAccountUpdateSuccess": "Уліковы запіс абноўлены", - "ToastAuthorImageRemoveSuccess": "Выява аўтара выдалена", + "ToastAppriseUrlRequired": "Неабходна ўвесці URL-адрас Apprise", + "ToastAsinRequired": "ASIN абавязковы", + "ToastAuthorImageRemoveSuccess": "Відарыс аўтара выдалены", + "ToastAuthorNotFound": "Аўтар \"{0}\" не знойдзены", + "ToastAuthorRemoveSuccess": "Аўтар выдалены", + "ToastAuthorSearchNotFound": "Аўтар не знойдзены", + "ToastAuthorUpdateMerged": "Аўтар аб'яднаны", "ToastAuthorUpdateSuccess": "Аўтар абноўлены", - "ToastAuthorUpdateSuccessNoImageFound": "Аўтар абноўлены (малюнак не знойдзены)", + "ToastAuthorUpdateSuccessNoImageFound": "Аўтар абноўлены (відарыс не знойдзены)", + "ToastBackupAppliedSuccess": "Рэзервовая копія прыменена", + "ToastBackupCreateFailed": "Не ўдалося стварыць рэзервовую копію", + "ToastBackupCreateSuccess": "Рэзервовая копія створана", + "ToastBackupDeleteFailed": "Не ўдалося выдаліць рэзервовую копію", + "ToastBackupDeleteSuccess": "Рэзервовая копія выдалена", "ToastBackupInvalidMaxKeep": "Няправільная колькасць рэзервовых копій для захоўвання", "ToastBackupInvalidMaxSize": "Няправільны максімальны памер рэзервовай копіі", + "ToastBackupRestoreFailed": "Не ўдалося аднавіць рэзервовую копію", + "ToastBackupUploadFailed": "Не ўдалося запампаваць рэзервовую копію", + "ToastBackupUploadSuccess": "Рэзервовая копія запампавана", + "ToastBatchApplyDetailsToItemsSuccess": "Звесткі прыменены да элементаў", + "ToastBatchDeleteFailed": "Памылка групавога выдалення", + "ToastBatchDeleteSuccess": "Групавое выдаленне выканана", + "ToastBatchQuickMatchFailed": "Памылка групавога хуткага параўнання!", + "ToastBatchQuickMatchStarted": "Групавое хуткае параўнанне {0} кніг запушчана!", + "ToastBatchUpdateFailed": "Памылка групавога абнаўлення", + "ToastBatchUpdateSuccess": "Групавое абнаўленне выканана", "ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку", + "ToastBookmarkCreateSuccess": "Закладка дададзена", + "ToastBookmarkRemoveSuccess": "Закладка выдалена", + "ToastBulkChapterInvalidCount": "Увядзіце лік ад 1 да 150", + "ToastCachePurgeFailed": "Не ўдалося ачысціць кэш", + "ToastCachePurgeSuccess": "Кэш паспяхова ачышчаны", + "ToastChapterLocked": "Раздзел заблакіраваны.", + "ToastChapterStartTimeAdjusted": "Час пачатку раздзела адкарэктаваны на {0} секунд", + "ToastChaptersAllLocked": "Усе раздзелы заблакіраваны. Разблакіруйце некаторыя раздзелы, каб зрушыць іх час.", + "ToastChaptersHaveErrors": "Раздзелы маюць памылкі", + "ToastChaptersInvalidShiftAmountLast": "Памылковая велічыня зруху. Час пачатку апошняга раздзела перавышае працягласць гэтай аўдыякнігі.", + "ToastChaptersInvalidShiftAmountStart": "Памылковая велічыня зруху. Першы раздзел будзе мець нулявую або адмоўную працягласць і будзе перазапісаны другім раздзелам. Павялічце пачатковую працягласць другога раздзела.", + "ToastChaptersMustHaveTitles": "Раздзелы павінны мець загалоўкі", + "ToastChaptersRemoved": "Раздзелы выдалены", + "ToastChaptersUpdated": "Раздзелы абноўлены", + "ToastCollectionItemsAddFailed": "Не ўдалося дадаць элемент(ы) у калекцыю", + "ToastCollectionRemoveSuccess": "Калекцыя выдалена", + "ToastCollectionUpdateSuccess": "Калекцыя абноўлена", + "ToastConnectionNotAvailable": "Падключэнне недаступна. Паспрабуйце яшчэ раз пазней", + "ToastCoverSearchFailed": "Не ўдалося знайсці вокладку", + "ToastCoverUpdateFailed": "Не ўдалося абнавіць вокладку", "ToastDateTimeInvalidOrIncomplete": "Дата і час указаны некарэктна або не цалкам", + "ToastDeleteFileFailed": "Не ўдалося выдаліць файл", + "ToastDeleteFileSuccess": "Файл выдалены", + "ToastDeviceAddFailed": "Не ўдалося дадаць прыладу", + "ToastDeviceNameAlreadyExists": "Прылада для чытання электронных кніг з такой назвай ужо існуе", "ToastDeviceTestEmailFailed": "Не ўдалося адправіць тэставае электроннае пісьмо", + "ToastDeviceTestEmailSuccess": "Тэставы электронны ліст адпраўлены", + "ToastEmailSettingsUpdateSuccess": "Налады электроннай пошты абноўлены", "ToastEncodeCancelFailed": "Не ўдалося скасаваць кадаванне", "ToastEncodeCancelSucces": "Кадаванне скасавана", "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", - "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана", - "ToastInvalidImageUrl": "Няправільны URL выявы", - "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі", - "ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана", - "ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як Завершаны", - "ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае", - "ToastItemMarkedAsNotFinishedSuccess": "Элемент пазначаны як Незавершаны", + "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўвання выпускаў ачышчана", + "ToastEpisodeUpdateSuccess": "Абноўлена выпускаў: {0}", + "ToastErrorCannotShare": "Немагчыма абагуліць на гэтай прыладзе", + "ToastFailedToCreate": "Не ўдалося стварыць", + "ToastFailedToDelete": "Не ўдалося выдаліць", + "ToastFailedToLoadData": "Не ўдалося загрузіць даныя", + "ToastFailedToMatch": "Не атрымалася знайсці супадзенне", + "ToastFailedToShare": "Не ўдалося абагуліць", + "ToastFailedToUpdate": "Не здалося абнавіць", + "ToastInvalidImageUrl": "Памылковы URL-адрас відарыса", + "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць выпускаў для спампоўвання", + "ToastInvalidUrl": "Памылковы URL-адрас", + "ToastInvalidUrls": "Адзін або некалькі URL-адрасоў памылковыя", + "ToastItemCoverUpdateSuccess": "Вокладка элемента абноўлена", + "ToastItemDeletedFailed": "Не ўдалося выдаліць элемент", + "ToastItemDeletedSuccess": "Выдалены элемент", + "ToastItemDetailsUpdateSuccess": "Звесткі элемента абноўлены", + "ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як завершаны", + "ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як завершаны", + "ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як незавершаны", + "ToastItemMarkedAsNotFinishedSuccess": "Элемент пазначаны як незавершаны", "ToastItemUpdateSuccess": "Элемент абноўлены", "ToastLibraryCreateFailed": "Не ўдалося стварыць бібліятэку", "ToastLibraryCreateSuccess": "Бібліятэка \"{0}\" створана", @@ -736,24 +1077,89 @@ "ToastMustHaveAtLeastOnePath": "Павінен быць хаця б адзін шлях", "ToastNameEmailRequired": "Імя і электронная пошта абавязковыя", "ToastNameRequired": "Імя абавязковае", + "ToastNewApiKeyUserError": "Трэба выбраць карыстальніка", + "ToastNewEpisodesFound": "Знойдзена новых выпускаў: {0}", "ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"", "ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны", + "ToastNewUserLibraryError": "Трэба выбраць хаця б адну бібліятэку", + "ToastNewUserPasswordError": "Мусіць мець пароль, толькі карыстальнік root можа мець пусты пароль", + "ToastNewUserTagError": "Трэбаа выбраць хаця б адзін тэг", + "ToastNewUserUsernameError": "Увядзіце імя карыстальніка", + "ToastNoNewEpisodesFound": "Новых выпускаў не знойдзена", "ToastNoRSSFeed": "У падкаста няма RSS-стужкі", - "ToastPlaylistCreateFailed": "Не ўдалося стварыць спіс прайгравання", - "ToastPlaylistCreateSuccess": "Спіс прайгравання створаны", - "ToastPlaylistRemoveSuccess": "Спіс прайгравання выдалены", - "ToastPlaylistUpdateSuccess": "Спіс прайгравання абноўлены", + "ToastNoUpdatesNecessary": "Абнаўленні не патрэбныя", + "ToastNotificationCreateFailed": "Не ўдалося стварыць апавяшчэнне", + "ToastNotificationDeleteFailed": "Не ўдалося выдаліць апавяшчэнне", + "ToastNotificationFailedMaximum": "Максімальная колькасць няўдалых спроб павінна быць >= 0", + "ToastNotificationQueueMaximum": "Максімальная чарга апавяшчэнняў павінна быць >= 0", + "ToastNotificationSettingsUpdateSuccess": "Налады апавяшчэнняў абноўлены", + "ToastNotificationTestTriggerFailed": "Не ўдалося ўключыць тэставае апавяшчэнне", + "ToastNotificationTestTriggerSuccess": "Уключана тэставае апавяшчэнне", + "ToastNotificationUpdateSuccess": "Апавяшчэнне абноўлена", + "ToastPlaylistCreateFailed": "Не ўдалося стварыць плэй-ліст", + "ToastPlaylistCreateSuccess": "Плэй-ліст створаны", + "ToastPlaylistRemoveSuccess": "Плэй-ліст выдалены", + "ToastPlaylistUpdateSuccess": "Плэй-ліст абноўлены", "ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст", "ToastPodcastCreateSuccess": "Падкаст паспяхова створаны", + "ToastPodcastEpisodeUpdated": "Выпуск абноўлены", "ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста", - "ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў", + "ToastPodcastNoEpisodesInFeed": "У RSS-ленце не знойдзена выпускаў", "ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі", + "ToastProgressIsNotBeingSynced": "Прагрэс не сінхранізуецца, перазапусціце прайграванне", + "ToastProviderCreatedFailed": "Не ўдалося дадаць пастаўшчыка", + "ToastProviderCreatedSuccess": "Новы пастаўшчык дададзены", + "ToastProviderNameAndUrlRequired": "Назва і URL-адрас абавязковыя", + "ToastProviderRemoveSuccess": "Пастаўшчык выдалены", "ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку", "ToastRSSFeedCloseSuccess": "RSS-стужка закрыта", + "ToastRemoveFailed": "Не ўдалося выдаліць", + "ToastRemoveItemFromCollectionFailed": "Не ўдалося выдаліць элемент з калекцыі", + "ToastRemoveItemFromCollectionSuccess": "Элемент выдалены з калекцыі", + "ToastRemoveItemsWithIssuesFailed": "Не ўдалося выдаліць элементы бібліятэкі з праблемамі", + "ToastRemoveItemsWithIssuesSuccess": "Выдалены элементы бібліятэкі з праблемамі", + "ToastRenameFailed": "Не ўдалося перайменаваць", + "ToastRescanFailed": "Не ўдалося паўторна прасканіраваць {0}", + "ToastRescanRemoved": "Паўторнае сканаванне завершана, элемент быў выдалены", + "ToastRescanUpToDate": "Паўторнае сканаванне завершана, элемент быў у актуальным стане", + "ToastRescanUpdated": "Паўторнае сканаванне завершана, элемент быў абноўлены", + "ToastScanFailed": "Не ўдалося адсканіраваць элемент бібліятэкі", + "ToastSelectAtLeastOneUser": "Выберыце прынамсі аднаго карыстальніка", "ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу", "ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Немагчыма дадаць дзве серыі з аднолькавай назвай", + "ToastSeriesUpdateFailed": "Не ўдалося абнавіць серыі", + "ToastSeriesUpdateSuccess": "Серыі абноўлены", + "ToastServerSettingsUpdateSuccess": "Налады сервера абноўлены", + "ToastSessionCloseFailed": "Не ўдалося закрыць сеанс", + "ToastSessionDeleteFailed": "Не ўдалося выдаліць сеанс", + "ToastSessionDeleteSuccess": "Сеанс выдалены", "ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р", - "ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.", + "ToastSlugMustChange": "Ідэнтыфікатар змяшчае недапушчальныя сімвалы", + "ToastSlugRequired": "Ідэнтыфікатар абавязковы", + "ToastSocketConnected": "Сокет падключаны", + "ToastSocketDisconnected": "Сокет адключаны", + "ToastSocketFailedToConnect": "Не ўдалося падключыць сокет", + "ToastSortingPrefixesEmptyError": "Мусіць мець хаця б адзін прэфікс сартавання", + "ToastSortingPrefixesUpdateSuccess": "Прэфіксы сартавання абноўлены ({0} элементаў)", + "ToastTitleRequired": "Загаловак абавязковы", + "ToastUnknownError": "Невядомая памылка", + "ToastUnlinkOpenIdFailed": "Не ўдалося адвязаць карыстальніка ад OpenID", + "ToastUnlinkOpenIdSuccess": "Карыстальнік адвязаны ад OpenID", + "ToastUploaderFilepathExistsError": "Файл \"{0}\" ужо існуе на серверы", + "ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху запампоўкі.", + "ToastUserDeleteFailed": "Не ўдалося выдаліць карыстальніка", + "ToastUserDeleteSuccess": "Карыстальнік выдалены", + "ToastUserPasswordChangeSuccess": "Пароль паспяхова зменены", + "ToastUserPasswordMismatch": "Паролі не супадаюць", "ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым", - "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара" + "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара", + "TooltipAddChapters": "Дадаць раздзел(ы)", + "TooltipAddOneSecond": "Дадаць 1 секунду", + "TooltipAdjustChapterStart": "Націсніце, каб адкарэкціраваць час пачатку", + "TooltipLockAllChapters": "Заблакіраваць усе раздзелы", + "TooltipLockChapter": "Заблакіраваць раздзел (Shift+націсканне для дыяпазону)", + "TooltipSubtractOneSecond": "Адняць 1 секунду", + "TooltipUnlockAllChapters": "Разблакіраваць усе раздзелы", + "TooltipUnlockChapter": "Разблакіраваць раздзел (Shift+націсканне для выбару дыяпазону)" } diff --git a/client/strings/bg.json b/client/strings/bg.json index dacd62087..460f0ff83 100644 --- a/client/strings/bg.json +++ b/client/strings/bg.json @@ -436,7 +436,7 @@ "LabelLibraryFilterSublistEmpty": "Не {0}", "LabelLibraryItem": "Елемент на Библиотека", "LabelLibraryName": "Име на Библиотека", - "LabelLibrarySortByProgress": "Прогрес: Последно Обновен", + "LabelLibrarySortByProgress": "Прогрес: Последно обновление", "LabelLibrarySortByProgressFinished": "Прогрес: Приключено", "LabelLibrarySortByProgressStarted": "Прогрес: Започнато", "LabelLimit": "Лимит", @@ -892,7 +892,7 @@ "MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}", "MessageSearchResultsFor": "Резултати от търсенето за", "MessageSelected": "{0} избрани", - "MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации.", + "MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации", "MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат", "MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла", "MessageShareExpirationWillBe": "Изтичането ще бъде на {0}", @@ -956,6 +956,8 @@ "NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод", "NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити", "NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод", + "NotificationOnTestDescription": "Event за тестване на системата за нотификации", + "PlaceholderBulkChapterInput": "Въведете име на глава или използвайте номериране (прим. 'Епизод 1', 'Глава 10', '1.')", "PlaceholderNewCollection": "Ново име на колекцията", "PlaceholderNewFolderPath": "Нов път на папката", "PlaceholderNewPlaylist": "Ново име на плейлиста", @@ -963,26 +965,58 @@ "PlaceholderSearchEpisode": "Търсене на Епизоди...", "StatsAuthorsAdded": "добаврени автори", "StatsBooksAdded": "добавени книги", + "StatsBooksAdditional": "Някой от вкючените добавки…", "StatsBooksFinished": "завършени книги", + "StatsBooksFinishedThisYear": "Някой от книгите приключени тази година…", + "StatsBooksListenedTo": "слушани книги", + "StatsCollectionGrewTo": "Твоята книжна колекция израсна до…", + "StatsSessions": "сесии", + "StatsSpentListening": "прекарано в слушане", + "StatsTopAuthor": "ТОП АВТОР", + "StatsTopAuthors": "ТОП АВТОРИ", + "StatsTopGenre": "ТОП ЖАНР", + "StatsTopGenres": "ТОП ЖАНРА", + "StatsTopMonth": "ТОП МЕСЕЦ", + "StatsTopNarrator": "ТОП РАЗКАЗВАЧ", + "StatsTopNarrators": "ТОП РАЗКАЗВАЧИ", + "StatsTotalDuration": "С пълно времетраене…", + "StatsYearInReview": "ГОДИНАТА В ПРЕГЛЕД", "ToastAccountUpdateSuccess": "Успешно обновяване на акаунта", + "ToastAppriseUrlRequired": "Трябва да въведете Apprise URL", + "ToastAsinRequired": "ASIN-а е задължителен", "ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната", + "ToastAuthorNotFound": "Автор \"{0}\" не е намерен", + "ToastAuthorRemoveSuccess": "Арторът е премахнат", + "ToastAuthorSearchNotFound": "Авторът не е намерен", "ToastAuthorUpdateMerged": "Обновяване на автора сливано", "ToastAuthorUpdateSuccess": "Автора обновен", "ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)", + "ToastBackupAppliedSuccess": "Архивът е приложен", "ToastBackupCreateFailed": "Неуспешно създаване на архив", "ToastBackupCreateSuccess": "Архивът е създаден", "ToastBackupDeleteFailed": "Неуспешно изтриване на архив", "ToastBackupDeleteSuccess": "Архивът е изтрит", + "ToastBackupInvalidMaxKeep": "Невалиден брой за архиви за запазване", + "ToastBackupInvalidMaxSize": "Невалиден максимален рамер на архив", "ToastBackupRestoreFailed": "Неуспешно възстановяване на архив", "ToastBackupUploadFailed": "Неуспешно качване на архив", "ToastBackupUploadSuccess": "Архивът е качен", + "ToastBatchApplyDetailsToItemsSuccess": "Детайли приложени на предмети", + "ToastBatchDeleteFailed": "Груповото изтриване се провали", + "ToastBatchDeleteSuccess": "Успешно групово изтриване", + "ToastBatchQuickMatchFailed": "Груповото Бързо Съвпадение се провали!", + "ToastBatchQuickMatchStarted": "Груповото Бързо Съвпадение на {0} книги започна!", "ToastBatchUpdateFailed": "Неуспешно групово актуализиране", "ToastBatchUpdateSuccess": "Успешно групово актуализиране", "ToastBookmarkCreateFailed": "Неуспешно създаване на отметка", "ToastBookmarkCreateSuccess": "Отметката е създадена", "ToastBookmarkRemoveSuccess": "Отметката е премахната", + "ToastBulkChapterInvalidCount": "Въведете число между 1 и 150", "ToastCachePurgeFailed": "Неуспешно изчистване на кеша", "ToastCachePurgeSuccess": "Успешно изчистване на кеша", + "ToastChapterLocked": "Главата е заключена.", + "ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди", + "ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.", "ToastChaptersHaveErrors": "Главите имат грешки", "ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия", "ToastCollectionRemoveSuccess": "Колекцията е премахната", diff --git a/client/strings/ca.json b/client/strings/ca.json index dbda8b8e4..4180b2518 100644 --- a/client/strings/ca.json +++ b/client/strings/ca.json @@ -166,6 +166,7 @@ "HeaderMetadataOrderOfPrecedence": "Ordre de Precedència de Metadades", "HeaderMetadataToEmbed": "Metadades a Inserir", "HeaderNewAccount": "Nou Compte", + "HeaderNewApiKey": "Nova clau API", "HeaderNewLibrary": "Nova Biblioteca", "HeaderNotificationCreate": "Crea Notificació", "HeaderNotificationUpdate": "Actualització de Notificació", @@ -199,6 +200,7 @@ "HeaderSettingsExperimental": "Funcionalitats experimentals", "HeaderSettingsGeneral": "Generals", "HeaderSettingsScanner": "Escàner", + "HeaderSettingsSecurity": "Seguretat", "HeaderSettingsWebClient": "Client web", "HeaderSleepTimer": "Temporitzador de son", "HeaderStatsLargestItems": "Elements més grans", @@ -421,6 +423,9 @@ "LabelLibraryFilterSublistEmpty": "Sense {0}", "LabelLibraryItem": "Element de Biblioteca", "LabelLibraryName": "Nom de Biblioteca", + "LabelLibrarySortByProgress": "Progrés: Última actualització", + "LabelLibrarySortByProgressFinished": "Progrés: Finalitzat", + "LabelLibrarySortByProgressStarted": "Progrés: Començat", "LabelLimit": "Límits", "LabelLineSpacing": "Interlineat", "LabelListenAgain": "Escoltar de nou", @@ -443,7 +448,7 @@ "LabelMetadataProvider": "Proveïdor de metadades", "LabelMinute": "Minut", "LabelMinutes": "Minuts", - "LabelMissing": "Absent", + "LabelMissing": "Falta", "LabelMissingEbook": "No té llibre electrònic", "LabelMissingSupplementaryEbook": "No té ebook complementari", "LabelMobileRedirectURIs": "URI de redirecció mòbil permeses", diff --git a/client/strings/cs.json b/client/strings/cs.json index f70d6693c..49b0d2bd7 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -385,7 +385,7 @@ "LabelFontBoldness": "Výraznost písma", "LabelFontFamily": "Rodina písem", "LabelFontItalic": "Kurzíva", - "LabelFontScale": "Měřítko písma", + "LabelFontScale": "Velikost písma", "LabelFontStrikethrough": "Přeškrtnutí", "LabelFormat": "Formát", "LabelFull": "Plné", diff --git a/client/strings/da.json b/client/strings/da.json index ab1444970..6cf517fa3 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -127,6 +127,7 @@ "HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer", "HeaderAuthentication": "Autentificering", "HeaderBackups": "Sikkerhedskopier", + "HeaderBulkChapterModal": "Tilføj flere kapitler", "HeaderChangePassword": "Skift Adgangskode", "HeaderChapters": "Kapitler", "HeaderChooseAFolder": "Vælg en Mappe", @@ -199,6 +200,7 @@ "HeaderSettingsExperimental": "Eksperimentelle Funktioner", "HeaderSettingsGeneral": "Generelt", "HeaderSettingsScanner": "Scanner", + "HeaderSettingsSecurity": "Sikkerhed", "HeaderSettingsWebClient": "Webklient", "HeaderSleepTimer": "Søvntimer", "HeaderStatsLargestItems": "Største Elementer", @@ -273,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Bøger", "LabelButtonText": "Knap tekst", - "LabelByAuthor": "af {0}", + "LabelByAuthor": "Efter Forfatter", "LabelChangePassword": "Ændre Adgangskode", "LabelChannels": "Kanaler", "LabelChapterCount": "{0} Kapitler", @@ -293,6 +295,7 @@ "LabelContinueListening": "Fortsæt med at lytte", "LabelContinueReading": "Fortsæt med at læse", "LabelContinueSeries": "Fortsæt Serien", + "LabelCorsAllowed": "Tilladte CORS-oprindelser", "LabelCover": "Omslag", "LabelCoverImageURL": "Omslagsbillede URL", "LabelCoverProvider": "Cover billede udbyder", @@ -306,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "Slet fra filsystem (afmarker kun for at fjerne fra databasen)", "LabelDescription": "Beskrivelse", "LabelDeselectAll": "Fravælg Alle", + "LabelDetectedPattern": "Identificeret mønster:", "LabelDevice": "Enheds", "LabelDeviceInfo": "Enhedsinformation", "LabelDeviceIsAvailableTo": "Enhed er tilgængelig for...", @@ -374,11 +378,12 @@ "LabelFilterByUser": "Filtrér efter bruger", "LabelFindEpisodes": "Find episoder", "LabelFinished": "Færdig", + "LabelFinishedDate": "Færdig {0}", "LabelFolder": "Mappe", "LabelFolders": "Mapper", "LabelFontBold": "Fed", "LabelFontBoldness": "Skrift tykkelse", - "LabelFontFamily": "Fontfamilie", + "LabelFontFamily": "Skrifttypefamilie", "LabelFontItalic": "Kursiv", "LabelFontScale": "Skriftstørrelse", "LabelFontStrikethrough": "Gennemstreget", @@ -418,6 +423,7 @@ "LabelLanguages": "Sprog", "LabelLastBookAdded": "Senest tilføjede bog", "LabelLastBookUpdated": "Senest opdaterede bog", + "LabelLastProgressDate": "Sidste fremgang: {0}", "LabelLastSeen": "Sidst set", "LabelLastTime": "Sidste gang", "LabelLastUpdate": "Seneste opdatering", @@ -430,6 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Nej {0}", "LabelLibraryItem": "Bibliotekselement", "LabelLibraryName": "Biblioteksnavn", + "LabelLibrarySortByProgress": "Fremgang: Sidst opdateret", + "LabelLibrarySortByProgressFinished": "Fremgang: Afsluttet", + "LabelLibrarySortByProgressStarted": "Fremgang: Startet", "LabelLimit": "Grænse", "LabelLineSpacing": "Linjeafstand", "LabelListenAgain": "Lyt igen", @@ -438,6 +447,7 @@ "LabelLogLevelWarn": "Advarsel", "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", "LabelLowestPriority": "Laveste prioritet", + "LabelMatchConfidence": "Confidens", "LabelMatchExistingUsersBy": "Match eksisterende brugere ved", "LabelMatchExistingUsersByDescription": "Anvendt for at forbinde brugere. Når forbundet, brugere vil blive matchet ved unikt id fra din SSO udbyder", "LabelMaxEpisodesToDownload": "Max # afsnit for at downloade. Anvend 0 for ubegrænset.", @@ -467,6 +477,7 @@ "LabelNewestAuthors": "Nyeste forfattere", "LabelNewestEpisodes": "Nyeste episoder", "LabelNextBackupDate": "Næste sikkerhedskopi dato", + "LabelNextChapters": "Næste kapitler vil være:", "LabelNextScheduledRun": "Næste planlagte kørsel", "LabelNoApiKeys": "Ingen API-nøgler", "LabelNoCustomMetadataProviders": "Ingen brugerdefinerede metadata udbydere", @@ -484,6 +495,7 @@ "LabelNotificationsMaxQueueSize": "Maksimal køstørrelse for meddelelseshændelser", "LabelNotificationsMaxQueueSizeHelp": "Hændelser begrænses til at udløse en gang pr. sekund. Hændelser ignoreres, hvis køen er fyldt. Dette forhindrer meddelelsesspam.", "LabelNumberOfBooks": "Antal bøger", + "LabelNumberOfChapters": "Antal kapitler:", "LabelNumberOfEpisodes": "# afsnit", "LabelOpenIDAdvancedPermsClaimDescription": "Navnet af OpenID claimet som indeholder avancerede brugerhandlinger inden i applikationen som vil gælde for ikke administrative roller (hvis konfigureret). Hvis et claim mangler fra svaret vil adgang til ABS blive nægtet. Hvis en enkelt indstilling/option mangler, vil det bliver behandlet som false. Sørg for at identity provider's claim matcher den forventede struktur:", "LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avanceret gruppe og adgangsindstilling, ved automatisk at assigne 'Bruger' grupper.", @@ -576,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder", "LabelSettingsChromecastSupport": "Chromecast-understøttelse", "LabelSettingsDateFormat": "Datoformat", - "LabelSettingsEnableWatcher": "Scan automatisk bibliotek for ændringer", - "LabelSettingsEnableWatcherForLibrary": "Scan automatisk bibliotek for ændringer", + "LabelSettingsEnableWatcher": "Automatisk biblioteksovervåger", + "LabelSettingsEnableWatcherForLibrary": "Automatisk biblioteksovervåger", "LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart", "LabelSettingsEpubsAllowScriptedContent": "Tillad scriptet indhold i epub", "LabelSettingsEpubsAllowScriptedContentHelp": "Tillad epub filer at køre scripts. Det anbefales at holde denne indstilling deaktiveret med mindre du stoler på kilderne af epub filerne.", @@ -626,6 +638,7 @@ "LabelStartTime": "Starttid", "LabelStarted": "Startet", "LabelStartedAt": "Startet klokken", + "LabelStartedDate": "Startet {0}", "LabelStatsAudioTracks": "Lydspor", "LabelStatsAuthors": "Forfattere", "LabelStatsBestDay": "Bedste dag", @@ -655,6 +668,7 @@ "LabelTheme": "Tema", "LabelThemeDark": "Mørk", "LabelThemeLight": "Lys", + "LabelThemeSepia": "Sepia", "LabelTimeBase": "Tidsbase", "LabelTimeDurationXHours": "{0} timer", "LabelTimeDurationXMinutes": "{0} minutter", @@ -739,6 +753,7 @@ "MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Intet resultat for query", "MessageBookshelfNoSeries": "Du har ingen serier", + "MessageBulkChapterPattern": "Hvor mange kapitler vil du tilføje med dette nummereringsmønster?", "MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog", "MessageChapterErrorFirstNotZero": "Første kapitel skal starte ved 0", "MessageChapterErrorStartGteDuration": "Ugyldig starttid skal være mindre end lydbogens varighed", @@ -775,6 +790,7 @@ "MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?", "MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?", "MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Obs: Dette sletter ikke lydfilen medmindre \"Permanent sletning af fil\" er aktiveret", "MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?", "MessageConfirmRemoveListeningSessions": "Er du sikker på at du vil fjerne {0} lytte sessioner?", "MessageConfirmRemoveMetadataFiles": "Er du sikker på at du vil fjerne alle metadata.{0} filer i dine biblioteksfoldere?", @@ -800,6 +816,8 @@ "MessageFeedURLWillBe": "Feed-URL vil være {0}", "MessageFetching": "Henter...", "MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.", + "MessageHeatmapListeningTimeTooltip": "{0} lytter på {1}", + "MessageHeatmapNoListeningSessions": "Ingen lyttesessioner på {0}", "MessageImportantNotice": "Vigtig besked!", "MessageInsertChapterBelow": "Indsæt kapitel nedenfor", "MessageInvalidAsin": "Ugyldig ASIN", @@ -870,7 +888,7 @@ "MessageResetChaptersConfirm": "Er du sikker på, at du vil nulstille kapitler og annullere ændringerne, du har foretaget?", "MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den", "MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.

Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.

Alle klienter, der bruger din server, opdateres automatisk.", - "MessageScheduleLibraryScanNote": "For de fleste brugere, er det anbefalet at efterlade denne funktion deaktiveret for at holde mappe lurer indstilling aktiveret. Mappe lureren vil automatisk opdage ændringer i biblioteksmapper. Mappe lureren virker ikke for alle filsystemer (så som NFS) så schedulerede biblioteksscans vil blive anvendt.", + "MessageScheduleLibraryScanNote": "For de fleste brugere er det anbefalet, at efterlade denne funktion deaktiveret, og lade biblioteksovervågeren være aktiveret - den vil automatisk opdage ændringer i dine biblioteksmapper. Aktiver denne funktion, hvis biblioteksovervågeren ikke virker med dit filsystem (f. eks. NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Kør hvert {0} af {1}", "MessageSearchResultsFor": "Søgeresultater for", "MessageSelected": "{0} valgt", @@ -939,6 +957,7 @@ "NotificationOnRSSFeedDisabledDescription": "Aktiveret når automatiske episode-downloads er slået fra, på grund af for mange forsøg", "NotificationOnRSSFeedFailedDescription": "Aktiveret når anmodning om RSS-feedet fejler for en automatisk episode-download", "NotificationOnTestDescription": "Event for test af notifikationssystemet", + "PlaceholderBulkChapterInput": "Indtast kapiteltitel eller brug nummerering (f.eks. 'Episode 1', 'Kapitel 10', '1.')", "PlaceholderNewCollection": "Nyt samlingnavn", "PlaceholderNewFolderPath": "Ny mappes sti", "PlaceholderNewPlaylist": "Nyt afspilningslistnavn", @@ -992,9 +1011,15 @@ "ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke", "ToastBookmarkCreateSuccess": "Bogmærke tilføjet", "ToastBookmarkRemoveSuccess": "Bogmærke fjernet", + "ToastBulkChapterInvalidCount": "Indtast et tal mellem 1 og 150", "ToastCachePurgeFailed": "Fejlede at opryde cache", "ToastCachePurgeSuccess": "Cache ryddet op i succesfuldt", + "ToastChapterLocked": "Kapitel er låst.", + "ToastChapterStartTimeAdjusted": "Kapitelstarttid justeret med {0} sekunder", + "ToastChaptersAllLocked": "Alle kapitler er låst. Lås op for nogle kapitler for at ændre deres tider.", "ToastChaptersHaveErrors": "Kapitler har fejl", + "ToastChaptersInvalidShiftAmountLast": "Ugyldig ændring. Det sidste kapitels starttid ville fortsætte længere end varigheden på denne lydbog.", + "ToastChaptersInvalidShiftAmountStart": "Ugyldig ændring. Første kapitel ville have en længde på nul eller negativt og ville blive overskrevet af andet kapitel. Udvid startvarigheden på andet kapitel.", "ToastChaptersMustHaveTitles": "Kapitler skal have titler", "ToastChaptersRemoved": "Kapitler fjernet", "ToastChaptersUpdated": "Kapitler opdateret", @@ -1002,6 +1027,7 @@ "ToastCollectionRemoveSuccess": "Samling fjernet", "ToastCollectionUpdateSuccess": "Samling opdateret", "ToastConnectionNotAvailable": "Forbindelse mislykkedes. Prøv igen senere", + "ToastCoverSearchFailed": "Cover-søgning mislykkedes", "ToastCoverUpdateFailed": "Cover opdatering fejlede", "ToastDateTimeInvalidOrIncomplete": "Dato og tid er ugyldig eller ufærdig", "ToastDeleteFileFailed": "Sletning af fil fejlede", @@ -1051,6 +1077,7 @@ "ToastMustHaveAtLeastOnePath": "Skal have mindst en sti", "ToastNameEmailRequired": "Navn og email påkrævet", "ToastNameRequired": "Navn påkrævet", + "ToastNewApiKeyUserError": "En bruger skal vælges", "ToastNewEpisodesFound": "{0} nye afsnit fundet", "ToastNewUserCreatedFailed": "Fejlede at oprette konto: \"{0}\"", "ToastNewUserCreatedSuccess": "Ny konto oprettet", @@ -1075,6 +1102,7 @@ "ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret", "ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast", "ToastPodcastCreateSuccess": "Podcast oprettet med succes", + "ToastPodcastEpisodeUpdated": "Episode opdateret", "ToastPodcastGetFeedFailed": "Fejlede at hente podcast feed", "ToastPodcastNoEpisodesInFeed": "Ingen nye afsnit fundet i RSS feed", "ToastPodcastNoRssFeed": "Podcast har ingen RSS feed", @@ -1125,5 +1153,13 @@ "ToastUserPasswordChangeSuccess": "Password ændret", "ToastUserPasswordMismatch": "Passwords passer ikke sammen", "ToastUserPasswordMustChange": "Nyt password må ikke være det gamle", - "ToastUserRootRequireName": "Skal indholde et root brugernavn" + "ToastUserRootRequireName": "Skal indholde et root brugernavn", + "TooltipAddChapters": "Tilføj kapitler", + "TooltipAddOneSecond": "Tilføj 1 sekund", + "TooltipAdjustChapterStart": "Klik for at ændre starttiden", + "TooltipLockAllChapters": "Lås alle kapitler", + "TooltipLockChapter": "Lås kapitel (Shift+click for at markere flere)", + "TooltipSubtractOneSecond": "Fratag 1 sekund", + "TooltipUnlockAllChapters": "Lås alle kapitaler op", + "TooltipUnlockChapter": "Lås kapitel op (Shift+click for at markere flere)" } diff --git a/client/strings/de.json b/client/strings/de.json index 970c94233..4d9ecd412 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -116,7 +116,7 @@ "ButtonViewAll": "Alles anzeigen", "ButtonYes": "Ja", "ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten", - "ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und/oder den Autor zu aktualisieren.", + "ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden - versuche den Titel und/oder den Autor zu aktualisieren", "ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden", "HeaderAccount": "Konto", "HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen", @@ -331,7 +331,7 @@ "LabelEmail": "E-Mail", "LabelEmailSettingsFromAddress": "Sender", "LabelEmailSettingsRejectUnauthorized": "Nicht autorisierte Zertifikate ablehnen", - "LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn due die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.", + "LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn du die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.", "LabelEmailSettingsSecure": "Sicher", "LabelEmailSettingsSecureHelp": "Wenn an, verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei „aus“ wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf „an“ schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert „aus“ bei. (von nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test-Adresse", @@ -436,7 +436,7 @@ "LabelLibraryFilterSublistEmpty": "Keine {0}", "LabelLibraryItem": "Bibliothekseintrag", "LabelLibraryName": "Bibliotheksname", - "LabelLibrarySortByProgress": "Fortschritt: Zuletzt aktualisiert", + "LabelLibrarySortByProgress": "Fortschritt: Letzte Aktualisierung", "LabelLibrarySortByProgressFinished": "Fortschritt: Beendet", "LabelLibrarySortByProgressStarted": "Fortschritt: Gestartet", "LabelLimit": "Begrenzung", @@ -622,7 +622,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet", "LabelSettingsTimeFormat": "Zeitformat", "LabelShare": "Freigeben", - "LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link, die Dateien des Mediums als ZIP herunterzuladen.", + "LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link die Dateien des Mediums als ZIP herunterzuladen.", "LabelShareOpen": "Freigeben", "LabelShareURL": "Freigabe URL", "LabelShowAll": "Alles anzeigen", @@ -710,7 +710,7 @@ "LabelUploaderDragAndDropFilesOnly": "Dateien per Drag & Drop hierher ziehen", "LabelUploaderDropFiles": "Dateien löschen", "LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie", - "LabelUseAdvancedOptions": "Nutze Erweiterte Optionen", + "LabelUseAdvancedOptions": "Erweiterte Optionen verwenden", "LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUseZeroForUnlimited": "0 für unbegrenzt", @@ -737,7 +737,7 @@ "MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen", "MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von Apprise API laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann.
Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter http://192.168.1.1:8337 läuft, würdest du http://192.168.1.1:8337/notify eingeben.", "MessageAsinCheck": "Stelle sicher, dass die ASIN aus der richtigen Audible Region verwendet wird, nicht Amazon.", - "MessageAuthenticationLegacyTokenWarning": "Alte API tokens werden in Zukunft entfernt. Benutze stattdessen API Keys.", + "MessageAuthenticationLegacyTokenWarning": "Nicht mehr unterstützte API tokens werden in der Zukunft entfernt. Nutze stattdessen API Schlüssel.", "MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.", "MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.", "MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in /metadata/items & /metadata/authors gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.", @@ -816,7 +816,7 @@ "MessageFeedURLWillBe": "Feed-URL wird {0} sein", "MessageFetching": "Wird abgerufen …", "MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.", - "MessageHeatmapListeningTimeTooltip": "{0} auf {1} gehört", + "MessageHeatmapListeningTimeTooltip": "{0} gehört auf {1}", "MessageHeatmapNoListeningSessions": "Keine Hörsitzungen am {0}", "MessageImportantNotice": "Wichtiger Hinweis!", "MessageInsertChapterBelow": "Kapitel unten einfügen", @@ -1103,11 +1103,11 @@ "ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden", "ToastPodcastCreateSuccess": "Podcast erstellt", "ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert", - "ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds", + "ToastPodcastGetFeedFailed": "Fehler beim Abrufen des Podcast Feeds", "ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden", "ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed", "ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet", - "ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters", + "ToastProviderCreatedFailed": "Fehler beim Hinzufügen des Anbieters", "ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt", "ToastProviderNameAndUrlRequired": "Name und URL notwendig", "ToastProviderRemoveSuccess": "Anbieter entfernt", diff --git a/client/strings/el.json b/client/strings/el.json index 881bd971b..2160b6e14 100644 --- a/client/strings/el.json +++ b/client/strings/el.json @@ -100,9 +100,11 @@ "ButtonShiftTimes": "Χρόνοι Μετακίνησης", "ButtonShow": "Εμφάνιση", "ButtonStartM4BEncode": "Έναρξη Κωδικοποίησης M4B", + "ButtonStartMetadataEmbed": "Έναρξη Ενσωμάτωσης Μεταδεδομένων", "ButtonStats": "Στατιστικά", "ButtonSubmit": "Υποβολή", "ButtonTest": "Δοκιμή", + "ButtonUnlinkOpenId": "Αποσύνδεση OpenID", "ButtonUpload": "Μεταφόρτωση", "ButtonUploadBackup": "Μεταφόρτωση Αντιγράφου Ασφαλείας", "ButtonUploadCover": "Μεταφόρτωση Εξωφύλλου", @@ -111,11 +113,17 @@ "ButtonUserEdit": "Επεξεργασίας χρήστη {0}", "ButtonViewAll": "Εμφάνιση Όλων", "ButtonYes": "Ναι", + "ErrorUploadFetchMetadataAPI": "Σφάλμα κατά την ανάκτηση μεταδεδομένων", + "ErrorUploadFetchMetadataNoResults": "Δεν ήταν δυνατή η ανάκτηση των μεταδεδομένων - δοκιμάστε να ενημερώσετε τον τίτλο και/ή τον συγγραφέα", "ErrorUploadLacksTitle": "Πρέπει να έχει τίτλο", "HeaderAccount": "Λογαριασμός", + "HeaderAddCustomMetadataProvider": "Προσθήκη Προσαρμοσμένου Παρόχου Μεταδεδομένων", "HeaderAdvanced": "Για Προχωρημένους", "HeaderApiKeys": "Κλειδιά API", + "HeaderAppriseNotificationSettings": "Ρυθμίσεις Ειδοποιήσεων Apprise", "HeaderAudioTracks": "Κομμάτια Ήχου", + "HeaderAudiobookTools": "Εργαλεία Διαχείρισης Αρχείων Audiobooks", + "HeaderAuthentication": "Αυθεντικοποίηση", "HeaderBackups": "Αντίγραφα Ασφαλείας", "HeaderBulkChapterModal": "Προσθήκη Πολλαπλών Κεφαλαίων", "HeaderChangePassword": "Αλλαγή Κωδικού Πρόσβασης", @@ -216,6 +224,7 @@ "LabelChapters": "Κεφάλαια", "LabelChaptersFound": "κεφάλαια βρέθηκαν", "LabelClosePlayer": "Κλείσιμο αναπαραγωγής", + "LabelCollapseSeries": "Σύμπτυξη Σειράς", "LabelCollection": "Συλλογή", "LabelCollections": "Συλλογές", "LabelComplete": "Ολοκλήρωση", diff --git a/client/strings/es.json b/client/strings/es.json index 4a611b3de..e9655563d 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -20,10 +20,10 @@ "ButtonCheckAndDownloadNewEpisodes": "Comprobar y descargar episodios nuevos", "ButtonChooseAFolder": "Elegir una carpeta", "ButtonChooseFiles": "Elegir archivos", - "ButtonClearFilter": "Quitar filtros", + "ButtonClearFilter": "Vaciar filtro", "ButtonClose": "Cerrar", "ButtonCloseFeed": "Cerrar suministro", - "ButtonCloseSession": "Cerrar la sesión abierta", + "ButtonCloseSession": "Cerrar sesión abierta", "ButtonCollections": "Colecciones", "ButtonConfigureScanner": "Configurar Escáner", "ButtonCreate": "Crear", @@ -33,27 +33,27 @@ "ButtonEdit": "Editar", "ButtonEditChapters": "Editar capítulos", "ButtonEditPodcast": "Editar pódcast", - "ButtonEnable": "Permitir", + "ButtonEnable": "Habilitar", "ButtonFireAndFail": "Ejecutado y fallido", "ButtonFireOnTest": "Activar evento de prueba", "ButtonForceReScan": "Forzar Re-Escaneo", "ButtonFullPath": "Ruta completa", "ButtonHide": "Ocultar", "ButtonHome": "Inicio", - "ButtonIssues": "Problemas", + "ButtonIssues": "Incidencias", "ButtonJumpBackward": "Retroceder", "ButtonJumpForward": "Adelantar", "ButtonLatest": "Más recientes", "ButtonLibrary": "Biblioteca", - "ButtonLogout": "Salir", - "ButtonLookup": "Buscar", + "ButtonLogout": "Cerrar Sesión", + "ButtonLookup": "Averiguar", "ButtonManageTracks": "Gestionar pistas", "ButtonMapChapterTitles": "Asignar Títulos a Capítulos", "ButtonMatchAllAuthors": "Encontrar Todos los Autores", - "ButtonMatchBooks": "Encontrar Libros", + "ButtonMatchBooks": "Cotejar Libros", "ButtonNevermind": "Olvidar", "ButtonNext": "Siguiente", - "ButtonNextChapter": "Siguiente Capítulo", + "ButtonNextChapter": "Siguiente capítulo", "ButtonNextItemInQueue": "El siguiente elemento en cola", "ButtonOk": "Aceptar", "ButtonOpenFeed": "Abrir suministro", @@ -64,26 +64,26 @@ "ButtonPlaying": "Reproduciendo", "ButtonPlaylists": "Listas de reproducción", "ButtonPrevious": "Anterior", - "ButtonPreviousChapter": "Capítulo Anterior", - "ButtonProbeAudioFile": "Examinar archivo de audio", - "ButtonPurgeAllCache": "Purgar toda la antememoria", - "ButtonPurgeItemsCache": "Purgar antememoria de elementos", - "ButtonQueueAddItem": "Añadir a la cola", - "ButtonQueueRemoveItem": "Quitar de la cola", + "ButtonPreviousChapter": "Capítulo anterior", + "ButtonProbeAudioFile": "Sonda del archivo de audio", + "ButtonPurgeAllCache": "Purgar toda la caché", + "ButtonPurgeItemsCache": "Purgar caché de elementos", + "ButtonQueueAddItem": "Añadir a cola", + "ButtonQueueRemoveItem": "Quitar de cola", "ButtonQuickEmbed": "Inserción rápida", - "ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente", - "ButtonQuickMatch": "Encontrar Rápido", + "ButtonQuickEmbedMetadata": "Empotrar metadatos rápidamente", + "ButtonQuickMatch": "Cotejo Rápido", "ButtonReScan": "Re-Escanear", "ButtonRead": "Leer", "ButtonReadLess": "Leer menos", "ButtonReadMore": "Leer más", - "ButtonRefresh": "Actualizar", + "ButtonRefresh": "Recargar", "ButtonRemove": "Quitar", "ButtonRemoveAll": "Quitar todo", "ButtonRemoveAllLibraryItems": "Quitar todos los elementos de la biblioteca", - "ButtonRemoveFromContinueListening": "Quitar de Continuar escuchando", - "ButtonRemoveFromContinueReading": "Quitar de Continuar leyendo", - "ButtonRemoveSeriesFromContinueSeries": "Quitar serie de Continuar serie", + "ButtonRemoveFromContinueListening": "Quitar desde Escucha Continua", + "ButtonRemoveFromContinueReading": "Quitar desde Continuar Leyendo", + "ButtonRemoveSeriesFromContinueSeries": "Quitar Series desde Series Continuas", "ButtonReset": "Restablecer", "ButtonResetToDefault": "Restaurar valores predeterminados", "ButtonRestore": "Restaurar", @@ -92,47 +92,47 @@ "ButtonSaveTracklist": "Guardar lista de pistas", "ButtonScan": "Escanear", "ButtonScanLibrary": "Escanear biblioteca", - "ButtonScrollLeft": "Desplazarse hacia la izquierda", - "ButtonScrollRight": "Desplazarse hacia la derecha", + "ButtonScrollLeft": "Desplazarse a la izquierda", + "ButtonScrollRight": "Desplazarse a la derecha", "ButtonSearch": "Buscar", "ButtonSelectFolderPath": "Seleccionar ruta de carpeta", "ButtonSeries": "Series", - "ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas", + "ButtonSetChaptersFromTracks": "Establecer capítulos según las pistas", "ButtonShare": "Compartir", - "ButtonShiftTimes": "Desplazar Tiempos", + "ButtonShiftTimes": "Veces de Desplazo", "ButtonShow": "Mostrar", "ButtonStartM4BEncode": "Iniciar Codificación M4B", - "ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata", + "ButtonStartMetadataEmbed": "Iniciar Inserción de Metadatos", "ButtonStats": "Estadísticas", - "ButtonSubmit": "Enviar", + "ButtonSubmit": "Entregar", "ButtonTest": "Prueba", - "ButtonUnlinkOpenId": "Desvincular OpenID", - "ButtonUpload": "Cargar", - "ButtonUploadBackup": "Cargar respaldo", - "ButtonUploadCover": "Cargar cubierta", - "ButtonUploadOPMLFile": "Cargar archivo OPML", + "ButtonUnlinkOpenId": "Desenlazar OpenID", + "ButtonUpload": "Subir", + "ButtonUploadBackup": "Subir Respaldo", + "ButtonUploadCover": "Subir Cubierta", + "ButtonUploadOPMLFile": "Subir archivo OPML", "ButtonUserDelete": "Eliminar usuario {0}", "ButtonUserEdit": "Editar usuario {0}", "ButtonViewAll": "Ver todo", "ButtonYes": "Sí", "ErrorUploadFetchMetadataAPI": "Error al recuperar los metadatos", - "ErrorUploadFetchMetadataNoResults": "No se pudieron recuperar los metadatos; pruebe a actualizar el título o autor", - "ErrorUploadLacksTitle": "Debe tener título", + "ErrorUploadFetchMetadataNoResults": "No se pudieron recuperar los metadatos; pruebe a actualizar el título y/o autor", + "ErrorUploadLacksTitle": "Debe tener un título", "HeaderAccount": "Cuenta", "HeaderAddCustomMetadataProvider": "Añadir proveedor de metadatos personalizado", "HeaderAdvanced": "Avanzado", "HeaderApiKeys": "Claves API", - "HeaderAppriseNotificationSettings": "Configuración de notificaciones de Apprise", - "HeaderAudioTracks": "Pistas de audio", + "HeaderAppriseNotificationSettings": "Ajustes de notificaciones de Apprise", + "HeaderAudioTracks": "Pistas de Audio", "HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro", "HeaderAuthentication": "Autenticación", "HeaderBackups": "Respaldos", "HeaderBulkChapterModal": "Añadir Múltiples Capítulos", - "HeaderChangePassword": "Cambiar contraseña", + "HeaderChangePassword": "Cambiar Contraseña", "HeaderChapters": "Capítulos", "HeaderChooseAFolder": "Escoger una Carpeta", "HeaderCollection": "Colección", - "HeaderCollectionItems": "Elementos en la colección", + "HeaderCollectionItems": "Elementos de colección", "HeaderCover": "Cubierta", "HeaderCurrentDownloads": "Descargas actuales", "HeaderCustomMessageOnLogin": "Mensaje personalizado al acceder", @@ -140,49 +140,49 @@ "HeaderDetails": "Detalles", "HeaderDownloadQueue": "Cola de descargas", "HeaderEbookFiles": "Archivos de libros digitales", - "HeaderEmail": "Correo electrónico", - "HeaderEmailSettings": "Configuración de correo electrónico", + "HeaderEmail": "Correo-e", + "HeaderEmailSettings": "Ajustes de correo-e", "HeaderEpisodes": "Episodios", - "HeaderEreaderDevices": "Dispositivos Ereader", - "HeaderEreaderSettings": "Configuración del lector", + "HeaderEreaderDevices": "Dispositivos Lector-e", + "HeaderEreaderSettings": "Ajustes del Lector-e", "HeaderFiles": "Archivos", "HeaderFindChapters": "Buscar capítulos", "HeaderIgnoredFiles": "Archivos ignorados", - "HeaderItemFiles": "Archivos de elementos", - "HeaderItemMetadataUtils": "Utilidades de metadatos de elementos", + "HeaderItemFiles": "Archivos del elemento", + "HeaderItemMetadataUtils": "Utilidades de metadatos del elemento", "HeaderLastListeningSession": "Última sesión de escucha", "HeaderLatestEpisodes": "Episodios más recientes", "HeaderLibraries": "Bibliotecas", "HeaderLibraryFiles": "Archivos de biblioteca", "HeaderLibraryStats": "Estadísticas de biblioteca", - "HeaderListeningSessions": "Sesión", + "HeaderListeningSessions": "Sesiones Listadas", "HeaderListeningStats": "Estadísticas de Tiempo Escuchado", - "HeaderLogin": "Acceder", - "HeaderLogs": "Registros", + "HeaderLogin": "Inicio de Sesión", + "HeaderLogs": "Bitácoras", "HeaderManageGenres": "Gestionar géneros", "HeaderManageTags": "Gestionar etiquetas", "HeaderMapDetails": "Asignar Detalles", - "HeaderMatch": "Encontrar", + "HeaderMatch": "Coincidir", "HeaderMetadataOrderOfPrecedence": "Orden de precedencia de metadatos", - "HeaderMetadataToEmbed": "Metadatos para Insertar", - "HeaderNewAccount": "Cuenta nueva", + "HeaderMetadataToEmbed": "Metadatos para empotrar", + "HeaderNewAccount": "Crear Cuenta", "HeaderNewApiKey": "Nueva clave API", "HeaderNewLibrary": "Biblioteca nueva", - "HeaderNotificationCreate": "Crear notificación", - "HeaderNotificationUpdate": "Notificación de actualización", + "HeaderNotificationCreate": "Crear Notificación", + "HeaderNotificationUpdate": "Notificación de Actualización", "HeaderNotifications": "Notificaciones", "HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect", - "HeaderOpenListeningSessions": "Sesiones públicas de escucha", + "HeaderOpenListeningSessions": "Abrir escucha de sesiones", "HeaderOpenRSSFeed": "Abrir suministro RSS", "HeaderOtherFiles": "Otros archivos", "HeaderPasswordAuthentication": "Autenticación por contraseña", "HeaderPermissions": "Permisos", "HeaderPlayerQueue": "Cola del reproductor", - "HeaderPlayerSettings": "Configuración del reproductor", + "HeaderPlayerSettings": "Ajustes del reproductor", "HeaderPlaylist": "Lista de reproducción", "HeaderPlaylistItems": "Elementos de lista de reproducción", "HeaderPodcastsToAdd": "Pódcast para añadir", - "HeaderPresets": "Preconfiguraciones", + "HeaderPresets": "Preajustes", "HeaderPreviewCover": "Previsualizar cubierta", "HeaderRSSFeedGeneral": "Detalles de RSS", "HeaderRSSFeedIsOpen": "El suministro RSS está abierto", @@ -191,18 +191,18 @@ "HeaderRemoveEpisodes": "Quitar {0} episodios", "HeaderSavedMediaProgress": "Guardar Progreso de Multimedia", "HeaderSchedule": "Horario", - "HeaderScheduleEpisodeDownloads": "Programar descargas automáticas de episodios", - "HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca", + "HeaderScheduleEpisodeDownloads": "Planificador de auto‐descargas de episodios", + "HeaderScheduleLibraryScans": "Planificar Auto‐Escaneo de Biblioteca", "HeaderSession": "Sesión", - "HeaderSetBackupSchedule": "Programar Respaldo", - "HeaderSettings": "Configuración", + "HeaderSetBackupSchedule": "Establecer Planificación de Respaldo", + "HeaderSettings": "Ajustes", "HeaderSettingsDisplay": "Interfaz", - "HeaderSettingsExperimental": "Funcionalidades experimentales", + "HeaderSettingsExperimental": "Características experimentales", "HeaderSettingsGeneral": "Generales", "HeaderSettingsScanner": "Escáner", "HeaderSettingsSecurity": "Seguridad", "HeaderSettingsWebClient": "Cliente web", - "HeaderSleepTimer": "Temporizador de apagado", + "HeaderSleepTimer": "Cronómetro de dormida", "HeaderStatsLargestItems": "Elementos más grandes", "HeaderStatsLongestItems": "Elementos más extensos (h)", "HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)", @@ -233,29 +233,29 @@ "LabelAddToCollectionBatch": "Añadir {0} libros a colección", "LabelAddToPlaylist": "Añadir a lista de reproducción", "LabelAddToPlaylistBatch": "Añadir {0} elementos a lista de reproducción", - "LabelAddedAt": "Añadido", - "LabelAddedDate": "{0} Añadido", + "LabelAddedAt": "Añadido en", + "LabelAddedDate": "Añadido {0}", "LabelAdminUsersOnly": "Solamente usuarios administradores", "LabelAll": "Todos", "LabelAllEpisodesDownloaded": "Todos los episodios descargados", "LabelAllUsers": "Todos los usuarios", "LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados", "LabelAllUsersIncludingGuests": "Todos los usuarios e invitados", - "LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca", - "LabelApiKeyCreated": "La clave de API “{0}” se ha creado con éxito.", + "LabelAlreadyInYourLibrary": "Ya dentro de tu biblioteca", + "LabelApiKeyCreated": "La clave de API “{0}” se ha creado correctamente.", "LabelApiKeyCreatedDescription": "Asegúrate de copiar la clave de API ahora, no la volverás a ver otra vez.", "LabelApiKeyUser": "Actuar en nombre del usuario", "LabelApiKeyUserDescription": "Esta clave de API tendrá los mismos permisos que el usuario al que representa. En los registros se verá como si la solicitud la hubiera hecho el usuario directamente.", - "LabelApiToken": "Token de la API", + "LabelApiToken": "Vale del API", "LabelAppend": "Adjuntar", - "LabelAudioBitrate": "Tasa de bits del audio (por ejemplo, 128k)", + "LabelAudioBitrate": "Tasa de bit del audio (p.ej., 128k)", "LabelAudioChannels": "Canales de audio (1 o 2)", "LabelAudioCodec": "Códec de audio", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (Nombre Apellido)", "LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthors": "Autores", - "LabelAutoDownloadEpisodes": "Descargar episodios automáticamente", + "LabelAutoDownloadEpisodes": "Auto‐Descargar episodios", "LabelAutoFetchMetadata": "Recuperar metadatos automáticamente", "LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.", "LabelAutoLaunch": "Lanzamiento automático", @@ -286,13 +286,13 @@ "LabelClickToUseCurrentValue": "Pulse para utilizar el valor actual", "LabelClosePlayer": "Cerrar reproductor", "LabelCodec": "Codec", - "LabelCollapseSeries": "Colapsar serie", + "LabelCollapseSeries": "Colapsar Series", "LabelCollapseSubSeries": "Contraer la subserie", "LabelCollection": "Colección", "LabelCollections": "Colecciones", "LabelComplete": "Completo", "LabelConfirmPassword": "Confirmar contraseña", - "LabelContinueListening": "Seguir escuchando", + "LabelContinueListening": "Seguir Escuchando", "LabelContinueReading": "Continuar leyendo", "LabelContinueSeries": "Continuar series", "LabelCorsAllowed": "Orígenes CORS Permitidos", @@ -325,14 +325,14 @@ "LabelDurationComparisonLonger": "({0} más largo)", "LabelDurationComparisonShorter": "({0} más corto)", "LabelDurationFound": "Duración Comprobada:", - "LabelEbook": "Libro electrónico", - "LabelEbooks": "Libros electrónicos", + "LabelEbook": "Libro-e", + "LabelEbooks": "Libros-e", "LabelEdit": "Editar", "LabelEmail": "Correo electrónico", "LabelEmailSettingsFromAddress": "Remitente", "LabelEmailSettingsRejectUnauthorized": "Rechazar certificados no autorizados", "LabelEmailSettingsRejectUnauthorizedHelp": "Desactivar la validación de certificados SSL puede exponer su conexión a riesgos de seguridad, como los ataques por intermediario. Desactive esta opción solo si conoce las implicaciones y confía en el servidor de correo al que se conecta.", - "LabelEmailSettingsSecure": "Seguridad", + "LabelEmailSettingsSecure": "Seguro", "LabelEmailSettingsSecureHelp": "Si está activado, se usará TLS para conectarse al servidor. Si está apagado, se usará TLS si su servidor tiene soporte para la extensión STARTTLS. En la mayoría de los casos, puede dejar esta opción activada si se está conectando al puerto 465. Apáguela en el caso de usar los puertos 587 o 25. (de nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Probar dirección", "LabelEmbeddedCover": "Cubierta incrustada", @@ -377,11 +377,12 @@ "LabelFilename": "Nombre del archivo", "LabelFilterByUser": "Filtrar por Usuario", "LabelFindEpisodes": "Buscar Episodio", - "LabelFinished": "Terminado", + "LabelFinished": "Finalizado", + "LabelFinishedDate": "Finalizado {0}", "LabelFolder": "Carpeta", "LabelFolders": "Carpetas", "LabelFontBold": "Negrilla", - "LabelFontBoldness": "Peso tipográfico", + "LabelFontBoldness": "Tipográfico sin Negrita", "LabelFontFamily": "Familia tipográfica", "LabelFontItalic": "Itálica", "LabelFontScale": "Escala de letra", @@ -391,8 +392,8 @@ "LabelGenre": "Género", "LabelGenres": "Géneros", "LabelHardDeleteFile": "Eliminar Definitivamente", - "LabelHasEbook": "Tiene un libro", - "LabelHasSupplementaryEbook": "Tiene un libro complementario", + "LabelHasEbook": "Tiene libro-e", + "LabelHasSupplementaryEbook": "Tiene un libro-e suplementario", "LabelHideSubtitles": "Ocultar subtítulos", "LabelHighestPriority": "Mayor prioridad", "LabelHost": "Anfitrión", @@ -422,6 +423,7 @@ "LabelLanguages": "Idiomas", "LabelLastBookAdded": "Último libro añadido", "LabelLastBookUpdated": "Último libro actualizado", + "LabelLastProgressDate": "Último progreso: {0}", "LabelLastSeen": "Última Vez Visto", "LabelLastTime": "Última Vez", "LabelLastUpdate": "Última Actualización", @@ -434,6 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Sin {0}", "LabelLibraryItem": "Elemento de Biblioteca", "LabelLibraryName": "Nombre de Biblioteca", + "LabelLibrarySortByProgress": "Progreso: Último actualizado", + "LabelLibrarySortByProgressFinished": "Progreso: Finalizado", + "LabelLibrarySortByProgressStarted": "Progreso: Iniciado", "LabelLimit": "Limites", "LabelLineSpacing": "Interlineado", "LabelListenAgain": "Volver a escuchar", @@ -442,6 +447,7 @@ "LabelLogLevelWarn": "Advertencia", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", "LabelLowestPriority": "Menor prioridad", + "LabelMatchConfidence": "Confidencia", "LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por", "LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO", "LabelMaxEpisodesToDownload": "Número máximo # de episodios para descargar. Usa 0 para descargar una cantidad ilimitada.", @@ -460,7 +466,7 @@ "LabelMissingEbook": "No tiene libro electrónico", "LabelMissingSupplementaryEbook": "No tiene libro electrónico suplementario", "LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos", - "LabelMobileRedirectURIsDescription": "Esta es una lista blanca de URI de redireccionamiento válidos para aplicaciones móviles. El predeterminado es audiobookshelf , que puede eliminar o complementar con URI adicionales para la integración de aplicaciones de terceros. Usando un asterisco ( * ) como única entrada que permite cualquier URI.", + "LabelMobileRedirectURIsDescription": "Esta es una lista en blanco de las URI de re‐direccionamiento válidos para aplicaciones móviles. El predeterminado es audiobookshelf , que puede retirar o sustituir con las URI adicionales para la integración de aplicaciones de terceros. Usando un asterisco (* ) como única entrada que permite cualquier URI.", "LabelMore": "Más", "LabelMoreInfo": "Más información", "LabelName": "Nombre", @@ -473,9 +479,10 @@ "LabelNextBackupDate": "Fecha del siguiente respaldo", "LabelNextChapters": "Los próximos capítulos serán:", "LabelNextScheduledRun": "Próxima ejecución programada", + "LabelNoApiKeys": "Sin claves API", "LabelNoCustomMetadataProviders": "Sin proveedores de metadatos personalizados", "LabelNoEpisodesSelected": "Ningún Episodio Seleccionado", - "LabelNotFinished": "No terminado", + "LabelNotFinished": "No finalizado", "LabelNotStarted": "Sin iniciar", "LabelNotes": "Notas", "LabelNotificationAppriseURL": "URL(s) de Apprise", @@ -489,7 +496,7 @@ "LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignoradas si llegan al numero máximo de cola para prevenir spam de eventos.", "LabelNumberOfBooks": "Número de libros", "LabelNumberOfChapters": "Número de capítulos:", - "LabelNumberOfEpisodes": "N.º de episodios", + "LabelNumberOfEpisodes": "Nº de episodios", "LabelOpenIDAdvancedPermsClaimDescription": "Nombre de la notificación de OpenID que contiene permisos avanzados para acciones de usuario dentro de la aplicación que se aplicarán a roles que no sean de administrador (si están configurados). Si el reclamo no aparece en la respuesta, se denegará el acceso a ABS. Si falta una sola opción, se tratará como falsa. Asegúrese de que la notificación del proveedor de identidades coincida con la estructura esperada:", "LabelOpenIDClaims": "Deje las siguientes opciones vacías para desactivar la asignación avanzada de grupos y permisos, lo que asignaría de manera automática al grupo «Usuario».", "LabelOpenIDGroupClaimDescription": "Nombre de la declaración OpenID que contiene una lista de grupos del usuario. Comúnmente conocidos como grupos. Si se configura, la aplicación asignará automáticamente roles en función de la pertenencia a grupos del usuario, siempre que estos grupos se denominen \"admin\", \"user\" o \"guest\" en la notificación. La solicitud debe contener una lista, y si un usuario pertenece a varios grupos, la aplicación asignará el rol correspondiente al mayor nivel de acceso. Si ningún grupo coincide, se denegará el acceso.", @@ -519,7 +526,7 @@ "LabelPodcasts": "Pódcast", "LabelPort": "Puerto", "LabelPrefixesToIgnore": "Prefijos para ignorar (no distingue entre mayúsculas y minúsculas)", - "LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indicen su suministro", + "LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indexen su suministro", "LabelPrimaryEbook": "Libro electrónico principal", "LabelProgress": "Progreso", "LabelProvider": "Proveedor", @@ -531,11 +538,11 @@ "LabelPublishedDecades": "Décadas publicadas", "LabelPublisher": "Editor", "LabelPublishers": "Editores", - "LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", - "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", + "LabelRSSFeedCustomOwnerEmail": "Correo-e de propietario personalizado", + "LabelRSSFeedCustomOwnerName": "Nombre de propietario personalizado", "LabelRSSFeedOpen": "Fuente RSS Abierta", "LabelRSSFeedPreventIndexing": "Evitar indización", - "LabelRSSFeedSlug": "«Slug» de suministro RSS", + "LabelRSSFeedSlug": "Ficha de suministro RSS", "LabelRSSFeedURL": "URL de suministro RSS", "LabelRandomly": "Aleatorio", "LabelReAddSeriesToContinueListening": "Volver a agregar la serie para continuar escuchándola", @@ -563,11 +570,12 @@ "LabelSelectAll": "Seleccionar todo", "LabelSelectAllEpisodes": "Seleccionar todos los episodios", "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", + "LabelSelectUser": "Seleccionar usuario", "LabelSelectUsers": "Seleccionar usuarios", "LabelSendEbookToDevice": "Enviar libro electrónico a...", "LabelSequence": "Secuencia", "LabelSerial": "En serie", - "LabelSeries": "Serie", + "LabelSeries": "Series", "LabelSeriesName": "Nombre de la serie", "LabelSeriesProgress": "Progreso de la serie", "LabelServerLogLevel": "Nivel de registro del servidor", @@ -580,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera", "LabelSettingsChromecastSupport": "Compatibilidad con Chromecast", "LabelSettingsDateFormat": "Formato de Fecha", - "LabelSettingsEnableWatcher": "Buscar cambios automáticamente en las bibliotecas", - "LabelSettingsEnableWatcherForLibrary": "Buscar cambios automáticamente en la biblioteca", + "LabelSettingsEnableWatcher": "Vigilar automáticamente los cambios en bibliotecas", + "LabelSettingsEnableWatcherForLibrary": "Vigilar automáticamente los cambios de biblioteca", "LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requiere reiniciar el servidor", "LabelSettingsEpubsAllowScriptedContent": "Permitir scripts en epubs", "LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que los archivos epub ejecuten scripts. Se recomienda mantener esta opción desactivada a menos que confíe en el origen de los archivos epub.", @@ -614,14 +622,14 @@ "LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca", "LabelSettingsTimeFormat": "Formato de Tiempo", "LabelShare": "Compartir", - "LabelShareDownloadableHelp": "Permite a quienes posean el enlace de compartición descargar un archivo ZIP del elemento de la biblioteca.", + "LabelShareDownloadableHelp": "Permite a quienes posean el enlace de compartición descargar un archivo zip del elemento de la biblioteca.", "LabelShareOpen": "abrir un recurso compartido", "LabelShareURL": "Compartir la URL", "LabelShowAll": "Mostrar todo", "LabelShowSeconds": "Mostrar segundos", "LabelShowSubtitles": "Mostrar subtítulos", "LabelSize": "Tamaño", - "LabelSleepTimer": "Temporizador de apagado", + "LabelSleepTimer": "Temporizador de dormida", "LabelSlug": "Slug", "LabelSortAscending": "Ascendente", "LabelSortDescending": "Descendente", @@ -630,6 +638,7 @@ "LabelStartTime": "Tiempo de Inicio", "LabelStarted": "Iniciado", "LabelStartedAt": "Iniciado En", + "LabelStartedDate": "Iniciado {0}", "LabelStatsAudioTracks": "Pistas de Audio", "LabelStatsAuthors": "Autores", "LabelStatsBestDay": "Mejor día", @@ -659,6 +668,7 @@ "LabelTheme": "Tema", "LabelThemeDark": "Oscuro", "LabelThemeLight": "Claro", + "LabelThemeSepia": "Sepia", "LabelTimeBase": "Tiempo Base", "LabelTimeDurationXHours": "{0} horas", "LabelTimeDurationXMinutes": "{0} minutos", @@ -727,8 +737,10 @@ "MessageAddToPlayerQueue": "Agregar a fila del Reproductor", "MessageAppriseDescription": "Para usar esta función deberás tener la API de Apprise corriendo o una API que maneje los mismos resultados.
La URL de la API de Apprise debe tener la misma ruta de archivos que donde se envían las notificaciones. Por ejemplo: si su API esta en http://192.168.1.1:8337 entonces pondría http://192.168.1.1:8337/notify.", "MessageAsinCheck": "Cerciórese de usar el ASIN de la región correcta de Audible, no de Amazon.", + "MessageAuthenticationLegacyTokenWarning": "Los vales de API heredados serán retirados en el futuro. Utilice las claves de API en su lugar.", "MessageAuthenticationOIDCChangesRestart": "Reinicie el servidor tras el guardado para aplicar los cambios de OIDC.", - "MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en /metadata/items y /metadata/authors. Los Respaldos NO incluyen ningún archivo guardado en la carpeta de tu biblioteca.", + "MessageAuthenticationSecurityMessage": "La autenticación ha sido mejorada para seguridad. Todos los usuarios requieren reiniciar sesión.", + "MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en /metadata/items y /metadata/authors. Los Respaldos no incluyen ningún archivo guardado en la carpeta de tu biblioteca.", "MessageBackupsLocationEditNote": "Nota: actualizar la ubicación de la copia de respaldo no moverá ni modificará los respaldos existentes", "MessageBackupsLocationNoEditNote": "Nota: la ubicación de la copia de respaldo se establece a través de una variable de entorno y no se puede cambiar aquí.", "MessageBackupsLocationPathEmpty": "La ruta de la copia de seguridad no puede estar vacía", @@ -750,6 +762,7 @@ "MessageChaptersNotFound": "Capítulos no encontrados", "MessageCheckingCron": "Revisando cron...", "MessageConfirmCloseFeed": "¿Confirma que quiere cerrar este suministro?", + "MessageConfirmDeleteApiKey": "¿Está seguro que desea eliminar la clave API «{0}»?", "MessageConfirmDeleteBackup": "¿Confirma que quiere eliminar el respaldo de {0}?", "MessageConfirmDeleteDevice": "¿Confirma que quiere eliminar el lector electrónico «{0}»?", "MessageConfirmDeleteFile": "Esto eliminará el archivo del sistema de archivos. ¿Quiere continuar?", @@ -768,8 +781,8 @@ "MessageConfirmMarkSeriesFinished": "¿Confirma que quiere marcar todos los libros de esta serie como terminados?", "MessageConfirmMarkSeriesNotFinished": "¿Confirma que quiere marcar todos los libros de esta serie como no terminados?", "MessageConfirmNotificationTestTrigger": "¿Activar esta notificación con datos de prueba?", - "MessageConfirmPurgeCache": "Purgar la antememoria eliminará el directorio completo ubicado en /metadata/cache.

¿Confirma que quiere eliminar el directorio de antememoria?", - "MessageConfirmPurgeItemsCache": "Purgar la antememoria de elementos eliminará el directorio completo ubicado en /metadata/cache/items.
¿Lo confirma?", + "MessageConfirmPurgeCache": "La purga del caché eliminará el directorio completo en /metadata/cache.

¿Confirma que desea quitar el directorio de caché?", + "MessageConfirmPurgeItemsCache": "Purgar el caché de elementos eliminará el directorio completo ubicado en /metadata/cache/items.
¿Lo confirma?", "MessageConfirmQuickEmbed": "Atención: la incrustación rápida no realiza copias de respaldo a ninguno de sus archivos de audio. Cerciórese de haber realizado una copia de los mismos previamente.

¿Quiere continuar?", "MessageConfirmQuickMatchEpisodes": "El reconocimiento rápido de extensiones sobrescribirá los detalles si se encuentra una coincidencia. Se actualizarán las extensiones no reconocidas. ¿Quiere continuar?", "MessageConfirmReScanLibraryItems": "¿Confirma que quiere volver a analizar {0} elementos?", @@ -795,14 +808,16 @@ "MessageDaysListenedInTheLastYear": "{0} días escuchados el año pasado", "MessageDownloadingEpisode": "Descargando episodio", "MessageDragFilesIntoTrackOrder": "Arrastre los archivos al orden correcto de las pistas", - "MessageEmbedFailed": "Falló la incrustación.", - "MessageEmbedFinished": "Finalizó la incrustación.", + "MessageEmbedFailed": "Incorporación incorrecta.", + "MessageEmbedFinished": "Incorporación finalizada.", "MessageEmbedQueue": "En cola para incrustar metadatos ({0} en cola)", "MessageEpisodesQueuedForDownload": "{0} episodio(s) en cola para descargar", "MessageEreaderDevices": "Para garantizar la entrega de libros electrónicos, es posible que tenga que agregar la dirección de correo electrónico anterior como remitente válido para cada dispositivo enumerado a continuación.", "MessageFeedURLWillBe": "El URL del suministro será {0}", "MessageFetching": "Recuperando...", "MessageForceReScanDescription": "Escaneará todos los archivos como un nuevo escaneo. Archivos de audio con etiquetas ID3, archivos OPF y archivos de texto serán escaneados como nuevos.", + "MessageHeatmapListeningTimeTooltip": "{0} escuchando en {1}", + "MessageHeatmapNoListeningSessions": "No enumera sesiones en {0}", "MessageImportantNotice": "¡Notificación importante!", "MessageInsertChapterBelow": "Insertar capítulo debajo", "MessageInvalidAsin": "ASIN no válido", @@ -835,7 +850,7 @@ "MessageNoEpisodes": "Ningún episodio", "MessageNoFoldersAvailable": "Ninguna carpeta disponible", "MessageNoGenres": "Ningún género", - "MessageNoIssues": "Ningún número", + "MessageNoIssues": "Sin incidencias", "MessageNoItems": "Ningún elemento", "MessageNoItemsFound": "Ningún elemento encontrado", "MessageNoListeningSessions": "Ninguna sesión de escucha", @@ -873,7 +888,7 @@ "MessageResetChaptersConfirm": "¿Confirma que quiere deshacer los cambios y restablecer los capítulos a su estado original?", "MessageRestoreBackupConfirm": "¿Confirma que quiere restaurar el respaldo creado el", "MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.

El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.

Todos los clientes que usen su servidor se actualizarán automáticamente.", - "MessageScheduleLibraryScanNote": "Para la mayoría de los usuarios, se recomienda dejar esta función desactivada y mantener activada la configuración del observador de carpetas. El observador de carpetas detectará automáticamente los cambios en las carpetas de la biblioteca. El observador de carpetas no funciona para todos los sistemas de archivos (como NFS), por lo que se pueden utilizar exploraciones programadas de la biblioteca en su lugar.", + "MessageScheduleLibraryScanNote": "Para muchos usuarios, es recomendado dejar esta característica inhabilitada y mantener habilitados los ajustes de la «Vigía automática de cambio de biblioteca»: detectará automáticamente los cambios en sus carpetas de bibliotecas. Habilitar esta características si «Vigía automática de cambio de biblioteca» no funciona en su sistema de archivo (como NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Ejecutar cada {0} a las {1}", "MessageSearchResultsFor": "Resultados de la búsqueda de", "MessageSelected": "{0} seleccionado(s)", @@ -999,6 +1014,8 @@ "ToastBulkChapterInvalidCount": "Por favor ingrese un número válido entre 1 y 150", "ToastCachePurgeFailed": "No se pudo purgar la antememoria", "ToastCachePurgeSuccess": "Se purgó la antememoria correctamente", + "ToastChapterLocked": "El capítulo está bloqueado.", + "ToastChapterStartTimeAdjusted": "El capítulo inicia el tiempo ajustado en {0} segundos", "ToastChaptersAllLocked": "Todos los capítulos están bloqueados. Desbloquee algunos capítulos para cambiar sus tiempos.", "ToastChaptersHaveErrors": "Los capítulos tienen errores", "ToastChaptersInvalidShiftAmountLast": "Cantidad de desplazamiento no válida. La hora de inicio del último capítulo se extendería más allá de la duración de este audiolibro.", @@ -1009,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Artículo(s) añadido(s) a la colección fallido(s)", "ToastCollectionRemoveSuccess": "Colección quitada", "ToastCollectionUpdateSuccess": "Colección actualizada", + "ToastConnectionNotAvailable": "Conexión no disponible. Intenta de nuevo más tarde", + "ToastCoverSearchFailed": "Cobertura de búsqueda incorrecta", "ToastCoverUpdateFailed": "Error al actualizar la cubierta", "ToastDateTimeInvalidOrIncomplete": "Fecha y hora no válidas o incompletas", "ToastDeleteFileFailed": "Falló la eliminación del archivo", @@ -1024,6 +1043,8 @@ "ToastEpisodeDownloadQueueClearSuccess": "Se borró la cola de descargas de los episodios", "ToastEpisodeUpdateSuccess": "{0} episodio(s) actualizado(s)", "ToastErrorCannotShare": "No se puede compartir de forma nativa en este dispositivo", + "ToastFailedToCreate": "Ha fallado al crear", + "ToastFailedToDelete": "Ha fallado al eliminar", "ToastFailedToLoadData": "Error al cargar data", "ToastFailedToMatch": "Error al emparejar", "ToastFailedToShare": "Error al compartir", @@ -1031,6 +1052,7 @@ "ToastInvalidImageUrl": "URL de la imagen no válida", "ToastInvalidMaxEpisodesToDownload": "Número máximo de episodios para descargar no válidos", "ToastInvalidUrl": "URL no válida", + "ToastInvalidUrls": "Una o más URL son inválidas", "ToastItemCoverUpdateSuccess": "Cubierta del elemento actualizada", "ToastItemDeletedFailed": "Error al eliminar el elemento", "ToastItemDeletedSuccess": "Elemento borrado", @@ -1055,6 +1077,7 @@ "ToastMustHaveAtLeastOnePath": "Debe tener al menos una ruta", "ToastNameEmailRequired": "Son obligatorios el nombre y el correo electrónico", "ToastNameRequired": "Nombre obligatorio", + "ToastNewApiKeyUserError": "Debe seleccionar un usuario", "ToastNewEpisodesFound": "{0} nuevo(s) episodio(s) encontrado(s)", "ToastNewUserCreatedFailed": "No se pudo crear la cuenta: «{0}»", "ToastNewUserCreatedSuccess": "Nueva cuenta creada", @@ -1093,8 +1116,8 @@ "ToastRemoveFailed": "Error al eliminar", "ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección", "ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección", - "ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca incorrectos", - "ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca incorrectos", + "ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca con incidencias", + "ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca con incidencias", "ToastRenameFailed": "Error al cambiar el nombre", "ToastRescanFailed": "Error al volver a escanear para {0}", "ToastRescanRemoved": "Se eliminó el elemento reescaneado", @@ -1133,6 +1156,7 @@ "ToastUserRootRequireName": "Debe introducir un nombre de usuario administrativo", "TooltipAddChapters": "Añadir capítulo(s)", "TooltipAddOneSecond": "Añadir 1 segundo", + "TooltipAdjustChapterStart": "Pulse para ajustar la hora de inicio", "TooltipLockAllChapters": "Bloquear todos los capítulos", "TooltipLockChapter": "Bloquear capítulo (Mayús+clic para rango)", "TooltipSubtractOneSecond": "Restar 1 segundo", diff --git a/client/strings/fr.json b/client/strings/fr.json index 4841ec9f6..507574033 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Livres", "LabelButtonText": "Texte du bouton", - "LabelByAuthor": "par {0}", + "LabelByAuthor": "de {0}", "LabelChangePassword": "Modifier le mot de passe", "LabelChannels": "Canaux", "LabelChapterCount": "{0} Chapitres", @@ -436,11 +436,11 @@ "LabelLibraryFilterSublistEmpty": "Aucun {0}", "LabelLibraryItem": "Élément de bibliothèque", "LabelLibraryName": "Nom de la bibliothèque", - "LabelLibrarySortByProgress": "Progression : dernière mise à jour", + "LabelLibrarySortByProgress": "Progression : Mise à jour", "LabelLibrarySortByProgressFinished": "Progression : Terminé", - "LabelLibrarySortByProgressStarted": "Progression : Commencé", + "LabelLibrarySortByProgressStarted": "Progression : En cours", "LabelLimit": "Limite", - "LabelLineSpacing": "Espacement des lignes", + "LabelLineSpacing": "Interligne", "LabelListenAgain": "Écouter à nouveau", "LabelLogLevelDebug": "Débogage", "LabelLogLevelInfo": "Info", @@ -622,7 +622,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les fichiers de métadonnées sont stockés dans /metadata/items. En activant ce paramètre, les fichiers de métadonnées seront stockés dans les dossiers des éléments de votre bibliothèque", "LabelSettingsTimeFormat": "Format d’heure", "LabelShare": "Partager", - "LabelShareDownloadableHelp": "Permet aux utilisateurs de télécharger un fichier ZIP de l'élément de la bibliothèque.", + "LabelShareDownloadableHelp": "Permet aux utilisateurs disposant du lien de partage de télécharger un fichier zip contenant l'élément de la bibliothèque.", "LabelShareOpen": "Ouvrir le partage", "LabelShareURL": "Partager l’URL", "LabelShowAll": "Tout afficher", @@ -961,8 +961,8 @@ "PlaceholderNewCollection": "Nom de la nouvelle collection", "PlaceholderNewFolderPath": "Nouveau chemin de dossier", "PlaceholderNewPlaylist": "Nouveau nom de liste de lecture", - "PlaceholderSearch": "Recherche…", - "PlaceholderSearchEpisode": "Rechercher un épisode..", + "PlaceholderSearch": "Recherche...", + "PlaceholderSearchEpisode": "Rechercher un épisode…", "StatsAuthorsAdded": "auteurs ajoutés", "StatsBooksAdded": "livres ajoutés", "StatsBooksAdditional": "Les ajouts comprennent…", diff --git a/client/strings/he.json b/client/strings/he.json index 0efc1ec9c..786683a5d 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -81,7 +81,7 @@ "ButtonRemove": "הסר", "ButtonRemoveAll": "הסר הכל", "ButtonRemoveAllLibraryItems": "הסר את כל פריטי הספרייה", - "ButtonRemoveFromContinueListening": "הסר מ- המשך האזנה", + "ButtonRemoveFromContinueListening": "הסר מ״המשך האזנה״", "ButtonRemoveFromContinueReading": "הסר מ- המשך קריאה", "ButtonRemoveSeriesFromContinueSeries": "הסר סדרה מ- המשך סדרה", "ButtonReset": "איפוס", @@ -121,6 +121,7 @@ "HeaderAccount": "חשבון", "HeaderAddCustomMetadataProvider": "הוסף ספק מטא-נתונים מותאם אישית", "HeaderAdvanced": "מתקדם", + "HeaderApiKeys": "מפתחות API", "HeaderAppriseNotificationSettings": "הגדרות התראות של Apprise", "HeaderAudioTracks": "רצועות קול", "HeaderAudiobookTools": "כלים לניהול קבצי ספרים קוליים", @@ -165,6 +166,7 @@ "HeaderMetadataOrderOfPrecedence": "סדר העדפת מטא-נתונים", "HeaderMetadataToEmbed": "מטא-נתונים להטמעה", "HeaderNewAccount": "חשבון חדש", + "HeaderNewApiKey": "מפתח API חדש", "HeaderNewLibrary": "ספרייה חדשה", "HeaderNotificationCreate": "צור התראה", "HeaderNotificationUpdate": "עדכון התראה", @@ -210,6 +212,7 @@ "HeaderTableOfContents": "תוכן עניינים", "HeaderTools": "כלים", "HeaderUpdateAccount": "עדכן חשבון", + "HeaderUpdateApiKey": "עדכן מפתח API", "HeaderUpdateAuthor": "עדכן יוצר", "HeaderUpdateDetails": "עדכן פרטים", "HeaderUpdateLibrary": "עדכן ספרייה", @@ -239,7 +242,10 @@ "LabelAllUsersExcludingGuests": "כל המשתמשים, ללא אורחים", "LabelAllUsersIncludingGuests": "כל המשתמשים כולל אורחים", "LabelAlreadyInYourLibrary": "כבר קיים בספרייה שלך", + "LabelApiKeyCreated": "מפתח API ״{0}״ נוצר בהצלחה.", + "LabelApiKeyCreatedDescription": "אנא העתק את מפתח ה־API כעת, לא ניתן יהיה להציגו שוב.", "LabelApiKeyUser": "פעל בשם המשתמש", + "LabelApiKeyUserDescription": "למפתח ה־API יהיו הרשאות זהות למשתמש שעל שמו הוא פועל. ביומני הרישום (logs), הפעולות יופיעו כאילו בוצעו על ידי המשתמש עצמו.", "LabelApiToken": "טוקן API", "LabelAppend": "הוסף לסוף", "LabelAudioBitrate": "קצב סיביות (לדוגמא 128k)", @@ -289,6 +295,7 @@ "LabelContinueListening": "המשך האזנה", "LabelContinueReading": "המשך קריאה", "LabelContinueSeries": "המשך סדרה", + "LabelCorsAllowed": "מקורות CORS מורשים", "LabelCover": "כריכה", "LabelCoverImageURL": "כתובת התמונה ברשת", "LabelCoverProvider": "ספק כריכה", @@ -302,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "מחיקה מהמערכת הקבצים (הסר סימון למחיקה רק ממסד הנתונים)", "LabelDescription": "תיאור", "LabelDeselectAll": "הסר בחירת כל הפריטים", + "LabelDetectedPattern": "תבנית שזוהתה:", "LabelDevice": "התקן", "LabelDeviceInfo": "מידע על התקן", "LabelDeviceIsAvailableTo": "התקן זמין ל...", @@ -351,6 +359,10 @@ "LabelExample": "דוגמה", "LabelExpandSeries": "הרחב סדרה", "LabelExpandSubSeries": "הרחב תת סדרה", + "LabelExpired": "פג תוקף", + "LabelExpiresAt": "יפוג בתאריך", + "LabelExpiresInSeconds": "יפוג בעוד (שניות)", + "LabelExpiresNever": "ללא הגבלת זמן", "LabelExplicit": "מפורש", "LabelExplicitChecked": "בוטה (מסומן)", "LabelExplicitUnchecked": "לא בוטה (לא מסומן)", @@ -366,6 +378,7 @@ "LabelFilterByUser": "סינון לפי משתמש", "LabelFindEpisodes": "מצא פרקים", "LabelFinished": "הושלם", + "LabelFinishedDate": "הושלם {0}", "LabelFolder": "תיקייה", "LabelFolders": "תיקיות", "LabelFontBold": "מודגש", @@ -410,6 +423,7 @@ "LabelLanguages": "שפות", "LabelLastBookAdded": "הספר האחרון שנוסף", "LabelLastBookUpdated": "הספר האחרון שעודכן", + "LabelLastProgressDate": "התקדמות אחרונה: {0}", "LabelLastSeen": "נראה לאחרונה", "LabelLastTime": "הזמן האחרון", "LabelLastUpdate": "עדכון אחרון", @@ -422,6 +436,9 @@ "LabelLibraryFilterSublistEmpty": "לא {0}", "LabelLibraryItem": "פריט ספרייה", "LabelLibraryName": "שם הספרייה", + "LabelLibrarySortByProgress": "התקדמות: עודכן לאחרונה", + "LabelLibrarySortByProgressFinished": "התקדמות: הושלם", + "LabelLibrarySortByProgressStarted": "התקדמות: הותחל", "LabelLimit": "מגבלה", "LabelLineSpacing": "מרווח שורה", "LabelListenAgain": "האזן שוב", @@ -430,6 +447,7 @@ "LabelLogLevelWarn": "אזהרה", "LabelLookForNewEpisodesAfterDate": "חפש פרקים חדשים לאחר תאריך זה", "LabelLowestPriority": "העדיפות הנמוכה ביותר", + "LabelMatchConfidence": "רמת ודאות", "LabelMatchExistingUsersBy": "התאם משתמשים קיימים לפי", "LabelMatchExistingUsersByDescription": "משמש לחיבור משתמשים קיימים. לאחר החיבור, המשתמשים יותאמו לפי זיהוי ייחודי מספק ה-SSO שלך", "LabelMaxEpisodesToDownload": "מספר פרקים מקסימלי להורדה. 0 - ללא הגבלה.", @@ -459,7 +477,9 @@ "LabelNewestAuthors": "הסופרים האחרונים", "LabelNewestEpisodes": "הפרקים החדשים ביותר", "LabelNextBackupDate": "תאריך הגיבוי הבא", + "LabelNextChapters": "הפרקים הבא יהיו:", "LabelNextScheduledRun": "הרצה מתוזמנת הבאה", + "LabelNoApiKeys": "אין מפתחות API", "LabelNoCustomMetadataProviders": "אין ספקי מטא-נתונים מותאמים אישית", "LabelNoEpisodesSelected": "לא נבחרו פרקים", "LabelNotFinished": "לא הושלם", @@ -475,16 +495,21 @@ "LabelNotificationsMaxQueueSize": "גודל התור המרבי לאירועי התראה", "LabelNotificationsMaxQueueSizeHelp": "האירועים מוגבלים לשליחה אחת לשנייה. האירועים יתעלמו אם התור מלא. הגדרה זו נועדה למנוע ספאם התראות.", "LabelNumberOfBooks": "מספר הספרים", + "LabelNumberOfChapters": "מספר הפרקים:", "LabelNumberOfEpisodes": "# פרקים", "LabelOpenIDAdvancedPermsClaimDescription": "שם OpenID claim המכילה הרשאות מתקדמות לפעולות משתמש בתוך האפליקציה, אשר יחולו על תפקידים שאינם מנהלי מערכת (אם הוגדרה). אם התביעה חסרה בתגובה, הגישה ל-ABS תידחה. אם אפשרות אחת חסרה, היא תטופל כ-false יש לוודא שטענת ספק הזהויות תואמת את המבנה הצפוי:", "LabelOpenIDClaims": "השאר את האפשרויות הבאות ריקות כדי להשבית הקצאת קבוצות והרשאות מתקדמת, ולאחר מכן להקצות אוטומטית את קבוצת 'משתמש'.", + "LabelOpenIDGroupClaimDescription": "שם ה־OpenID claim המכיל את רשימת הקבוצות של המשתמש. בדרך כלל נקרא groups. אם הוגדרה, האפליקציה תקצה תפקידים באופן אוטומטי על סמך השיוך לקבוצות, בתנאי ששמות הקבוצות ב־claim הם 'admin', 'user' או 'guest' (ללא רגישות לרישיות - Case-insensitive). ה־claim צריך להכיל רשימה; אם המשתמש משויך למספר קבוצות, האפליקציה תקצה את התפקיד בעל רמת הגישה הגבוהה ביותר. במידה ולא נמצאה קבוצה תואמת, הגישה תיחסם.", "LabelOpenRSSFeed": "פתח ערוץ RSS", "LabelOverwrite": "לשכפל", + "LabelPaginationPageXOfY": "עמוד {0} מתוך {1}", "LabelPassword": "סיסמה", "LabelPath": "נתיב", + "LabelPermanent": "קבוע", "LabelPermissionsAccessAllLibraries": "ניתן לגשת לכל הספריות", "LabelPermissionsAccessAllTags": "ניתן לגשת לכל התגיות", "LabelPermissionsAccessExplicitContent": "ניתן לגשת לתוכן בוטה", + "LabelPermissionsCreateEreader": "ניתן ליצור קורא ספרים דיגיטלי", "LabelPermissionsDelete": "מותר למחוק", "LabelPermissionsDownload": "מותר להוריד", "LabelPermissionsUpdate": "מותר לעדכן", @@ -492,6 +517,8 @@ "LabelPersonalYearReview": "השנה שלך בסקירה ({0})", "LabelPhotoPathURL": "נתיב/URL לתמונה", "LabelPlayMethod": "שיטת הפעלה", + "LabelPlaybackRateIncrementDecrement": "שיעור הגדלה/הפחתה של מהירות ההשמעה", + "LabelPlayerChapterNumberMarker": "{0} מתוך {1}", "LabelPlaylists": "רשימות השמעה", "LabelPodcast": "פודקאסט", "LabelPodcastSearchRegion": "אזור חיפוש פודקאסט", @@ -503,10 +530,14 @@ "LabelPrimaryEbook": "ספר אלקטרוני ראשי", "LabelProgress": "התקדמות", "LabelProvider": "ספק", + "LabelProviderAuthorizationValue": "ערך כותרת האימות (Authorization Header)", "LabelPubDate": "תאריך פרסום", "LabelPublishYear": "שנת הפרסום", "LabelPublishedDate": "פורסם {0}", + "LabelPublishedDecade": "עשור פרסום", + "LabelPublishedDecades": "עשורי פרסום", "LabelPublisher": "מוציא לאור", + "LabelPublishers": "מוצאים לאור", "LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית", "LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית", "LabelRSSFeedOpen": "ערוץ RSS פתוח", @@ -514,6 +545,7 @@ "LabelRSSFeedSlug": "Slug של ערוץ ה-RSS", "LabelRSSFeedURL": "כתובת ערוץ ה-RSS", "LabelRandomly": "באופן אקראי", + "LabelReAddSeriesToContinueListening": "הוסף סדרה בחזרה אל ״המשך האזנה״", "LabelRead": "קריאה", "LabelReadAgain": "קרא שוב", "LabelReadEbookWithoutProgress": "קרא/י ספר אלקטרוני ללא שמירת התקדמות", @@ -523,29 +555,44 @@ "LabelRedo": "עשה שוב", "LabelRegion": "אזור", "LabelReleaseDate": "תאריך הוצאה לאור", + "LabelRemoveAllMetadataAbs": "הסר את כל קבצי metadata.abs", + "LabelRemoveAllMetadataJson": "הסר את כל קבצי metadata.json", + "LabelRemoveAudibleBranding": "הסר פתיח וסיום של Audible מהפרקים", "LabelRemoveCover": "הסר כריכה", + "LabelRemoveMetadataFile": "הסר קבצי מטא־נתונים מתיקיות הפריטים בספרייה", + "LabelRemoveMetadataFileHelp": "הסר את כל קבצי metadata.json ו־metadata.abs מתיקיות {0}.", "LabelRowsPerPage": "שורות לעמוד", "LabelSearchTerm": "מונח חיפוש", "LabelSearchTitle": "כותרת חיפוש", "LabelSearchTitleOrASIN": "כותרת חיפוש או ASIN", "LabelSeason": "עונה", + "LabelSeasonNumber": "עונה #{0}", + "LabelSelectAll": "בחר הכל", "LabelSelectAllEpisodes": "בחר את כל הפרקים", "LabelSelectEpisodesShowing": "בחר {0} פרקים המוצגים", + "LabelSelectUser": "בחר משתמש", "LabelSelectUsers": "בחר משתמשים", "LabelSendEbookToDevice": "שלח ספר אלקטרוני ל...", "LabelSequence": "רצף", + "LabelSerial": "מספר סידורי", "LabelSeries": "סדרה", "LabelSeriesName": "שם הסדרה", "LabelSeriesProgress": "התקדמות בסדרה", + "LabelServerLogLevel": "רמת פירוט יומני הרישום", "LabelServerYearReview": "השנה בסקירה של השרת ({0})", "LabelSetEbookAsPrimary": "קבע כראשי", "LabelSetEbookAsSupplementary": "קבע כמשלים", + "LabelSettingsAllowIframe": "אפשר הטמעה בתוך iframe", "LabelSettingsAudiobooksOnly": "רק ספרי קול", "LabelSettingsAudiobooksOnlyHelp": "הפעלת ההגדרה הזו תתעלם מקבצי ספרים אלקטרוניים אלא אם כן הם נמצאים בתיקיית ספרי קול, שבמקרה זה יקבעו כספרים אלקטרוניים נלווים", "LabelSettingsBookshelfViewHelp": "עיצוב סקאומורפי עם מדפי עץ", "LabelSettingsChromecastSupport": "תמיכה ב-Chromecast", "LabelSettingsDateFormat": "פורמט תאריך", + "LabelSettingsEnableWatcher": "הפעל מעקב שינויים בספריות", + "LabelSettingsEnableWatcherForLibrary": "הפעל מעקב שינויים בספרייה", "LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", + "LabelSettingsEpubsAllowScriptedContent": "אפשור תוכן הכולל סקריפטים ב־ePubs", + "LabelSettingsEpubsAllowScriptedContentHelp": "אפשר לקובצי EPUB להריץ סקריפטים. מומלץ להשאיר את ההגדרה כבויה, אלא אם כן מקור קובצי ה־ePub מהימן.", "LabelSettingsExperimentalFeatures": "תכונות ניסיוניות", "LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.", "LabelSettingsFindCovers": "מצא כריכות", @@ -554,7 +601,8 @@ "LabelSettingsHideSingleBookSeriesHelp": "סדרות הכוללות ספר אחד יוסתרו מדף הסדרות ומדף הבית.", "LabelSettingsHomePageBookshelfView": "השתמש בתצוגת מדף בדף הבית", "LabelSettingsLibraryBookshelfView": "השתמש בתצוגת מדף בספרייה", - "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים ב-המשך סדרה", + "LabelSettingsLibraryMarkAsFinishedWhen": "סמן פריט מדיה כהושלם כאשר", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "דלג על ספרים קודמים ב״המשך סדרה״", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "מדף המשך סדרות מציג את הספר הראשון שלא הושמע בסדרה שיש בה לפחות ספר אחד שהושלם ואין ספרים שכבר באמצע שמיעה. הפעלת הגדרה זו תמשיך סדרות מהספר שהושלם הכי מתקדם בסדרה במקום מהספר הראשון שלא הושמע.", "LabelSettingsParseSubtitles": "פענח כתוביות", "LabelSettingsParseSubtitlesHelp": "העתק כותרת משנה משם תיקיית הספר.
כותרת המשנה חייבת להיות מופרדת עם התו ״-״
לדוגמא, כותרת המשנה לספר ״שם הספר - כותרת משנה״, היא ״כותרת משנה״", @@ -571,13 +619,22 @@ "LabelSettingsStoreMetadataWithItem": "אחסן מטה-נתונים עם הפריט", "LabelSettingsStoreMetadataWithItemHelp": "כברירת מחדל, קבצי מטה-נתונים מאוחסנים ב- /metadata/items, הפעלת ההגדרה תאחסן קבצי מטה-נתונים בתיקיית פריט שלך בספרייה", "LabelSettingsTimeFormat": "פורמט זמן", + "LabelShare": "שתף", + "LabelShareDownloadableHelp": "אפשר למי שיש ברשותו קישור שיתוף להוריד קובץ ZIP של פריט הספרייה.", + "LabelShareURL": "שתף קישור", "LabelShowAll": "הצג הכל", + "LabelShowSeconds": "הצג שניות", + "LabelShowSubtitles": "הצג כתוביות", "LabelSize": "גודל", "LabelSleepTimer": "טיימר שינה", + "LabelSortAscending": "סדר עולה", + "LabelSortDescending": "סדר יורד", + "LabelSortPubDate": "מיין לפי תאריך פרסום", "LabelStart": "התחל", "LabelStartTime": "זמן התחלה", "LabelStarted": "התחיל", "LabelStartedAt": "התחיל ב", + "LabelStartedDate": "הותחל {0}", "LabelStatsAudioTracks": "רצועות שמע", "LabelStatsAuthors": "מחברים", "LabelStatsBestDay": "היום הטוב ביותר", @@ -607,7 +664,13 @@ "LabelTheme": "ערכת נושא", "LabelThemeDark": "כהה", "LabelThemeLight": "בהיר", + "LabelThemeSepia": "ספיה", "LabelTimeBase": "בסיס זמן", + "LabelTimeDurationXHours": "{0} שעות", + "LabelTimeDurationXMinutes": "{0} דקות", + "LabelTimeDurationXSeconds": "{0} שניות", + "LabelTimeInMinutes": "זמן בשניות", + "LabelTimeLeft": "נותרו {0}", "LabelTimeListened": "זמן האזנה", "LabelTimeListenedToday": "זמן האזנה היום", "LabelTimeRemaining": "{0} נותרו", @@ -615,6 +678,7 @@ "LabelTitle": "כותרת", "LabelToolsEmbedMetadata": "הטמעת מטה-נתונים", "LabelToolsEmbedMetadataDescription": "הטמעת מטה-נתונים לקבצי שמע כולל תמונות כריכה ופרקים.", + "LabelToolsM4bEncoder": "מקודד M4B", "LabelToolsMakeM4b": "יצירת קובץ אודיו M4B", "LabelToolsMakeM4bDescription": "יצירת קובץ אודיו .M4B עם מטה-נתונים מוטמעים, תמונת שער ופרקים.", "LabelToolsSplitM4b": "פיצול M4B ל-MP3", @@ -627,29 +691,39 @@ "LabelTracksMultiTrack": "רב-ערוצי", "LabelTracksNone": "אין ערוצים", "LabelTracksSingleTrack": "רצועה יחידה", + "LabelTrailer": "קדימון", "LabelType": "סוג", "LabelUnabridged": "לא מקוצר", "LabelUndo": "בטל", "LabelUnknown": "לא ידוע", + "LabelUnknownPublishDate": "תאריך הוצאה לאור לא ידוע", "LabelUpdateCover": "עדכן כריכה", "LabelUpdateCoverHelp": "אפשר החלפה של כריכות קיימות עבור הספרים הנבחרים כאשר נמצאה התאמה", "LabelUpdateDetails": "עדכון פרטים", "LabelUpdateDetailsHelp": "אפשר החלפה של פרטים קיימים עבור הספרים הנבחרים כאשר נמצאה התאמה", "LabelUpdatedAt": "עודכן ב-", "LabelUploaderDragAndDrop": "גרור ושחרר קבצים או תיקיות", + "LabelUploaderDragAndDropFilesOnly": "גרור ושחרר קבצים", "LabelUploaderDropFiles": "שחרר קבצים", "LabelUploaderItemFetchMetadataHelp": "משיכת כותרת, סופר וסדרה באופן אוטומטי", + "LabelUseAdvancedOptions": "השתמש באפשרויות מתקדמות", "LabelUseChapterTrack": "השתמש ברצועות הפרקים", "LabelUseFullTrack": "השתמש ברצועה המלאה", + "LabelUseZeroForUnlimited": "השתמש ב־0 מתוך אין־סוף", "LabelUser": "משתמש", "LabelUsername": "שם משתמש", "LabelValue": "ערך", "LabelVersion": "גרסה", "LabelViewBookmarks": "הצג סימניות", "LabelViewChapters": "הצג פרקים", + "LabelViewPlayerSettings": "הצג הגדרות נגן", "LabelViewQueue": "הצג תור נגן", "LabelVolume": "עוצמת קול", + "LabelWebRedirectURLsDescription": "יש לאשר את הכתובות הבאות אצל ספק ה־OAuth כדי לאפשר הפניה חזרה לאפליקציית הדפדפן לאחר ההתחברות:", + "LabelWebRedirectURLsSubfolder": "תיקיית משנה לכתובות הפניה", "LabelWeekdaysToRun": "ימי השבוע להרצה", + "LabelXBooks": "{0} ספרים", + "LabelXItems": "{0} פריטים", "LabelYearReviewHide": "הסתר סקירת שנה", "LabelYearReviewShow": "הצג סקירת שנה", "LabelYourAudiobookDuration": "משך הספר הקולי שלך", @@ -658,31 +732,55 @@ "LabelYourProgress": "ההתקדמות שלך", "MessageAddToPlayerQueue": "הוסף לתור הנגן", "MessageAppriseDescription": "כדי להשתמש בתכונה זו יש לך להריץ מופע של ממשק התכנית האפליקציה או API שיטפל בבקשות אלו.
כתובת URL של ממשק ה-Apprise API צריכה להיות הנתיב המלא לשליחת ההתראה, לדוגמה, אם המופע של ה-API שלך מוצע ב-http://192.168.1.1:8337 אז עליך לשים http://192.168.1.1:8337/notify.", + "MessageAsinCheck": "יש לוודא שימוש ב־ASIN מאזור ה־Audible הנכון, ולא מ־Amazon.", + "MessageAuthenticationLegacyTokenWarning": "אסימוני API ישנים יוסרו בעתיד. יש להשתמש ב מפתחות API במקום.", + "MessageAuthenticationOIDCChangesRestart": "יש להפעיל מחדש את השרת לאחר השמירה כדי להחיל את שינויי ה־OIDC.", + "MessageAuthenticationSecurityMessage": "האימות שופר מטעמי אבטחה. כל המשתמשים נדרשים להתחבר מחדש.", "MessageBackupsDescription": "גיבויים כוללים משתמשים, התקדמות משתמש, פרטי פריטי ספרייה, הגדרות שרת ותמונות השמורות ב-/metadata/items & /metadata/authors. גיבויים לא כוללים קבצים שמורים בתיקיות הספרייה שלך.", + "MessageBackupsLocationEditNote": "הערה: שינוי מיקום הגיבוי לא יגרום להעברה או לשינוי של גיבויים קיימים", + "MessageBackupsLocationNoEditNote": "הערה: מיקום הגיבוי מוגדר באמצעות משתנה סביבה ולא ניתן לשנותו כאן.", + "MessageBackupsLocationPathEmpty": "נתיב מיקום הגיבוי אינו יכול להיות ריק", + "MessageBatchEditPopulateMapDetailsAllHelp": "מלא את השדות הפעילים בנתונים מכל הפריטים. שדות בעלי ערכים מרובים ימוזגו", + "MessageBatchEditPopulateMapDetailsItemHelp": "מלא את שדות פרטי המיפוי הפעילים בנתונים מפריט זה", "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה להחליף כריכות קיימות ו/או מטה-נתונים.", "MessageBookshelfNoCollections": "עדיין לא יצרת אוספים", + "MessageBookshelfNoCollectionsHelp": "האוספים ציבוריים. כל המשתמשים בעלי גישה לספרייה יכולים לראות אותם.", "MessageBookshelfNoRSSFeeds": "אין ערוצי RSS פתוחים", "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור סינון \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "אין תוצאות עבור השאילתה", "MessageBookshelfNoSeries": "אין לך סדרות", + "MessageBulkChapterPattern": "כמה פרקים להוסיף לפי תבנית מספור זו?", "MessageChapterEndIsAfter": "זמן סיום הפרק אחרי סיום הספר הקולי שלך", "MessageChapterErrorFirstNotZero": "הפרק הראשון חייב להתחיל ב-0", "MessageChapterErrorStartGteDuration": "זמן התחלה לא תקין, חייב להיות פחות ממשך הספר הקולי", "MessageChapterErrorStartLtPrev": "זמן התחלה לא תקין, חייב להיות גדול או שווה לזמן ההתחלה של הפרק הקודם", "MessageChapterStartIsAfter": "התחלת הפרק אחרי סיום הספר הקולי שלך", + "MessageChaptersNotFound": "לא נמצאו פרקים", "MessageCheckingCron": "בודק את תזמון העבודה...", "MessageConfirmCloseFeed": "האם אתה בטוח שאתה רוצה לסגור את הערוץ הזה?", + "MessageConfirmDeleteApiKey": "האם למחוק את מפתח ה־API \"{0}\"?", "MessageConfirmDeleteBackup": "האם אתה בטוח שברצונך למחוק גיבוי עבור {0}?", + "MessageConfirmDeleteDevice": "האם למחוק את הקורא האלקטרוני \"{0}\"?", "MessageConfirmDeleteFile": "הקובץ ימחק לצמיתות מהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteLibrary": "האם אתה בטוח שברצונך למחוק לצמיתות את הספרייה \"{0}\"?", "MessageConfirmDeleteLibraryItem": "פריט הספרייה יימחק לצמיתות ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", "MessageConfirmDeleteLibraryItems": "פריטי הספרייה {0} יימחקו ממסד הנתונים ומהמערכת שלך. האם אתה בטוח?", + "MessageConfirmDeleteMetadataProvider": "האם למחוק את ספק המטא־נתונים המותאם \"{0}\"?", + "MessageConfirmDeleteNotification": "האם למחוק התראה זו?", "MessageConfirmDeleteSession": "האם אתה בטוח שאתה רוצה למחוק את ההפעלה הזו?", + "MessageConfirmEmbedMetadataInAudioFiles": "האם להטמיע מטא־נתונים ב־{0} קובצי שמע?", "MessageConfirmForceReScan": "האם אתה בטוח שאתה רוצה להכריח סריקה מחדש?", "MessageConfirmMarkAllEpisodesFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כהסתיימו?", "MessageConfirmMarkAllEpisodesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הפרקים כלא הסתיימו?", + "MessageConfirmMarkItemFinished": "האם לסמן את \"{0}\" כהושלם?", + "MessageConfirmMarkItemNotFinished": "האם לסמן את \"{0}\" כלא הושלם?", "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כהסתיימו?", "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא הסתיימו?", + "MessageConfirmNotificationTestTrigger": "האם להפעיל התראה זו עם נתוני בדיקה?", + "MessageConfirmPurgeCache": "ניקוי המטמון ימחק את כל התיקייה ב־/metadata/cache.

האם למחוק את תיקיית המטמון?", + "MessageConfirmPurgeItemsCache": "ניקוי מטמון הפריטים ימחק את כל התיקייה ב־metadata/cache/items/.
האם למחוק?", "MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך.

האם ברצונך להמשיך?", + "MessageConfirmQuickMatchEpisodes": "התאמה מהירה תדרוס פרטים עבור פרקים תואמים. רק פרקים ללא התאמה יעודכנו. האם להמשיך?", "MessageConfirmReScanLibraryItems": "האם אתה בטוח שברצונך לסרוק מחדש {0} פריטים?", "MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?", "MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?", diff --git a/client/strings/hr.json b/client/strings/hr.json index eccd12563..65150409f 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Br {0}", "LabelLibraryItem": "Stavka knjižnice", "LabelLibraryName": "Ime knjižnice", - "LabelLibrarySortByProgress": "Napredak: zadnje ažurirano", - "LabelLibrarySortByProgressFinished": "Napredak: završeno", - "LabelLibrarySortByProgressStarted": "Napredak: započeto", + "LabelLibrarySortByProgress": "Napredak: Zadnje ažuriranje", + "LabelLibrarySortByProgressFinished": "Napredak: Završeno", + "LabelLibrarySortByProgressStarted": "Napredak: Započeto", "LabelLimit": "Ograničenje", "LabelLineSpacing": "Razmak između redaka", "LabelListenAgain": "Ponovno poslušaj", diff --git a/client/strings/hu.json b/client/strings/hu.json index 574fd1c9b..4c36af869 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -16,7 +16,7 @@ "ButtonBrowseForFolder": "Mappa keresése", "ButtonCancel": "Mégse", "ButtonCancelEncode": "Kódolás megszakítása", - "ButtonChangeRootPassword": "Gyökérjelszó megváltoztatása", + "ButtonChangeRootPassword": "Root jelszó megváltoztatása", "ButtonCheckAndDownloadNewEpisodes": "Új epizódok ellenőrzése és letöltése", "ButtonChooseAFolder": "Válassz egy mappát", "ButtonChooseFiles": "Fájlok kiválasztása", @@ -205,7 +205,7 @@ "HeaderSleepTimer": "Alvásidőzítő", "HeaderStatsLargestItems": "Legnagyobb elemek", "HeaderStatsLongestItems": "Leghosszabb elemek (órában)", - "HeaderStatsMinutesListeningChart": "Hallgatási grafikon percekben (az elmúlt 7 napból)", + "HeaderStatsMinutesListeningChart": "Hallgatási grafikon percben (az elmúlt 7 napból)", "HeaderStatsRecentSessions": "Legutóbbi munkamenetek", "HeaderStatsTop10Authors": "Top 10 szerző", "HeaderStatsTop5Genres": "Top 5 műfaj", @@ -499,7 +499,7 @@ "LabelNumberOfEpisodes": "Epizódok száma", "LabelOpenIDAdvancedPermsClaimDescription": "Az OpenID-igény neve, amely a felhasználói műveletekre vonatkozó haladó jogosultságokat tartalmazza az alkalmazáson belül, és amely a nem adminisztrátori szerepkörökre vonatkozik (ha konfigurálva van). Ha az igény hiányzik a válaszból, az ABS-hez való hozzáférés megtagadásra kerül. Ha egyetlen opció hiányzik, azt false-ként fogja kezelni. Győződj meg arról, hogy az identitásszolgáltató igénye megfelel a várt struktúrának:", "LabelOpenIDClaims": "Hagyd üresen a következő opciókat, hogy letiltsd a haladó csoport- és jogosultság-hozzárendelést, ekkor automatikusan a ‘Felhasználó’ csoport kerül hozzárendelésre.", - "LabelOpenIDGroupClaimDescription": "Az OpenID-igény neve, amely a felhasználó csoportjainak listáját tartalmazza. Általában groups néven hivatkoznak rá. Ha konfigurálva van, az alkalmazás automatikusan hozzárendeli a szerepköröket a felhasználó csoporttagságai alapján, feltéve, hogy ezek a csoportok az igényben kis- és nagybetűkre érzéketlenül ‘admin’, ‘user’ vagy ‘guest’ néven szerepelnek. Az igénynek egy listát kell tartalmaznia, és ha egy felhasználó több csoport tagja, az alkalmazás a legmagasabb szintű hozzáféréssel rendelkező szerepkört rendeli hozzá. Ha egyetlen csoport sem felel meg, a hozzáférés megtagadásra kerül.", + "LabelOpenIDGroupClaimDescription": "Az OpenID-igény neve, amely a felhasználó csoportjainak listáját tartalmazza. Általában groups néven hivatkoznak rá. Ha konfigurálva van, az alkalmazás automatikusan hozzárendeli a szerepköröket a felhasználó csoporttagságai alapján, feltéve, hogy ezek a csoportok az igényben kis- és nagybetűkre érzéketlenül ‘admin’, ‘user’ vagy ‘guest’ néven szerepelnek. Az igénynek egy listát kell tartalmaznia, és ha egy felhasználó több csoport tagja, az alkalmazás a legmagasabb szintű hozzáféréssel rendelkező szerepkört rendeli hozzá. Ha egyetlen csoport sem felel meg, a hozzáférés megtagadásra kerül.", "LabelOpenRSSFeed": "RSS hírcsatorna megnyitása", "LabelOverwrite": "Felülírás", "LabelPaginationPageXOfY": "{0} oldal {1}-ból/ből", @@ -643,8 +643,8 @@ "LabelStatsAuthors": "Szerző", "LabelStatsBestDay": "Legjobb nap", "LabelStatsDailyAverage": "Napi átlag", - "LabelStatsDays": "Napok", - "LabelStatsDaysListened": "Hallgatással töltött napok", + "LabelStatsDays": "Nap", + "LabelStatsDaysListened": "Hallgatással töltött nap", "LabelStatsHours": "Órák", "LabelStatsInARow": "egymás után", "LabelStatsItemsFinished": "Befejezett elem", diff --git a/client/strings/it.json b/client/strings/it.json index 46ecb0f53..4b3921624 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -34,7 +34,7 @@ "ButtonEditChapters": "Modifica Capitoli", "ButtonEditPodcast": "Modifica Podcast", "ButtonEnable": "Abilita", - "ButtonFireAndFail": "Fire and Fail", + "ButtonFireAndFail": "Centro e fallimento", "ButtonFireOnTest": "Fire onTest event", "ButtonForceReScan": "Forza Re-Scan", "ButtonFullPath": "Percorso Completo", @@ -182,7 +182,7 @@ "HeaderPlaylist": "Playlist", "HeaderPlaylistItems": "Elementi della playlist", "HeaderPodcastsToAdd": "Podcasts da Aggiungere", - "HeaderPresets": "Presets", + "HeaderPresets": "Preimpostazioni", "HeaderPreviewCover": "Anteprima Cover", "HeaderRSSFeedGeneral": "Dettagli RSS", "HeaderRSSFeedIsOpen": "RSS Feed è aperto", @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Libri", "LabelButtonText": "Buttone Testo", - "LabelByAuthor": "da {0}", + "LabelByAuthor": "di {0}", "LabelChangePassword": "Cambia Password", "LabelChannels": "Canali", "LabelChapterCount": "{0} Capitoli", @@ -306,7 +306,7 @@ "LabelCustomCronExpression": "Espressione Cron personalizzata:", "LabelDatetime": "Data & Ora", "LabelDays": "Giorni", - "LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)", + "LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (despunta per rimuoverla solo dal database)", "LabelDescription": "Descrizione", "LabelDeselectAll": "Deseleziona Tutto", "LabelDetectedPattern": "Trovato pattern:", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Nessuno {0}", "LabelLibraryItem": "Elementi della biblioteca", "LabelLibraryName": "Nome della biblioteca", - "LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti", - "LabelLibrarySortByProgressFinished": "Progressi: Completati", - "LabelLibrarySortByProgressStarted": "Progressi: Iniziati", + "LabelLibrarySortByProgress": "Progresso: ultimo aggiornamento", + "LabelLibrarySortByProgressFinished": "Progresso: finito", + "LabelLibrarySortByProgressStarted": "Progresso: iniziato", "LabelLimit": "Limiti", "LabelLineSpacing": "Interlinea", "LabelListenAgain": "Ascolta ancora", @@ -497,7 +497,7 @@ "LabelNumberOfBooks": "Numero di libri", "LabelNumberOfChapters": "Numero di capitoli:", "LabelNumberOfEpisodes": "Numero di episodi", - "LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministratori (se configurato). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata comefalsa. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:", + "LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministrativi (se configurato). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata come falso. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:", "LabelOpenIDClaims": "Lasciare vuote le seguenti opzioni per disabilitare l'assegnazione avanzata di gruppi e autorizzazioni, assegnando quindi automaticamente il gruppo \"Utente\".", "LabelOpenIDGroupClaimDescription": "Nome dell'attestazione OpenID che contiene un elenco dei gruppi dell'utente. Comunemente indicato come gruppo. se configurato, l'applicazione assegnerà automaticamente i ruoli in base alle appartenenze ai gruppi dell'utente, a condizione che tali gruppi siano denominati \"admin\", \"utente\" o \"ospite\" senza distinzione tra maiuscole e minuscole nell'attestazione. L'attestazione deve contenere un elenco e, se un utente appartiene a più gruppi, l'applicazione assegnerà il ruolo corrispondente al livello di accesso più alto. Se nessun gruppo corrisponde, l'accesso verrà negato.", "LabelOpenRSSFeed": "Apri RSS Feed", @@ -530,7 +530,7 @@ "LabelPrimaryEbook": "Libro principale", "LabelProgress": "Cominciati", "LabelProvider": "Fornitore", - "LabelProviderAuthorizationValue": "Authorization Header Value", + "LabelProviderAuthorizationValue": "Valore intestazione di autorizzazione", "LabelPubDate": "Data di pubblicazione", "LabelPublishYear": "Anno di pubblicazione", "LabelPublishedDate": "Pubblicati {0}", @@ -674,7 +674,7 @@ "LabelTimeDurationXMinutes": "{0} minuti", "LabelTimeDurationXSeconds": "{0} secondi", "LabelTimeInMinutes": "Tempo in minuti", - "LabelTimeLeft": "{0} sinistra", + "LabelTimeLeft": "{0} rimasti", "LabelTimeListened": "Tempo di Ascolto", "LabelTimeListenedToday": "Tempo di Ascolto Oggi", "LabelTimeRemaining": "{0} rimanente", @@ -682,7 +682,7 @@ "LabelTitle": "Titolo", "LabelToolsEmbedMetadata": "Incorpora Metadata", "LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.", - "LabelToolsM4bEncoder": "M4B Encoder", + "LabelToolsM4bEncoder": "Codificatore M4B", "LabelToolsMakeM4b": "Crea un file M4B", "LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.", "LabelToolsSplitM4b": "Converti M4B in MP3", @@ -854,7 +854,7 @@ "MessageNoItems": "Nessun oggetto", "MessageNoItemsFound": "Nessun oggetto trovato", "MessageNoListeningSessions": "Nessuna sessione di ascolto", - "MessageNoLogs": "Nessun Log", + "MessageNoLogs": "Nessun rapporto", "MessageNoMediaProgress": "Nessun progresso multimediale", "MessageNoNotifications": "Nessuna notifica", "MessageNoPodcastFeed": "Podcast non valido: nessun feed", @@ -1109,7 +1109,7 @@ "ToastProgressIsNotBeingSynced": "L'avanzamento non è sincronizzato, riavviare la riproduzione", "ToastProviderCreatedFailed": "Impossibile aggiungere il provider", "ToastProviderCreatedSuccess": "Aggiunto nuovo provider", - "ToastProviderNameAndUrlRequired": "Nome e URL richiesti", + "ToastProviderNameAndUrlRequired": "Nome e Url richiesti", "ToastProviderRemoveSuccess": "Provider rimosso", "ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS", "ToastRSSFeedCloseSuccess": "Flusso RSS chiuso", diff --git a/client/strings/ja.json b/client/strings/ja.json index 7c2acb889..95cfba2b8 100644 --- a/client/strings/ja.json +++ b/client/strings/ja.json @@ -16,7 +16,7 @@ "ButtonBrowseForFolder": "フォルダーを選択する", "ButtonCancel": "キャンセル", "ButtonCancelEncode": "エンコードを取り消す", - "ButtonChangeRootPassword": "Rootのパスワードを変更する", + "ButtonChangeRootPassword": "ルートのパスワードを変更する", "ButtonCheckAndDownloadNewEpisodes": "新しいエピソードを確認してダウンロード", "ButtonChooseAFolder": "フォルダーを選ぶ", "ButtonChooseFiles": "ファイルを選ぶ", @@ -29,7 +29,7 @@ "ButtonCreate": "作成", "ButtonCreateBackup": "バックアップを作成する", "ButtonDelete": "削除", - "ButtonDownloadQueue": "次に再生", + "ButtonDownloadQueue": "ダウンロードキュー", "ButtonEdit": "編集", "ButtonEditChapters": "チャプターの編集", "ButtonEditPodcast": "ポッドキャストの編集", @@ -52,9 +52,9 @@ "ButtonMatchAllAuthors": "すべての作者と紐付け", "ButtonMatchBooks": "本と紐付け", "ButtonNevermind": "中止", - "ButtonNext": "次", + "ButtonNext": "次へ", "ButtonNextChapter": "次のチャプター", - "ButtonNextItemInQueue": "キューの中の次のアイテム", + "ButtonNextItemInQueue": "キューにある次のアイテム", "ButtonOk": "はい", "ButtonOpenFeed": "フィードを開く", "ButtonOpenManager": "管理画面を開く", @@ -63,120 +63,1103 @@ "ButtonPlayAll": "全て再生", "ButtonPlaying": "プレイ中", "ButtonPlaylists": "プレイリスト", - "ButtonPrevious": "先", + "ButtonPrevious": "前へ", "ButtonPreviousChapter": "前のチャプター", "ButtonProbeAudioFile": "オーディオファイルを解析", "ButtonPurgeAllCache": "全てのキャッシュを削除", "ButtonPurgeItemsCache": "項目のキャッシュを削除", "ButtonQueueAddItem": "次に再生する", - "ButtonQueueRemoveItem": "次に再生から削除", + "ButtonQueueRemoveItem": "「次に再生」から削除", "ButtonQuickEmbed": "クイック埋め込み", "ButtonQuickEmbedMetadata": "メタデータの埋め込み", + "ButtonQuickMatch": "クイックマッチ", "ButtonReScan": "再スキャン", "ButtonRead": "読む", - "ButtonReadLess": "閉じる", + "ButtonReadLess": "少なく表示", "ButtonReadMore": "もっと見る", "ButtonRefresh": "再読み込み", "ButtonRemove": "削除", "ButtonRemoveAll": "全て削除", "ButtonRemoveAllLibraryItems": "ライブラリーの項目を全て削除", + "ButtonRemoveFromContinueListening": "「続きを聴く」から削除", + "ButtonRemoveFromContinueReading": "「続きを読む」から削除", + "ButtonRemoveSeriesFromContinueSeries": "「シリーズを続ける」からシリーズを削除", "ButtonReset": "元に戻す", "ButtonResetToDefault": "デフォルトに戻す", "ButtonRestore": "復元", "ButtonSave": "保存", "ButtonSaveAndClose": "保存して閉じる", + "ButtonSaveTracklist": "トラックリストを保存", "ButtonScan": "スキャン", "ButtonScanLibrary": "ライブラリーをスキャン", "ButtonScrollLeft": "左にスクロール", "ButtonScrollRight": "右にスクロール", "ButtonSearch": "検索", + "ButtonSelectFolderPath": "保存先フォルダを選択", "ButtonSeries": "シリーズ", + "ButtonSetChaptersFromTracks": "トラックからチャプターを設定する", + "ButtonShare": "共有", + "ButtonShiftTimes": "再生時間の移動", + "ButtonShow": "表示", + "ButtonStartM4BEncode": "M4Bエンコード開始", + "ButtonStartMetadataEmbed": "メタデータ埋め込み開始", + "ButtonStats": "統計", "ButtonSubmit": "送信", + "ButtonTest": "テスト", + "ButtonUnlinkOpenId": "OpenID 連携解除", + "ButtonUpload": "アップロード", + "ButtonUploadBackup": "バックアップをアップロード", + "ButtonUploadCover": "カバー画像をアップロード", + "ButtonUploadOPMLFile": "OPMLファイルをアップロード", + "ButtonUserDelete": "ユーザーを削除 {0}", + "ButtonUserEdit": "ユーザーを編集 {0}", + "ButtonViewAll": "すべて表示", "ButtonYes": "はい", + "ErrorUploadFetchMetadataAPI": "メタデータの取得中にエラーが発生しました", + "ErrorUploadFetchMetadataNoResults": "メタデータの取得に失敗しました。タイトルや作者名を更新してください", + "ErrorUploadLacksTitle": "タイトルは必須です", "HeaderAccount": "アカウント", + "HeaderAddCustomMetadataProvider": "カスタムメタデータプロバイダーを追加", "HeaderAdvanced": "上級者向け", + "HeaderApiKeys": "APIキー", + "HeaderAppriseNotificationSettings": "Apprise 通知設定", "HeaderAudioTracks": "オーディオトラック", + "HeaderAudiobookTools": "オーディオブックのファイル管理ツール", + "HeaderAuthentication": "認証", + "HeaderBackups": "バックアップ", + "HeaderBulkChapterModal": "チャプターをまとめて追加", + "HeaderChangePassword": "パスワードを変更", "HeaderChapters": "チャプター", + "HeaderChooseAFolder": "フォルダを選択", "HeaderCollection": "コレクション", "HeaderCollectionItems": "コレクションの項目", + "HeaderCover": "カバー", + "HeaderCurrentDownloads": "現在のダウンロード", + "HeaderCustomMessageOnLogin": "ログイン時のカスタムメッセージ", + "HeaderCustomMetadataProviders": "カスタムメタデータプロバイダー", "HeaderDetails": "詳細", + "HeaderDownloadQueue": "ダウンロード待ち", "HeaderEbookFiles": "電子書籍ファイル", + "HeaderEmail": "メール", + "HeaderEmailSettings": "メール設定", "HeaderEpisodes": "エピソード", + "HeaderEreaderDevices": "電子書籍リーダー端末", "HeaderEreaderSettings": "電子書籍リーダーの設定", + "HeaderFiles": "ファイル", + "HeaderFindChapters": "チャプターを検索", + "HeaderIgnoredFiles": "無視されたファイル", + "HeaderItemFiles": "アイテムファイル", + "HeaderItemMetadataUtils": "アイテムメタデータユーティリティ", + "HeaderLastListeningSession": "直近の再生セッション", "HeaderLatestEpisodes": "最新のエピソード", "HeaderLibraries": "ライブラリー", + "HeaderLibraryFiles": "ライブラリーファイル", + "HeaderLibraryStats": "ライブラリー統計", + "HeaderListeningSessions": "再生セッション", + "HeaderListeningStats": "再生統計", + "HeaderLogin": "ログイン", + "HeaderLogs": "ログ", + "HeaderManageGenres": "ジャンルを管理", + "HeaderManageTags": "タグを管理", + "HeaderMapDetails": "マップの詳細", + "HeaderMatch": "マッチ", + "HeaderMetadataOrderOfPrecedence": "メタデータの優先順", + "HeaderMetadataToEmbed": "埋め込むメタデータ", + "HeaderNewAccount": "新規アカウント", + "HeaderNewApiKey": "新規APIキー", + "HeaderNewLibrary": "新規ライブラリー", + "HeaderNotificationCreate": "通知を作成", + "HeaderNotificationUpdate": "通知を更新", + "HeaderNotifications": "通知", + "HeaderOpenIDConnectAuthentication": "OpenID Connect 認証", + "HeaderOpenListeningSessions": "再生中のセッション", "HeaderOpenRSSFeed": "RSS Feedを開く", + "HeaderOtherFiles": "その他のファイル", + "HeaderPasswordAuthentication": "パスワード認証", + "HeaderPermissions": "権限", + "HeaderPlayerQueue": "次に再生", "HeaderPlayerSettings": "プレーヤーの設定", "HeaderPlaylist": "プレイリスト", "HeaderPlaylistItems": "プレイリストアイテム", + "HeaderPodcastsToAdd": "追加するポッドキャスト", + "HeaderPresets": "プリセット", + "HeaderPreviewCover": "カバープレビュー", "HeaderRSSFeedGeneral": "RSS 詳細", + "HeaderRSSFeedIsOpen": "RSSフィードが開いています", + "HeaderRSSFeeds": "RSSフィード", + "HeaderRemoveEpisode": "エピソードを削除", + "HeaderRemoveEpisodes": "{0}個のエピソードを削除", + "HeaderSavedMediaProgress": "保存済みメディア進捗", + "HeaderSchedule": "スケジュール", + "HeaderScheduleEpisodeDownloads": "エピソード自動ダウンロードスケジュール", + "HeaderScheduleLibraryScans": "ライブラリー自動スキャンスケジュール", + "HeaderSession": "セッション", + "HeaderSetBackupSchedule": "バックアップスケジュールの設定", "HeaderSettings": "設定", + "HeaderSettingsDisplay": "表示", + "HeaderSettingsExperimental": "実験的な機能", "HeaderSettingsGeneral": "一般", "HeaderSettingsScanner": "スキャナー", + "HeaderSettingsSecurity": "セキュリティ", + "HeaderSettingsWebClient": "Webクライアント", "HeaderSleepTimer": "スリープタイマー", - "HeaderStatsMinutesListeningChart": "過去7日間の視聴時間(分)", + "HeaderStatsLargestItems": "最大のアイテム", + "HeaderStatsLongestItems": "最長のアイテム(時間)", + "HeaderStatsMinutesListeningChart": "過去7日間のリスニング時間(分)", + "HeaderStatsRecentSessions": "最近の再生履歴", + "HeaderStatsTop10Authors": "上位10作者", + "HeaderStatsTop5Genres": "上位5ジャンル", + "HeaderTableOfContents": "目次", + "HeaderTools": "ツール", + "HeaderUpdateAccount": "アカウントの更新", + "HeaderUpdateApiKey": "APIキーの更新", + "HeaderUpdateAuthor": "作者の更新", + "HeaderUpdateDetails": "詳細の更新", + "HeaderUpdateLibrary": "ライブラリーの更新", + "HeaderUsers": "ユーザー", + "HeaderYearReview": "{0}年の振り返り", + "HeaderYourStats": "再生統計", + "LabelAbridged": "要約版", + "LabelAbridgedChecked": "要約版(チェック済み)", + "LabelAbridgedUnchecked": "完全版(未チェック)", + "LabelAccessibleBy": "アクセス可能なユーザー", + "LabelAccountType": "アカウントの種類", + "LabelAccountTypeAdmin": "管理者", + "LabelAccountTypeGuest": "ゲスト", + "LabelAccountTypeUser": "ユーザー", + "LabelActivities": "アクティビティー", + "LabelActivity": "アクティビティー", + "LabelAddToCollection": "コレクションに追加", + "LabelAddToCollectionBatch": "コレクションに{0}冊追加", "LabelAddToPlaylist": "プレイリストの追加", - "LabelAuthor": "著者", - "LabelAuthorFirstLast": "著者(名 氏)", - "LabelAuthorLastFirst": "著者(氏 名)", - "LabelAuthors": "著者", + "LabelAddToPlaylistBatch": "プレイリストに{0}件追加", + "LabelAddedAt": "追加日時", + "LabelAddedDate": "追加日時 {0}", + "LabelAdminUsersOnly": "管理者ユーザーのみ", + "LabelAll": "すべて", + "LabelAllEpisodesDownloaded": "すべてのエピソードをダウンロード済み", + "LabelAllUsers": "すべてのユーザー", + "LabelAllUsersExcludingGuests": "ゲストを除くすべてのユーザー", + "LabelAllUsersIncludingGuests": "ゲストを含むすべてのユーザー", + "LabelAlreadyInYourLibrary": "すでにライブラリーにあります", + "LabelApiKeyCreated": "APIキー \"{0}\" が正常に作成されました。", + "LabelApiKeyCreatedDescription": "このAPIキーを今すぐコピーしてください。二度と確認できません。", + "LabelApiKeyUser": "ユーザーの代わりに動作", + "LabelApiKeyUserDescription": "このAPIキーは代わりに動作するユーザーと同じ権限を持ちます。ログではユーザーがリクエストを行っているのと同じように表示されます。", + "LabelApiToken": "APIトークン", + "LabelAppend": "追記", + "LabelAudioBitrate": "オーディオビットレート(例: 128k)", + "LabelAudioChannels": "オーディオチャンネル(1または2)", + "LabelAudioCodec": "オーディオコーデック", + "LabelAuthor": "作者", + "LabelAuthorFirstLast": "作者(名 氏)", + "LabelAuthorLastFirst": "作者(氏 名)", + "LabelAuthors": "作者", "LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード", - "LabelBooks": "ほん", + "LabelAutoFetchMetadata": "メタデータの自動取得", + "LabelAutoFetchMetadataHelp": "タイトル・作者・シリーズのメタデータを取得してアップロードを効率化します。アップロード後に追加のメタデータのマッチングが必要になることがあります。", + "LabelAutoLaunch": "自動起動", + "LabelAutoLaunchDescription": "ログインページにアクセスすると自動的に認証プロバイダーにリダイレクトします(手動上書きパス /login?autoLaunch=0)", + "LabelAutoRegister": "自動登録", + "LabelAutoRegisterDescription": "ログイン後に新しいユーザーを自動的に作成", + "LabelBackToUser": "ユーザーに戻る", + "LabelBackupAudioFiles": "オーディオファイルをバックアップ", + "LabelBackupLocation": "バックアップ場所", + "LabelBackupsEnableAutomaticBackups": "自動バックアップ", + "LabelBackupsEnableAutomaticBackupsHelp": "/metadata/backups に保存されたバックアップ", + "LabelBackupsMaxBackupSize": "最大バックアップサイズ(GB)(無制限は0)", + "LabelBackupsMaxBackupSizeHelp": "設定ミスを防ぐため、バックアップが設定サイズを超えると失敗します。", + "LabelBackupsNumberToKeep": "保持するバックアップ数", + "LabelBackupsNumberToKeepHelp": "一度に1つのバックアップのみ削除されます。すでにこれより多くのバックアップがある場合は手動で削除してください。", + "LabelBitrate": "ビットレート", + "LabelBonus": "ボーナス", + "LabelBooks": "本", + "LabelButtonText": "ボタンテキスト", + "LabelByAuthor": "by {0}", + "LabelChangePassword": "パスワードを変更", + "LabelChannels": "チャンネル", + "LabelChapterCount": "{0}チャプター", + "LabelChapterTitle": "チャプタータイトル", "LabelChapters": "チャプター", - "LabelClosePlayer": "プレイヤーを閉じる", + "LabelChaptersFound": "チャプターが見つかりました", + "LabelClickForMoreInfo": "クリックして詳細を表示", + "LabelClickToUseCurrentValue": "クリックして現在の値を使用", + "LabelClosePlayer": "プレーヤーを閉じる", + "LabelCodec": "コーデック", + "LabelCollapseSeries": "シリーズを折りたたむ", + "LabelCollapseSubSeries": "サブシリーズを折りたたむ", + "LabelCollection": "コレクション", + "LabelCollections": "コレクション", "LabelComplete": "完了", - "LabelContinueListening": "続きから聞く", + "LabelConfirmPassword": "パスワード確認", + "LabelContinueListening": "続きから聴く", + "LabelContinueReading": "続きを読む", + "LabelContinueSeries": "シリーズを続ける", + "LabelCorsAllowed": "許可されたCORSオリジン", + "LabelCover": "カバー", + "LabelCoverImageURL": "カバー画像URL", + "LabelCoverProvider": "カバープロバイダー", + "LabelCreatedAt": "作成日時", + "LabelCronExpression": "Cron式", + "LabelCurrent": "現在", + "LabelCurrently": "現在:", + "LabelCustomCronExpression": "カスタムCron式:", + "LabelDatetime": "日時", + "LabelDays": "日", + "LabelDeleteFromFileSystemCheckbox": "ファイルシステムから削除(チェック解除するとデータベースからのみ削除)", "LabelDescription": "説明", + "LabelDeselectAll": "すべて解除", + "LabelDetectedPattern": "検出されたパターン:", + "LabelDevice": "デバイス", + "LabelDeviceInfo": "デバイス情報", + "LabelDeviceIsAvailableTo": "デバイスが使用可能な対象...", + "LabelDirectory": "ディレクトリ", + "LabelDiscFromFilename": "ファイル名からディスク", + "LabelDiscFromMetadata": "メタデータからディスク", + "LabelDiscover": "おすすめ", "LabelDownload": "ダウンロード", + "LabelDownloadNEpisodes": "{0}エピソードをダウンロード", + "LabelDownloadable": "ダウンロード可能", "LabelDuration": "長さ", - "LabelEbook": "Eブック", - "LabelEbooks": "Eブック", + "LabelDurationComparisonExactMatch": "(完全一致)", + "LabelDurationComparisonLonger": "({0} 長い)", + "LabelDurationComparisonShorter": "({0} 短い)", + "LabelDurationFound": "見つかった長さ:", + "LabelEbook": "電子書籍", + "LabelEbooks": "電子書籍", + "LabelEdit": "編集", + "LabelEmail": "メール", + "LabelEmailSettingsFromAddress": "送信元アドレス", + "LabelEmailSettingsRejectUnauthorized": "未認証の証明書を拒否", + "LabelEmailSettingsRejectUnauthorizedHelp": "SSL証明書の検証を無効にすると、中間者攻撃などのセキュリティリスクにさらされる可能性があります。接続するメールサーバーを信頼している場合にのみ無効にしてください。", + "LabelEmailSettingsSecure": "セキュア", + "LabelEmailSettingsSecureHelp": "trueの場合、サーバーへの接続にTLSを使用します。falseの場合、サーバーがSTARTTLS拡張をサポートしていればTLSが使用されます。ポート465に接続する場合はtrueに設定してください。ポート587または25の場合はfalseのままにしてください。(nodemailer.com/smtp/#authentication より)", + "LabelEmailSettingsTestAddress": "テストアドレス", + "LabelEmbeddedCover": "埋め込みカバー", "LabelEnable": "有効", + "LabelEncodingBackupLocation": "元のオーディオファイルのバックアップは以下に保存されます:", + "LabelEncodingChaptersNotEmbedded": "マルチトラックのオーディオブックにはチャプターが埋め込まれません。", + "LabelEncodingClearItemCache": "定期的にアイテムキャッシュを削除してください。", + "LabelEncodingFinishedM4B": "完成したM4Bはオーディオブックフォルダーに配置されます:", + "LabelEncodingInfoEmbedded": "メタデータはオーディオブックフォルダー内のオーディオトラックに埋め込まれます。", + "LabelEncodingStartedNavigation": "タスクが開始されたら、このページから離れることができます。", + "LabelEncodingTimeWarning": "エンコードには最大30分かかる場合があります。", + "LabelEncodingWarningAdvancedSettings": "警告: ffmpegエンコードオプションに詳しくない場合はこれらの設定を変更しないでください。", + "LabelEncodingWatcherDisabled": "ウォッチャーを無効にしている場合は、後でこのオーディオブックを再スキャンする必要があります。", + "LabelEnd": "終了", "LabelEndOfChapter": "チャプターの最後", "LabelEpisode": "エピソード", - "LabelFeedURL": "Feed URL", + "LabelEpisodeNotLinkedToRssFeed": "RSSフィードにリンクされていないエピソード", + "LabelEpisodeNumber": "エピソード#{0}", + "LabelEpisodeTitle": "エピソードタイトル", + "LabelEpisodeType": "エピソードタイプ", + "LabelEpisodeUrlFromRssFeed": "RSSフィードのエピソードURL", + "LabelEpisodes": "エピソード", + "LabelEpisodic": "エピソード", + "LabelExample": "例", + "LabelExpandSeries": "シリーズを展開", + "LabelExpandSubSeries": "サブシリーズを展開", + "LabelExpired": "期限切れ", + "LabelExpiresAt": "有効期限", + "LabelExpiresInSeconds": "有効期限(秒)", + "LabelExpiresNever": "なし", + "LabelExplicit": "露骨な表現", + "LabelExplicitChecked": "露骨な表現あり(チェック済み)", + "LabelExplicitUnchecked": "露骨な表現なし(未チェック)", + "LabelExportOPML": "OPMLをエクスポート", + "LabelFeedURL": "フィードURL", + "LabelFetchingMetadata": "メタデータ取得中", "LabelFile": "ファイル", + "LabelFileBirthtime": "ファイル作成日時", + "LabelFileBornDate": "{0}に生成", + "LabelFileModified": "ファイル更新日時", + "LabelFileModifiedDate": "{0}に更新", "LabelFilename": "ファイル名", + "LabelFilterByUser": "ユーザーでフィルター", + "LabelFindEpisodes": "エピソードを検索", "LabelFinished": "完了", - "LabelFolder": "フォルダ", + "LabelFinishedDate": "{0}に完了", + "LabelFolder": "フォルダー", + "LabelFolders": "フォルダー", + "LabelFontBold": "太字", "LabelFontBoldness": "フォントの太さ", + "LabelFontFamily": "フォントファミリー", + "LabelFontItalic": "斜体", "LabelFontScale": "フォントサイズ", + "LabelFontStrikethrough": "打消線", + "LabelFormat": "形式", + "LabelFull": "完全", "LabelGenre": "ジャンル", "LabelGenres": "ジャンル", + "LabelHardDeleteFile": "ファイルを完全削除", + "LabelHasEbook": "電子書籍あり", + "LabelHasSupplementaryEbook": "付属電子書籍あり", + "LabelHideSubtitles": "字幕を非表示", + "LabelHighestPriority": "最高優先度", "LabelHost": "ホスト", + "LabelHour": "時間", + "LabelHours": "時間", + "LabelIcon": "アイコン", + "LabelImageURLFromTheWeb": "ウェブからの画像URL", "LabelInProgress": "進行中", + "LabelIncludeInTracklist": "トラックリストに含める", + "LabelIncomplete": "未完了", + "LabelInterval": "間隔", + "LabelIntervalCustomDailyWeekly": "カスタム(毎日/曜日指定)", + "LabelIntervalEvery12Hours": "12時間ごと", + "LabelIntervalEvery15Minutes": "15分ごと", + "LabelIntervalEvery2Hours": "2時間ごと", + "LabelIntervalEvery30Minutes": "30分ごと", + "LabelIntervalEvery6Hours": "6時間ごと", + "LabelIntervalEveryDay": "毎日", + "LabelIntervalEveryHour": "1時間ごと", + "LabelIntervalEveryMinute": "1分ごと", + "LabelInvert": "反転", + "LabelItem": "アイテム", + "LabelJumpBackwardAmount": "巻き戻し量", + "LabelJumpForwardAmount": "早送り量", "LabelLanguage": "言語", + "LabelLanguageDefaultServer": "デフォルトサーバー言語", "LabelLanguages": "言語", + "LabelLastBookAdded": "最後に追加された本", + "LabelLastBookUpdated": "最後に更新された本", + "LabelLastProgressDate": "最終進捗: {0}", + "LabelLastSeen": "最終確認", + "LabelLastTime": "終了時刻", + "LabelLastUpdate": "最終更新", "LabelLayout": "レイアウト", "LabelLayoutSinglePage": "単ページ", + "LabelLayoutSplitPage": "ページを分割", + "LabelLess": "少ない", + "LabelLibrariesAccessibleToUser": "ユーザーがアクセス可能なライブラリー", + "LabelLibrary": "ライブラリー", + "LabelLibraryFilterSublistEmpty": "{0}がない", + "LabelLibraryItem": "ライブラリーアイテム", + "LabelLibraryName": "ライブラリー名", + "LabelLibrarySortByProgress": "進捗: 最終更新", + "LabelLibrarySortByProgressFinished": "進捗: 完了", + "LabelLibrarySortByProgressStarted": "進捗: 開始済み", + "LabelLimit": "制限", "LabelLineSpacing": "行間", - "LabelListenAgain": "再度視聴", + "LabelListenAgain": "もう一度聴く", + "LabelLogLevelDebug": "デバッグ", + "LabelLogLevelInfo": "情報", + "LabelLogLevelWarn": "警告", + "LabelLookForNewEpisodesAfterDate": "この日以降の新しいエピソードを検索", + "LabelLowestPriority": "最低優先度", + "LabelMatchConfidence": "信頼度", + "LabelMatchExistingUsersBy": "既存ユーザーの照合方法", + "LabelMatchExistingUsersByDescription": "既存ユーザーの接続に使用されます。接続後はSSOプロバイダーの一意のIDでユーザーが照合されます", + "LabelMaxEpisodesToDownload": "最大ダウンロードエピソード数(無制限は0)。", + "LabelMaxEpisodesToDownloadPerCheck": "1回のチェックでダウンロードする最大の新着エピソード数", + "LabelMaxEpisodesToKeep": "保持する最大エピソード数", + "LabelMaxEpisodesToKeepHelp": "0は上限なし。新しいエピソードの自動ダウンロード後、X件以上のエピソードがある場合は最も古いエピソードを削除します。1回のダウンロードにつき1件のみ削除されます。", + "LabelMediaPlayer": "メディアプレーヤー", "LabelMediaType": "メディアの種類", + "LabelMetaTag": "メタタグ", + "LabelMetaTags": "メタタグ", + "LabelMetadataOrderOfPrecedenceDescription": "優先度の高いメタデータソースが優先度の低いメタデータソースを上書きします", + "LabelMetadataProvider": "メタデータプロバイダー", + "LabelMinute": "分", + "LabelMinutes": "分", + "LabelMissing": "消失", + "LabelMissingEbook": "電子書籍なし", + "LabelMissingSupplementaryEbook": "付属電子書籍なし", + "LabelMobileRedirectURIs": "許可されたモバイルリダイレクトURI", + "LabelMobileRedirectURIsDescription": "これはモバイルアプリの有効なリダイレクトURIのホワイトリストです。デフォルトは audiobookshelf://oauth です。削除するか、サードパーティーアプリ統合用の追加URIで補足できます。アスタリスク(*)を唯一のエントリとして使用すると、任意のURIが許可されます。", + "LabelMore": "多い", "LabelMoreInfo": "追加情報", - "LabelName": "名", + "LabelName": "名前", "LabelNarrator": "ナレーター", "LabelNarrators": "ナレーター", "LabelNew": "新しい", - "LabelNewPassword": "新しいのパスワード", - "LabelNewestAuthors": "最新の著者", + "LabelNewPassword": "新しいパスワード", + "LabelNewestAuthors": "最新の作者", "LabelNewestEpisodes": "最新エピソード", + "LabelNextBackupDate": "次回バックアップ日", + "LabelNextChapters": "次のチャプター:", + "LabelNextScheduledRun": "次のスケジュール実行", + "LabelNoApiKeys": "APIキーなし", + "LabelNoCustomMetadataProviders": "カスタムメタデータプロバイダーなし", + "LabelNoEpisodesSelected": "エピソードが選択されていません", + "LabelNotFinished": "未完了", + "LabelNotStarted": "未開始", + "LabelNotes": "ノート", + "LabelNotificationAppriseURL": "Apprise URL", + "LabelNotificationAvailableVariables": "利用可能な変数", + "LabelNotificationBodyTemplate": "本文テンプレート", + "LabelNotificationEvent": "通知イベント", + "LabelNotificationTitleTemplate": "タイトルテンプレート", + "LabelNotificationsMaxFailedAttempts": "最大失敗試行回数", + "LabelNotificationsMaxFailedAttemptsHelp": "この回数失敗すると通知は無効になります", + "LabelNotificationsMaxQueueSize": "通知イベントの最大キューサイズ", + "LabelNotificationsMaxQueueSizeHelp": "イベントは1秒に1回の発火に制限されます。キューが最大サイズの場合、イベントは無視されます。これは通知スパムを防ぎます。", + "LabelNumberOfBooks": "本の冊数", + "LabelNumberOfChapters": "チャプター数:", + "LabelNumberOfEpisodes": "エピソード数", + "LabelOpenIDAdvancedPermsClaimDescription": "管理者以外のロールに適用されるアプリケーション内のユーザーアクション用の高度な権限を含むOpenIDクレーム名(設定されている場合)。クレームがレスポンスに含まれていない場合、ABSへのアクセスは拒否されます。単一のオプションがない場合は false として扱われます。IDプロバイダーのクレームが期待される構造と一致することを確認してください:", + "LabelOpenIDClaims": "以下のオプションを空のままにすると、高度なグループと権限の割り当てが無効になり、自動的に「User」グループが割り当てられます。", + "LabelOpenIDGroupClaimDescription": "ユーザーのグループリストを含むOpenIDクレーム名。一般的には groups と呼ばれます。設定されている場合、アプリケーションはユーザーのグループメンバーシップに基づいて自動的にロールを割り当てます。クレームのグループ名が大文字小文字を区別せずに「admin」「user」「guest」である場合、対応するロールが割り当てられます。グループが一致しない場合、アクセスは拒否されます。", + "LabelOpenRSSFeed": "RSSフィードを開く", + "LabelOverwrite": "上書き", + "LabelPaginationPageXOfY": "{0}/{1}ページ", "LabelPassword": "パスワード", "LabelPath": "パス", + "LabelPermanent": "無期限", + "LabelPermissionsAccessAllLibraries": "すべてのライブラリーにアクセス可能", + "LabelPermissionsAccessAllTags": "すべてのタグにアクセス可能", + "LabelPermissionsAccessExplicitContent": "露骨なコンテンツにアクセス可能", + "LabelPermissionsCreateEreader": "電子書籍リーダー作成可能", + "LabelPermissionsDelete": "削除可能", + "LabelPermissionsDownload": "ダウンロード可能", + "LabelPermissionsUpdate": "更新可能", + "LabelPermissionsUpload": "アップロード可能", + "LabelPersonalYearReview": "個人の年間振り返り({0})", + "LabelPhotoPathURL": "写真パス/URL", + "LabelPlayMethod": "再生方法", + "LabelPlaybackRateIncrementDecrement": "再生速度の増減量", + "LabelPlayerChapterNumberMarker": "{0}/{1}", "LabelPlaylists": "プレイリスト", "LabelPodcast": "ポッドキャスト", + "LabelPodcastSearchRegion": "ポッドキャスト検索地域", + "LabelPodcastType": "ポッドキャストタイプ", "LabelPodcasts": "ポッドキャスト", + "LabelPort": "ポート", + "LabelPrefixesToIgnore": "無視するプレフィックス(大文字小文字を区別しない)", "LabelPreventIndexing": "フィードがiTunesおよびGoogleのポッドキャストディレクトリにインデックス登録されるのを防ぎます", + "LabelPrimaryEbook": "メイン電子書籍", + "LabelProgress": "進捗", + "LabelProvider": "プロバイダー", + "LabelProviderAuthorizationValue": "認証ヘッダーの値", + "LabelPubDate": "公開日", "LabelPublishYear": "公開年", + "LabelPublishedDate": "{0}発行", + "LabelPublishedDecade": "発行年代", + "LabelPublishedDecades": "発行年代", + "LabelPublisher": "出版社", + "LabelPublishers": "出版社", + "LabelRSSFeedCustomOwnerEmail": "カスタムオーナーメール", + "LabelRSSFeedCustomOwnerName": "カスタムオーナー名", + "LabelRSSFeedOpen": "RSSフィードを公開", + "LabelRSSFeedPreventIndexing": "インデックス化を防止", + "LabelRSSFeedSlug": "RSSフィードスラグ", + "LabelRSSFeedURL": "RSSフィードURL", + "LabelRandomly": "ランダム", + "LabelReAddSeriesToContinueListening": "シリーズを再追加して「続きを聴く」に戻す", + "LabelRead": "読む", + "LabelReadAgain": "もう一度読む", + "LabelReadEbookWithoutProgress": "進捗を記録せずに電子書籍を読む", + "LabelRecentSeries": "最近のシリーズ", + "LabelRecentlyAdded": "最近追加", + "LabelRecommended": "おすすめ", + "LabelRedo": "やり直す", + "LabelRegion": "地域", + "LabelReleaseDate": "リリース日", + "LabelRemoveAllMetadataAbs": "すべてのmetadata.absファイルを削除", + "LabelRemoveAllMetadataJson": "すべてのmetadata.jsonファイルを削除", + "LabelRemoveAudibleBranding": "Audibleのイントロとアウトロをチャプターから削除", + "LabelRemoveCover": "カバーを削除", + "LabelRemoveMetadataFile": "ライブラリーアイテムフォルダー内のメタデータファイルを削除", + "LabelRemoveMetadataFileHelp": "{0}フォルダー内のすべてのmetadata.jsonおよびmetadata.absファイルを削除します。", + "LabelRowsPerPage": "1ページあたりの行数", + "LabelSearchTerm": "検索ワード", + "LabelSearchTitle": "タイトルを検索", + "LabelSearchTitleOrASIN": "タイトルまたはASINを検索", + "LabelSeason": "シーズン", + "LabelSeasonNumber": "シーズン#{0}", + "LabelSelectAll": "すべて選択", + "LabelSelectAllEpisodes": "すべてのエピソードを選択", + "LabelSelectEpisodesShowing": "表示中の{0}件のエピソードを選択", + "LabelSelectUser": "ユーザーを選択", + "LabelSelectUsers": "ユーザーを選択", + "LabelSendEbookToDevice": "電子書籍を送信...", + "LabelSequence": "シーケンス", + "LabelSerial": "シリアル", + "LabelSeries": "シリーズ", + "LabelSeriesName": "シリーズ名", + "LabelSeriesProgress": "シリーズ進捗", + "LabelServerLogLevel": "サーバーログレベル", + "LabelServerYearReview": "サーバーの年間振り返り({0})", + "LabelSetEbookAsPrimary": "メインとして設定", + "LabelSetEbookAsSupplementary": "付属として設定", + "LabelSettingsAllowIframe": "iframeへの埋め込みを許可", + "LabelSettingsAudiobooksOnly": "オーディオブックのみ", + "LabelSettingsAudiobooksOnlyHelp": "この設定を有効にすると電子書籍ファイルは無視されます。ただしオーディオブックフォルダー内の電子書籍は付属電子書籍として設定されます", + "LabelSettingsBookshelfViewHelp": "木製の棚を備えたスキューモーフィックデザイン", + "LabelSettingsChromecastSupport": "Chromecastサポート", + "LabelSettingsDateFormat": "日付フォーマット", + "LabelSettingsEnableWatcher": "ライブラリーの変更を自動的に監視する", + "LabelSettingsEnableWatcherForLibrary": "ライブラリーの変更を自動的に監視する", + "LabelSettingsEnableWatcherHelp": "ファイルの変更が検出されたときにアイテムの自動追加/更新を有効にします。*サーバーの再起動が必要です", + "LabelSettingsEpubsAllowScriptedContent": "epubのスクリプトコンテンツを許可", + "LabelSettingsEpubsAllowScriptedContentHelp": "epubファイルがスクリプトを実行することを許可します。epubファイルのソースを信頼しない限り、この設定を無効のままにすることをお勧めします。", + "LabelSettingsExperimentalFeatures": "実験的機能", + "LabelSettingsExperimentalFeaturesHelp": "開発中の機能でご意見とテストのご協力をお願いしています。クリックしてGitHubディスカッションを開く。", "LabelSettingsFindCovers": "表紙を探す", - "LabelSettingsFindCoversHelp": "もしオーディオブックに表紙が埋め込まれていない、もしくは表紙画像がフォルダー内に見つからなければ、スキャナーは表紙を探そうとします。
注記: これによってスキャン時間が長くなります", + "LabelSettingsFindCoversHelp": "もしオーディオブックに表紙が埋め込まれていない、もしくは表紙画像がフォルダー内に見つからなければ、スキャナーは表紙を探そうとします。
注: これによってスキャン時間が長くなります", + "LabelSettingsHideSingleBookSeries": "単一の本のシリーズを非表示", + "LabelSettingsHideSingleBookSeriesHelp": "単一の本を持つシリーズはシリーズページとホームページの棚から非表示になります。", + "LabelSettingsHomePageBookshelfView": "ホームページは本棚ビューを使用", + "LabelSettingsLibraryBookshelfView": "ライブラリーは本棚ビューを使用", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "完了率が次より大きい", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "残り時間が次より少ない(秒)", + "LabelSettingsLibraryMarkAsFinishedWhen": "メディアアイテムを完了としてマークする条件", + "LabelSettingsOnlyShowLaterBooksInContinueSeries": "「シリーズを続ける」で前の本をスキップ", + "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "「シリーズを続ける」のホームページ棚は、1冊以上完了していて進行中の本がないシリーズの最初の未開始の本を表示します。この設定を有効にすると、最初の未開始の本ではなく、最後に完了した本からシリーズが継続されます。", "LabelSettingsParseSubtitles": "サブタイトルを抽出する", "LabelSettingsParseSubtitlesHelp": "オーディオブックのフォルダー名からサブタイトルを抽出します。
サブタイトルは \"-\" で区切ってください
例: \"本のタイトル - ここにサブタイトル\" という名前だと \"ここにサブタイトル\" というサブタイトルになります", "LabelSettingsPreferMatchedMetadata": "一致したメタデータを優先する", "LabelSettingsPreferMatchedMetadataHelp": "クイックマッチを使用する時、一致したデータは書籍の詳細を上書きします。デフォルトでは、埋まっていない項目のみ入力されます。", + "LabelSettingsSkipMatchingBooksWithASIN": "ASINを持つ本のマッチングをスキップ", + "LabelSettingsSkipMatchingBooksWithISBN": "ISBNを持つ本のマッチングをスキップ", "LabelSettingsSortingIgnorePrefixes": "並び替えでプレフィックスを無視する", "LabelSettingsSortingIgnorePrefixesHelp": "例: プレフィックス \"the\" の付いた本のタイトル \"The Book Title\" は \"Book Title, The\" として並び替えられます", + "LabelSettingsSquareBookCovers": "正方形の本のカバーを使用", + "LabelSettingsSquareBookCoversHelp": "標準の1.6:1のカバーより正方形のカバーを優先", "LabelSettingsStoreCoversWithItem": "表紙を項目と一緒に保存する", "LabelSettingsStoreCoversWithItemHelp": "デフォルトでは表紙は /metadata/items に保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます。\"cover\" という名前のファイル一つのみが保持されます", "LabelSettingsStoreMetadataWithItem": "メタデータを項目と一緒に保存する", - "LabelSettingsStoreMetadataWithItemHelp": "デフォルトではメタデータは/metadata/itemsに保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます" + "LabelSettingsStoreMetadataWithItemHelp": "デフォルトではメタデータは/metadata/itemsに保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます", + "LabelSettingsTimeFormat": "時刻フォーマット", + "LabelShare": "共有", + "LabelShareDownloadableHelp": "共有リンクを持つユーザーがライブラリーアイテムのzipファイルをダウンロードできるようにします。", + "LabelShareOpen": "共有を公開", + "LabelShareURL": "共有URL", + "LabelShowAll": "すべて表示", + "LabelShowSeconds": "秒を表示", + "LabelShowSubtitles": "字幕を表示", + "LabelSize": "サイズ", + "LabelSleepTimer": "スリープタイマー", + "LabelSlug": "スラグ", + "LabelSortAscending": "昇順", + "LabelSortDescending": "降順", + "LabelSortPubDate": "公開日でソート", + "LabelStart": "開始", + "LabelStartTime": "開始時刻", + "LabelStarted": "開始済み", + "LabelStartedAt": "開始日時", + "LabelStartedDate": "{0}に開始", + "LabelStatsAudioTracks": "オーディオトラック", + "LabelStatsAuthors": "作者", + "LabelStatsBestDay": "日ごとの最長", + "LabelStatsDailyAverage": "日ごとの平均", + "LabelStatsDays": "日数", + "LabelStatsDaysListened": "聴いた日数", + "LabelStatsHours": "時間", + "LabelStatsInARow": "連続", + "LabelStatsItemsFinished": "完了したアイテム", + "LabelStatsItemsInLibrary": "ライブラリー内のアイテム", + "LabelStatsMinutes": "分", + "LabelStatsMinutesListening": "聴いた時間(分)", + "LabelStatsOverallDays": "総日数", + "LabelStatsOverallHours": "総時間", + "LabelStatsWeekListening": "週間リスニング", + "LabelSubtitle": "サブタイトル", + "LabelSupportedFileTypes": "サポートされているファイルタイプ", + "LabelTag": "タグ", + "LabelTags": "タグ", + "LabelTagsAccessibleToUser": "ユーザーがアクセス可能なタグ", + "LabelTagsNotAccessibleToUser": "ユーザーがアクセスできないタグ", + "LabelTasks": "実行中のタスク", + "LabelTextEditorBulletedList": "箇条書きリスト", + "LabelTextEditorLink": "リンク", + "LabelTextEditorNumberedList": "番号付きリスト", + "LabelTextEditorUnlink": "リンク解除", + "LabelTheme": "テーマ", + "LabelThemeDark": "ダーク", + "LabelThemeLight": "ライト", + "LabelThemeSepia": "セピア", + "LabelTimeBase": "タイムベース", + "LabelTimeDurationXHours": "{0}時間", + "LabelTimeDurationXMinutes": "{0}分", + "LabelTimeDurationXSeconds": "{0}秒", + "LabelTimeInMinutes": "時間(分単位)", + "LabelTimeLeft": "残り{0}", + "LabelTimeListened": "リスニング時間", + "LabelTimeListenedToday": "本日のリスニング時間", + "LabelTimeRemaining": "残り{0}", + "LabelTimeToShift": "シフトする時間(秒単位)", + "LabelTitle": "タイトル", + "LabelToolsEmbedMetadata": "メタデータの埋め込み", + "LabelToolsEmbedMetadataDescription": "カバー画像とチャプターを含むメタデータをオーディオファイルに埋め込みます。", + "LabelToolsM4bEncoder": "M4Bエンコーダー", + "LabelToolsMakeM4b": "M4Bオーディオブックファイルを作成", + "LabelToolsMakeM4bDescription": "メタデータ、カバー画像、チャプターを埋め込んだ.M4Bオーディオブックファイルを生成します。", + "LabelToolsSplitM4b": "M4BをMP3に分割", + "LabelToolsSplitM4bDescription": "メタデータ、カバー画像、チャプターを埋め込んだチャプター分割MP3ファイルをM4Bから作成します。", + "LabelTotalDuration": "総再生時間", + "LabelTotalTimeListened": "合計リスニング時間", + "LabelTrackFromFilename": "ファイル名からトラック", + "LabelTrackFromMetadata": "メタデータからトラック", + "LabelTracks": "トラック", + "LabelTracksMultiTrack": "マルチトラック", + "LabelTracksNone": "トラックなし", + "LabelTracksSingleTrack": "シングルトラック", + "LabelTrailer": "トレーラー", + "LabelType": "タイプ", + "LabelUnabridged": "完全版", + "LabelUndo": "元に戻す", + "LabelUnknown": "不明", + "LabelUnknownPublishDate": "公開日不明", + "LabelUpdateCover": "カバーを更新", + "LabelUpdateCoverHelp": "マッチが見つかった場合、選択した本の既存のカバーの上書きを許可", + "LabelUpdateDetails": "詳細を更新", + "LabelUpdateDetailsHelp": "マッチが見つかった場合、選択した本の既存の詳細の上書きを許可", + "LabelUpdatedAt": "更新日時", + "LabelUploaderDragAndDrop": "ファイルまたはフォルダーをドラッグ&ドロップ", + "LabelUploaderDragAndDropFilesOnly": "ファイルをドラッグ&ドロップ", + "LabelUploaderDropFiles": "ファイルをドロップ", + "LabelUploaderItemFetchMetadataHelp": "タイトル、作者、シリーズを自動取得", + "LabelUseAdvancedOptions": "詳細オプションを使用", + "LabelUseChapterTrack": "チャプタートラックを使用", + "LabelUseFullTrack": "フルトラックを使用", + "LabelUseZeroForUnlimited": "0で無制限", + "LabelUser": "ユーザー", + "LabelUsername": "ユーザー名", + "LabelValue": "値", + "LabelVersion": "バージョン", + "LabelViewBookmarks": "ブックマークを表示", + "LabelViewChapters": "チャプターを表示", + "LabelViewPlayerSettings": "プレーヤー設定を表示", + "LabelViewQueue": "「次に再生」を表示", + "LabelVolume": "ボリューム", + "LabelWebRedirectURLsDescription": "ログイン後にウェブアプリへのリダイレクトを許可するため、OAuthプロバイダーでこれらのURLを認可してください:", + "LabelWebRedirectURLsSubfolder": "リダイレクトURLのサブフォルダー", + "LabelWeekdaysToRun": "実行する曜日", + "LabelXBooks": "{0}冊の本", + "LabelXItems": "{0}件のアイテム", + "LabelYearReviewHide": "年間振り返りを非表示", + "LabelYearReviewShow": "年間振り返りを表示", + "LabelYourAudiobookDuration": "オーディオブックの再生時間", + "LabelYourBookmarks": "ブックマーク", + "LabelYourPlaylists": "プレイリスト", + "LabelYourProgress": "進捗状況", + "MessageAddToPlayerQueue": "「次に再生」に追加", + "MessageAppriseDescription": "この機能を使用するには、Apprise API のインスタンスを起動するか、同じリクエストを処理できるAPIが必要です。
Apprise API URLは通知を送信するための完全なURLパスである必要があります。例えば、APIインスタンスが http://192.168.1.1:8337 で起動している場合は http://192.168.1.1:8337/notify を入力してください。", + "MessageAsinCheck": "AmazonではなくAudibleの正しいリージョンのASINを使用していることを確認してください。", + "MessageAuthenticationLegacyTokenWarning": "レガシーAPIトークンは将来削除されます。代わりに APIキー を使用してください。", + "MessageAuthenticationOIDCChangesRestart": "OIDCの変更を適用するには、保存後にサーバーを再起動してください。", + "MessageAuthenticationSecurityMessage": "セキュリティのため認証が改善されました。すべてのユーザーは再ログインが必要です。", + "MessageBackupsDescription": "バックアップにはユーザー、ユーザーの進捗状況、ライブラリーアイテムの詳細、サーバー設定、および /metadata/items/metadata/authors に保存されている画像が含まれます。バックアップにはライブラリーフォルダーに保存されているファイルは含まれません。", + "MessageBackupsLocationEditNote": "注: バックアップ場所を更新しても、既存のバックアップは移動または変更されません", + "MessageBackupsLocationNoEditNote": "注: バックアップ場所は環境変数で設定されており、ここでは変更できません。", + "MessageBackupsLocationPathEmpty": "バックアップ場所のパスは空にできません", + "MessageBatchEditPopulateMapDetailsAllHelp": "すべてのアイテムのデータで有効なフィールドを設定します。複数の値があるフィールドはマージされます", + "MessageBatchEditPopulateMapDetailsItemHelp": "このアイテムのデータで有効なマップ詳細フィールドを設定します", + "MessageBatchQuickMatchDescription": "クイックマッチは選択したアイテムの不足しているカバーとメタデータを追加しようとします。クイックマッチが既存のカバーやメタデータを上書きできるようにするには、以下のオプションを有効にしてください。", + "MessageBookshelfNoCollections": "まだコレクションを作成していません", + "MessageBookshelfNoCollectionsHelp": "コレクションは公開されています。ライブラリーにアクセスできるすべてのユーザーが見ることができます。", + "MessageBookshelfNoRSSFeeds": "公開中のRSSフィードはありません", + "MessageBookshelfNoResultsForFilter": "フィルター \"{0}: {1}\" に一致する結果がありません", + "MessageBookshelfNoResultsForQuery": "クエリに一致する結果がありません", + "MessageBookshelfNoSeries": "シリーズがありません", + "MessageBulkChapterPattern": "この番号パターンでいくつのチャプターを追加しますか?", + "MessageChapterEndIsAfter": "チャプターの終了がオーディオブックの末尾を超えています", + "MessageChapterErrorFirstNotZero": "最初のチャプターは0から始まる必要があります", + "MessageChapterErrorStartGteDuration": "開始時間はオーディオブックの長さより短くなければなりません", + "MessageChapterErrorStartLtPrev": "開始時間は前のチャプターの開始時間以上でなければなりません", + "MessageChapterStartIsAfter": "チャプターの開始がオーディオブックの末尾を超えています", + "MessageChaptersNotFound": "チャプターが見つかりません", + "MessageCheckingCron": "cronを確認中...", + "MessageConfirmCloseFeed": "このフィードを閉じてもよろしいですか?", + "MessageConfirmDeleteApiKey": "APIキー \"{0}\" を削除してもよろしいですか?", + "MessageConfirmDeleteBackup": "{0} のバックアップを削除してもよろしいですか?", + "MessageConfirmDeleteDevice": "電子書籍リーダー端末 \"{0}\" を削除してもよろしいですか?", + "MessageConfirmDeleteFile": "ファイルシステムからファイルを削除します。よろしいですか?", + "MessageConfirmDeleteLibrary": "ライブラリー \"{0}\" を完全に削除してもよろしいですか?", + "MessageConfirmDeleteLibraryItem": "データベースとファイルシステムからライブラリーアイテムを削除します。よろしいですか?", + "MessageConfirmDeleteLibraryItems": "データベースとファイルシステムから {0}件のライブラリーアイテムを削除します。よろしいですか?", + "MessageConfirmDeleteMetadataProvider": "カスタムメタデータプロバイダー \"{0}\" を削除してもよろしいですか?", + "MessageConfirmDeleteNotification": "この通知を削除してもよろしいですか?", + "MessageConfirmDeleteSession": "このセッションを削除してもよろしいですか?", + "MessageConfirmEmbedMetadataInAudioFiles": "{0}件のオーディオファイルにメタデータを埋め込んでもよろしいですか?", + "MessageConfirmForceReScan": "強制的に再スキャンしてもよろしいですか?", + "MessageConfirmMarkAllEpisodesFinished": "すべてのエピソードを完了済みにしてもよろしいですか?", + "MessageConfirmMarkAllEpisodesNotFinished": "すべてのエピソードを未完了にしてもよろしいですか?", + "MessageConfirmMarkItemFinished": "\"{0}\" を完了済みにしてもよろしいですか?", + "MessageConfirmMarkItemNotFinished": "\"{0}\" を未完了にしてもよろしいですか?", + "MessageConfirmMarkSeriesFinished": "このシリーズのすべての本を完了済みにしてもよろしいですか?", + "MessageConfirmMarkSeriesNotFinished": "このシリーズのすべての本を未完了にしてもよろしいですか?", + "MessageConfirmNotificationTestTrigger": "テストデータでこの通知をトリガーしてもよろしいですか?", + "MessageConfirmPurgeCache": "キャッシュの削除により /metadata/cache ディレクトリ全体が削除されます。

キャッシュディレクトリを削除してもよろしいですか?", + "MessageConfirmPurgeItemsCache": "アイテムキャッシュの削除により /metadata/cache/items ディレクトリ全体が削除されます。
よろしいですか?", + "MessageConfirmQuickEmbed": "警告!クイック埋め込みはオーディオファイルをバックアップしません。オーディオファイルのバックアップがあることを確認してください。

続行しますか?", + "MessageConfirmQuickMatchEpisodes": "エピソードのクイックマッチは一致が見つかった場合に詳細を上書きします。未マッチのエピソードのみ更新されます。よろしいですか?", + "MessageConfirmReScanLibraryItems": "{0}件のアイテムを再スキャンしてもよろしいですか?", + "MessageConfirmRemoveAllChapters": "すべてのチャプターを削除してもよろしいですか?", + "MessageConfirmRemoveAuthor": "作者 \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemoveCollection": "コレクション \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemoveEpisode": "エピソード \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemoveEpisodeNote": "注: 「ファイルも削除する」を有効にしない限り、オーディオファイルは削除されません", + "MessageConfirmRemoveEpisodes": "{0}件のエピソードを削除してもよろしいですか?", + "MessageConfirmRemoveListeningSessions": "{0}件のリスニングセッションを削除してもよろしいですか?", + "MessageConfirmRemoveMetadataFiles": "ライブラリーアイテムフォルダー内のすべての metadata.{0} ファイルを削除してもよろしいですか?", + "MessageConfirmRemoveNarrator": "ナレーター \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRemovePlaylist": "プレイリスト \"{0}\" を削除してもよろしいですか?", + "MessageConfirmRenameGenre": "すべてのアイテムのジャンル \"{0}\" を \"{1}\" に変更してもよろしいですか?", + "MessageConfirmRenameGenreMergeNote": "注: このジャンルはすでに存在するためマージされます。", + "MessageConfirmRenameGenreWarning": "警告!大文字・小文字が異なる類似したジャンル \"{0}\" がすでに存在します。", + "MessageConfirmRenameTag": "すべてのアイテムのタグ \"{0}\" を \"{1}\" に変更してもよろしいですか?", + "MessageConfirmRenameTagMergeNote": "注: このタグはすでに存在するためマージされます。", + "MessageConfirmRenameTagWarning": "警告!大文字・小文字が異なる類似したタグ \"{0}\" がすでに存在します。", + "MessageConfirmResetProgress": "進捗状況をリセットしてもよろしいですか?", + "MessageConfirmSendEbookToDevice": "{0} の電子書籍 \"{1}\" を端末 \"{2}\" に送信してもよろしいですか?", + "MessageConfirmUnlinkOpenId": "このユーザーをOpenIDからリンク解除してもよろしいですか?", + "MessageDaysListenedInTheLastYear": "過去1年間に {0}日間聴きました", + "MessageDownloadingEpisode": "エピソードをダウンロード中", + "MessageDragFilesIntoTrackOrder": "ファイルを正しいトラック順にドラッグしてください", + "MessageEmbedFailed": "埋め込み失敗!", + "MessageEmbedFinished": "埋め込み完了!", + "MessageEmbedQueue": "メタデータ埋め込みキューに追加されました(キュー内: {0}件)", + "MessageEpisodesQueuedForDownload": "{0}件のエピソードがダウンロードキューに追加されました", + "MessageEreaderDevices": "電子書籍を確実に配信するため、以下の各端末で上記のメールアドレスを送信元として許可する設定が必要になることがあります。", + "MessageFeedURLWillBe": "フィードURLは {0} になります", + "MessageFetching": "取得中...", + "MessageForceReScanDescription": "すべてのファイルを新規スキャンのように再スキャンします。オーディオファイルのID3タグ、OPFファイル、テキストファイルが新規として読み込まれます。", + "MessageHeatmapListeningTimeTooltip": "{1} に {0} のリスニング", + "MessageHeatmapNoListeningSessions": "{0} にリスニングセッションはありません", + "MessageImportantNotice": "重要なお知らせ!", + "MessageInsertChapterBelow": "下にチャプターを挿入", + "MessageInvalidAsin": "無効なASIN", + "MessageItemsSelected": "{0}件選択中", + "MessageItemsUpdated": "{0}件更新されました", + "MessageJoinUsOn": "参加する:", + "MessageLoading": "読み込み中...", + "MessageLoadingFolders": "フォルダーを読み込み中...", + "MessageLogsDescription": "ログは /metadata/logs にJSONファイルとして保存されます。クラッシュログは /metadata/logs/crash_logs.txt に保存されます。", + "MessageM4BFailed": "M4B変換失敗!", + "MessageM4BFinished": "M4B変換完了!", + "MessageMapChapterTitles": "タイムスタンプを調整せずにチャプタータイトルを既存のオーディオブックのチャプターに割り当てます", + "MessageMarkAllEpisodesFinished": "すべてのエピソードを完了済みにする", + "MessageMarkAllEpisodesNotFinished": "すべてのエピソードを未完了にする", + "MessageMarkAsFinished": "完了済みにする", + "MessageMarkAsNotFinished": "未完了にする", + "MessageMatchBooksDescription": "ライブラリー内の本を選択した検索プロバイダーの本と照合し、空の詳細とカバーアートを補完しようとします。既存の詳細は上書きしません。", + "MessageNoAudioTracks": "オーディオトラックなし", + "MessageNoAuthors": "作者なし", + "MessageNoBackups": "バックアップなし", + "MessageNoBookmarks": "ブックマークなし", + "MessageNoChapters": "チャプターなし", + "MessageNoCollections": "コレクションなし", + "MessageNoCoversFound": "カバーが見つかりません", + "MessageNoDescription": "説明なし", + "MessageNoDevices": "端末なし", + "MessageNoDownloadsInProgress": "現在進行中のダウンロードはありません", + "MessageNoDownloadsQueued": "ダウンロードキューが空です", + "MessageNoEpisodeMatchesFound": "エピソードの一致が見つかりません", + "MessageNoEpisodes": "エピソードなし", + "MessageNoFoldersAvailable": "利用可能なフォルダーがありません", + "MessageNoGenres": "ジャンルなし", + "MessageNoIssues": "問題なし", + "MessageNoItems": "アイテムなし", + "MessageNoItemsFound": "アイテムが見つかりません", + "MessageNoListeningSessions": "リスニングセッションなし", + "MessageNoLogs": "ログなし", + "MessageNoMediaProgress": "メディアの進捗状況なし", + "MessageNoNotifications": "通知なし", + "MessageNoPodcastFeed": "無効なポッドキャスト: フィードなし", + "MessageNoPodcastsFound": "ポッドキャストが見つかりません", + "MessageNoResults": "結果なし", + "MessageNoSearchResultsFor": "\"{0}\" の検索結果はありません", + "MessageNoSeries": "シリーズなし", + "MessageNoTags": "タグなし", + "MessageNoTasksRunning": "実行中のタスクなし", + "MessageNoUpdatesWereNecessary": "更新は不要でした", + "MessageNoUserPlaylists": "プレイリストがありません", + "MessageNoUserPlaylistsHelp": "プレイリストは非公開です。作成したユーザーのみが見ることができます。", + "MessageNotYetImplemented": "未実装", + "MessageOpmlPreviewNote": "注: これは解析されたOPMLファイルのプレビューです。実際のポッドキャストタイトルはRSSフィードから取得されます。", + "MessageOr": "または", + "MessagePauseChapter": "チャプターの再生を一時停止", + "MessagePlayChapter": "チャプターの最初から聴く", + "MessagePlaylistCreateFromCollection": "コレクションからプレイリストを作成", + "MessagePleaseWait": "しばらくお待ちください...", + "MessagePodcastHasNoRSSFeedForMatching": "このポッドキャストには照合に使用できるRSSフィードURLがありません", + "MessagePodcastSearchField": "検索キーワードまたはRSSフィードURLを入力", + "MessageQuickEmbedInProgress": "クイック埋め込みを実行中", + "MessageQuickEmbedQueue": "クイック埋め込みキューに追加されました(キュー内: {0}件)", + "MessageQuickMatchAllEpisodes": "すべてのエピソードをクイックマッチ", + "MessageQuickMatchDescription": "'{0}' の最初の一致結果でアイテムの空の詳細とカバーを設定します。サーバー設定の「一致したメタデータを優先」が有効な場合を除き、既存の詳細は上書きされません。", + "MessageRemoveChapter": "チャプターを削除", + "MessageRemoveEpisodes": "{0}件のエピソードを削除", + "MessageRemoveFromPlayerQueue": "「次に再生」から削除", + "MessageRemoveUserWarning": "ユーザー \"{0}\" を完全に削除してもよろしいですか?", + "MessageReportBugsAndContribute": "バグ報告、機能リクエスト、コントリビューション:", + "MessageResetChaptersConfirm": "チャプターをリセットして変更を元に戻してもよろしいですか?", + "MessageRestoreBackupConfirm": "以下の日時に作成されたバックアップを復元してもよろしいですか:", + "MessageRestoreBackupWarning": "バックアップを復元すると、/config にあるデータベース全体と /metadata/items および /metadata/authors のカバー画像が上書きされます。

バックアップはライブラリーフォルダー内のファイルを変更しません。カバーアートとメタデータをライブラリーフォルダーに保存するサーバー設定を有効にしている場合、それらはバックアップされず上書きもされません。

サーバーを使用しているすべてのクライアントは自動的に更新されます。", + "MessageScheduleLibraryScanNote": "ほとんどのユーザーには、この機能を無効のままにして「ライブラリーの変更を自動的に監視する」設定を有効にしておくことをお勧めします。これによりライブラリーフォルダーの変更が自動的に検出されます。NFS などのファイルシステムで「ライブラリーの変更を自動的に監視する」が機能しない場合はこの機能を有効にしてください。", + "MessageScheduleRunEveryWeekdayAtTime": "毎{0} {1} に実行", + "MessageSearchResultsFor": "検索結果:", + "MessageSelected": "{0}件選択中", + "MessageSeriesSequenceCannotContainSpaces": "シリーズのシーケンスにスペースを含めることはできません", + "MessageServerCouldNotBeReached": "サーバーに接続できませんでした", + "MessageSetChaptersFromTracksDescription": "各オーディオファイルをチャプターとして設定し、チャプタータイトルにはファイル名を使用します", + "MessageShareExpirationWillBe": "有効期限は {0} になります", + "MessageShareExpiresIn": "{0} 後に期限切れ", + "MessageShareURLWillBe": "共有URLは {0} になります", + "MessageStartPlaybackAtTime": "\"{0}\" を {1} から再生しますか?", + "MessageTaskAudioFileNotWritable": "オーディオファイル \"{0}\" は書き込み不可です", + "MessageTaskCanceledByUser": "タスクがユーザーによりキャンセルされました", + "MessageTaskDownloadingEpisodeDescription": "エピソード \"{0}\" をダウンロード中", + "MessageTaskEmbeddingMetadata": "メタデータを埋め込み中", + "MessageTaskEmbeddingMetadataDescription": "オーディオブック \"{0}\" にメタデータを埋め込み中", + "MessageTaskEncodingM4b": "M4Bをエンコード中", + "MessageTaskEncodingM4bDescription": "オーディオブック \"{0}\" を単一のm4bファイルにエンコード中", + "MessageTaskFailed": "失敗", + "MessageTaskFailedToBackupAudioFile": "オーディオファイル \"{0}\" のバックアップに失敗しました", + "MessageTaskFailedToCreateCacheDirectory": "キャッシュディレクトリの作成に失敗しました", + "MessageTaskFailedToEmbedMetadataInFile": "ファイル \"{0}\" へのメタデータ埋め込みに失敗しました", + "MessageTaskFailedToMergeAudioFiles": "オーディオファイルのマージに失敗しました", + "MessageTaskFailedToMoveM4bFile": "m4bファイルの移動に失敗しました", + "MessageTaskFailedToWriteMetadataFile": "メタデータファイルの書き込みに失敗しました", + "MessageTaskMatchingBooksInLibrary": "ライブラリー \"{0}\" の本を照合中", + "MessageTaskNoFilesToScan": "スキャンするファイルがありません", + "MessageTaskOpmlImport": "OPMLインポート", + "MessageTaskOpmlImportDescription": "{0}件のRSSフィードからポッドキャストを作成中", + "MessageTaskOpmlImportFeed": "OPMLインポートフィード", + "MessageTaskOpmlImportFeedDescription": "RSSフィード \"{0}\" をインポート中", + "MessageTaskOpmlImportFeedFailed": "ポッドキャストフィードの取得に失敗しました", + "MessageTaskOpmlImportFeedPodcastDescription": "ポッドキャスト \"{0}\" を作成中", + "MessageTaskOpmlImportFeedPodcastExists": "このパスにポッドキャストがすでに存在します", + "MessageTaskOpmlImportFeedPodcastFailed": "ポッドキャストの作成に失敗しました", + "MessageTaskOpmlImportFinished": "{0}件のポッドキャストを追加しました", + "MessageTaskOpmlParseFailed": "OPMLファイルの解析に失敗しました", + "MessageTaskOpmlParseFastFail": "無効なOPMLファイル: タグが見つからないか タグが見つかりません", + "MessageTaskOpmlParseNoneFound": "OPMLファイルにフィードが見つかりません", + "MessageTaskScanItemsAdded": "{0}件追加", + "MessageTaskScanItemsMissing": "{0}件不明", + "MessageTaskScanItemsUpdated": "{0}件更新", + "MessageTaskScanNoChangesNeeded": "変更は不要でした", + "MessageTaskScanningFileChanges": "\"{0}\" のファイル変更をスキャン中", + "MessageTaskScanningLibrary": "\"{0}\" ライブラリーをスキャン中", + "MessageTaskTargetDirectoryNotWritable": "ターゲットディレクトリは書き込み不可です", + "MessageThinking": "処理中...", + "MessageUploaderItemFailed": "アップロード失敗", + "MessageUploaderItemSuccess": "アップロード成功!", + "MessageUploading": "アップロード中...", + "MessageValidCronExpression": "有効なCron式です", + "MessageWatcherIsDisabledGlobally": "ウォッチャーはサーバー設定でグローバルに無効化されています", + "MessageXLibraryIsEmpty": "{0} ライブラリーは空です!", + "MessageYourAudiobookDurationIsLonger": "オーディオブックの長さが見つかった長さより長くなっています", + "MessageYourAudiobookDurationIsShorter": "オーディオブックの長さが見つかった長さより短くなっています", + "NoteChangeRootPassword": "ルートユーザーのみ空のパスワードを設定できます", + "NoteChapterEditorTimes": "注: 最初のチャプターの開始時間は必ず0:00のままにしてください。また最後のチャプターの開始時間はオーディオブックの長さを超えることはできません。", + "NoteFolderPicker": "注: すでにマッピング済みのフォルダーは表示されません", + "NoteRSSFeedPodcastAppsHttps": "警告: ほとんどのポッドキャストアプリはHTTPSのRSSフィードURLが必要です", + "NoteRSSFeedPodcastAppsPubDate": "警告: 1件以上のエピソードに公開日がありません。一部のポッドキャストアプリではこれが必要です。", + "NoteUploaderFoldersWithMediaFiles": "メディアファイルを含むフォルダーは別々のライブラリーアイテムとして処理されます。", + "NoteUploaderOnlyAudioFiles": "オーディオファイルのみをアップロードする場合、各オーディオファイルは個別のオーディオブックとして処理されます。", + "NoteUploaderUnsupportedFiles": "サポートされていないファイルは無視されます。フォルダーを選択またはドロップする場合、アイテムフォルダー内にない他のファイルは無視されます。", + "NotificationOnBackupCompletedDescription": "バックアップが完了したときにトリガーされます", + "NotificationOnBackupFailedDescription": "バックアップが失敗したときにトリガーされます", + "NotificationOnEpisodeDownloadedDescription": "ポッドキャストエピソードが自動ダウンロードされたときにトリガーされます", + "NotificationOnRSSFeedDisabledDescription": "失敗が多すぎたため自動エピソードダウンロードが無効になったときにトリガーされます", + "NotificationOnRSSFeedFailedDescription": "自動エピソードダウンロードのRSSフィードリクエストが失敗したときにトリガーされます", + "NotificationOnTestDescription": "通知システムのテスト用イベント", + "PlaceholderBulkChapterInput": "チャプタータイトルを入力するか、番号付けを使用(例: 「エピソード 1」「チャプター 10」「1.」)", + "PlaceholderNewCollection": "新しいコレクション名", + "PlaceholderNewFolderPath": "新しいフォルダーパス", + "PlaceholderNewPlaylist": "新しいプレイリスト名", + "PlaceholderSearch": "検索...", + "PlaceholderSearchEpisode": "エピソードを検索...", + "StatsAuthorsAdded": "追加された作者", + "StatsBooksAdded": "追加された本", + "StatsBooksAdditional": "その他の追加内容…", + "StatsBooksFinished": "完了した本", + "StatsBooksFinishedThisYear": "今年完了した本…", + "StatsBooksListenedTo": "聴いた本", + "StatsCollectionGrewTo": "あなたの本のコレクションが増えました…", + "StatsSessions": "セッション", + "StatsSpentListening": "リスニング時間", + "StatsTopAuthor": "トップ作者", + "StatsTopAuthors": "トップ作者", + "StatsTopGenre": "トップジャンル", + "StatsTopGenres": "トップジャンル", + "StatsTopMonth": "トップ月", + "StatsTopNarrator": "トップナレーター", + "StatsTopNarrators": "トップナレーター", + "StatsTotalDuration": "合計再生時間…", + "StatsYearInReview": "年間振り返り", + "ToastAccountUpdateSuccess": "アカウントを更新しました", + "ToastAppriseUrlRequired": "Apprise URLを入力してください", + "ToastAsinRequired": "ASINを入力してください", + "ToastAuthorImageRemoveSuccess": "作者の画像を削除しました", + "ToastAuthorNotFound": "作者「{0}」が見つかりません", + "ToastAuthorRemoveSuccess": "作者を削除しました", + "ToastAuthorSearchNotFound": "作者が見つかりません", + "ToastAuthorUpdateMerged": "作者をマージしました", + "ToastAuthorUpdateSuccess": "作者を更新しました", + "ToastAuthorUpdateSuccessNoImageFound": "作者を更新しました(画像が見つかりません)", + "ToastBackupAppliedSuccess": "バックアップを適用しました", + "ToastBackupCreateFailed": "バックアップの作成に失敗しました", + "ToastBackupCreateSuccess": "バックアップを作成しました", + "ToastBackupDeleteFailed": "バックアップの削除に失敗しました", + "ToastBackupDeleteSuccess": "バックアップを削除しました", + "ToastBackupInvalidMaxKeep": "保持するバックアップ数が無効です", + "ToastBackupInvalidMaxSize": "バックアップの最大サイズが無効です", + "ToastBackupRestoreFailed": "バックアップの復元に失敗しました", + "ToastBackupUploadFailed": "バックアップのアップロードに失敗しました", + "ToastBackupUploadSuccess": "バックアップをアップロードしました", + "ToastBatchApplyDetailsToItemsSuccess": "詳細をアイテムに適用しました", + "ToastBatchDeleteFailed": "一括削除に失敗しました", + "ToastBatchDeleteSuccess": "一括削除に成功しました", + "ToastBatchQuickMatchFailed": "一括クイックマッチに失敗しました!", + "ToastBatchQuickMatchStarted": "{0}冊の一括クイックマッチを開始しました!", + "ToastBatchUpdateFailed": "一括更新に失敗しました", + "ToastBatchUpdateSuccess": "一括更新に成功しました", + "ToastBookmarkCreateFailed": "ブックマークの作成に失敗しました", + "ToastBookmarkCreateSuccess": "ブックマークを追加しました", + "ToastBookmarkRemoveSuccess": "ブックマークを削除しました", + "ToastBulkChapterInvalidCount": "1〜150の数値を入力してください", + "ToastCachePurgeFailed": "キャッシュのクリアに失敗しました", + "ToastCachePurgeSuccess": "キャッシュをクリアしました", + "ToastChapterLocked": "チャプターはロックされています。", + "ToastChapterStartTimeAdjusted": "チャプター開始時間を{0}秒調整しました", + "ToastChaptersAllLocked": "すべてのチャプターがロックされています。時間をシフトするにはいくつかのチャプターのロックを解除してください。", + "ToastChaptersHaveErrors": "チャプターにエラーがあります", + "ToastChaptersInvalidShiftAmountLast": "シフト量が無効です。最後のチャプターの開始時間がこのオーディオブックの長さを超えてしまいます。", + "ToastChaptersInvalidShiftAmountStart": "シフト量が無効です。最初のチャプターがゼロまたは負の長さになり、2番目のチャプターに上書きされます。2番目のチャプターの開始時間を長くしてください。", + "ToastChaptersMustHaveTitles": "チャプターにはタイトルが必要です", + "ToastChaptersRemoved": "チャプターを削除しました", + "ToastChaptersUpdated": "チャプターを更新しました", + "ToastCollectionItemsAddFailed": "アイテムのコレクションへの追加に失敗しました", + "ToastCollectionRemoveSuccess": "コレクションを削除しました", + "ToastCollectionUpdateSuccess": "コレクションを更新しました", + "ToastConnectionNotAvailable": "接続が利用できません。後でもう一度お試しください", + "ToastCoverSearchFailed": "カバー検索に失敗しました", + "ToastCoverUpdateFailed": "カバーの更新に失敗しました", + "ToastDateTimeInvalidOrIncomplete": "日時が無効または不完全です", + "ToastDeleteFileFailed": "ファイルの削除に失敗しました", + "ToastDeleteFileSuccess": "ファイルを削除しました", + "ToastDeviceAddFailed": "デバイスの追加に失敗しました", + "ToastDeviceNameAlreadyExists": "その名前の電子書籍リーダー端末はすでに存在します", + "ToastDeviceTestEmailFailed": "テストメールの送信に失敗しました", + "ToastDeviceTestEmailSuccess": "テストメールを送信しました", + "ToastEmailSettingsUpdateSuccess": "メール設定を更新しました", + "ToastEncodeCancelFailed": "エンコードのキャンセルに失敗しました", + "ToastEncodeCancelSucces": "エンコードをキャンセルしました", + "ToastEpisodeDownloadQueueClearFailed": "ダウンロードキューのクリアに失敗しました", + "ToastEpisodeDownloadQueueClearSuccess": "エピソードのダウンロードキューをクリアしました", + "ToastEpisodeUpdateSuccess": "{0}個のエピソードを更新しました", + "ToastErrorCannotShare": "このデバイスではネイティブ共有ができません", + "ToastFailedToCreate": "作成に失敗しました", + "ToastFailedToDelete": "削除に失敗しました", + "ToastFailedToLoadData": "データの読み込みに失敗しました", + "ToastFailedToMatch": "マッチングに失敗しました", + "ToastFailedToShare": "共有に失敗しました", + "ToastFailedToUpdate": "更新に失敗しました", + "ToastInvalidImageUrl": "画像のURLが無効です", + "ToastInvalidMaxEpisodesToDownload": "ダウンロード可能な最大エピソード数が無効です", + "ToastInvalidUrl": "URLが無効です", + "ToastInvalidUrls": "1つ以上のURLが無効です", + "ToastItemCoverUpdateSuccess": "アイテムのカバーを更新しました", + "ToastItemDeletedFailed": "アイテムの削除に失敗しました", + "ToastItemDeletedSuccess": "アイテムを削除しました", + "ToastItemDetailsUpdateSuccess": "アイテムの詳細を更新しました", + "ToastItemMarkedAsFinishedFailed": "完了としてマークするのに失敗しました", + "ToastItemMarkedAsFinishedSuccess": "アイテムを完了としてマークしました", + "ToastItemMarkedAsNotFinishedFailed": "未完了としてマークするのに失敗しました", + "ToastItemMarkedAsNotFinishedSuccess": "アイテムを未完了としてマークしました", + "ToastItemUpdateSuccess": "アイテムを更新しました", + "ToastLibraryCreateFailed": "ライブラリーの作成に失敗しました", + "ToastLibraryCreateSuccess": "ライブラリー「{0}」を作成しました", + "ToastLibraryDeleteFailed": "ライブラリーの削除に失敗しました", + "ToastLibraryDeleteSuccess": "ライブラリーを削除しました", + "ToastLibraryScanFailedToStart": "スキャンの開始に失敗しました", + "ToastLibraryScanStarted": "ライブラリースキャンを開始しました", + "ToastLibraryUpdateSuccess": "ライブラリー「{0}」を更新しました", + "ToastMatchAllAuthorsFailed": "すべての作者のマッチングに失敗しました", + "ToastMetadataFilesRemovedError": "metadata.{0}ファイルの削除中にエラーが発生しました", + "ToastMetadataFilesRemovedNoneFound": "ライブラリー内にmetadata.{0}ファイルが見つかりません", + "ToastMetadataFilesRemovedNoneRemoved": "削除されたmetadata.{0}ファイルはありません", + "ToastMetadataFilesRemovedSuccess": "{0}個のmetadata.{1}ファイルを削除しました", + "ToastMustHaveAtLeastOnePath": "少なくとも1つのパスが必要です", + "ToastNameEmailRequired": "名前とメールアドレスが必要です", + "ToastNameRequired": "名前を入力してください", + "ToastNewApiKeyUserError": "ユーザーを選択してください", + "ToastNewEpisodesFound": "{0}個の新しいエピソードが見つかりました", + "ToastNewUserCreatedFailed": "アカウントの作成に失敗しました:「{0}」", + "ToastNewUserCreatedSuccess": "新しいアカウントを作成しました", + "ToastNewUserLibraryError": "少なくとも1つのライブラリーを選択してください", + "ToastNewUserPasswordError": "パスワードが必要です。ルートユーザーのみ空のパスワードを使用できます", + "ToastNewUserTagError": "少なくとも1つのタグを選択してください", + "ToastNewUserUsernameError": "ユーザー名を入力してください", + "ToastNoNewEpisodesFound": "新しいエピソードは見つかりません", + "ToastNoRSSFeed": "ポッドキャストにはRSSフィードがありません", + "ToastNoUpdatesNecessary": "更新の必要はありません", + "ToastNotificationCreateFailed": "通知の作成に失敗しました", + "ToastNotificationDeleteFailed": "通知の削除に失敗しました", + "ToastNotificationFailedMaximum": "最大失敗試行回数は0以上である必要があります", + "ToastNotificationQueueMaximum": "最大通知キューは0以上である必要があります", + "ToastNotificationSettingsUpdateSuccess": "通知設定を更新しました", + "ToastNotificationTestTriggerFailed": "テスト通知のトリガーに失敗しました", + "ToastNotificationTestTriggerSuccess": "テスト通知をトリガーしました", + "ToastNotificationUpdateSuccess": "通知を更新しました", + "ToastPlaylistCreateFailed": "プレイリストの作成に失敗しました", + "ToastPlaylistCreateSuccess": "プレイリストを作成しました", + "ToastPlaylistRemoveSuccess": "プレイリストを削除しました", + "ToastPlaylistUpdateSuccess": "プレイリストを更新しました", + "ToastPodcastCreateFailed": "ポッドキャストの作成に失敗しました", + "ToastPodcastCreateSuccess": "ポッドキャストを作成しました", + "ToastPodcastEpisodeUpdated": "エピソードを更新しました", + "ToastPodcastGetFeedFailed": "ポッドキャストフィードの取得に失敗しました", + "ToastPodcastNoEpisodesInFeed": "RSSフィードにエピソードが見つかりません", + "ToastPodcastNoRssFeed": "ポッドキャストにはRSSフィードがありません", + "ToastProgressIsNotBeingSynced": "進捗が同期されていません。再生を再開してください", + "ToastProviderCreatedFailed": "プロバイダーの追加に失敗しました", + "ToastProviderCreatedSuccess": "新しいプロバイダーを追加しました", + "ToastProviderNameAndUrlRequired": "名前とURLが必要です", + "ToastProviderRemoveSuccess": "プロバイダーを削除しました", + "ToastRSSFeedCloseFailed": "RSSフィードのクローズに失敗しました", + "ToastRSSFeedCloseSuccess": "RSSフィードをクローズしました", + "ToastRemoveFailed": "削除に失敗しました", + "ToastRemoveItemFromCollectionFailed": "コレクションからのアイテム削除に失敗しました", + "ToastRemoveItemFromCollectionSuccess": "アイテムをコレクションから削除しました", + "ToastRemoveItemsWithIssuesFailed": "問題のあるライブラリーアイテムの削除に失敗しました", + "ToastRemoveItemsWithIssuesSuccess": "問題のあるライブラリーアイテムを削除しました", + "ToastRenameFailed": "名前変更に失敗しました", + "ToastRescanFailed": "{0}の再スキャンに失敗しました", + "ToastRescanRemoved": "再スキャンが完了しました。アイテムが削除されました", + "ToastRescanUpToDate": "再スキャンが完了しました。アイテムは最新です", + "ToastRescanUpdated": "再スキャンが完了しました。アイテムが更新されました", + "ToastScanFailed": "ライブラリーアイテムのスキャンに失敗しました", + "ToastSelectAtLeastOneUser": "少なくとも1人のユーザーを選択してください", + "ToastSendEbookToDeviceFailed": "電子書籍のデバイスへの送信に失敗しました", + "ToastSendEbookToDeviceSuccess": "電子書籍をデバイス「{0}」に送信しました", + "ToastSeriesSubmitFailedSameName": "同じ名前のシリーズを2つ追加することはできません", + "ToastSeriesUpdateFailed": "シリーズの更新に失敗しました", + "ToastSeriesUpdateSuccess": "シリーズを更新しました", + "ToastServerSettingsUpdateSuccess": "サーバー設定を更新しました", + "ToastSessionCloseFailed": "セッションのクローズに失敗しました", + "ToastSessionDeleteFailed": "セッションの削除に失敗しました", + "ToastSessionDeleteSuccess": "セッションを削除しました", + "ToastSleepTimerDone": "スリープタイマーが終了しました... zZzzZz", + "ToastSlugMustChange": "スラグに無効な文字が含まれています", + "ToastSlugRequired": "スラグを入力してください", + "ToastSocketConnected": "ソケットが接続されました", + "ToastSocketDisconnected": "ソケットが切断されました", + "ToastSocketFailedToConnect": "ソケット接続に失敗しました", + "ToastSortingPrefixesEmptyError": "少なくとも1つのソート接頭辞が必要です", + "ToastSortingPrefixesUpdateSuccess": "ソート接頭辞を更新しました({0}アイテム)", + "ToastTitleRequired": "タイトルを入力してください", + "ToastUnknownError": "不明なエラー", + "ToastUnlinkOpenIdFailed": "OpenIDからのユーザーのリンク解除に失敗しました", + "ToastUnlinkOpenIdSuccess": "ユーザーをOpenIDからリンク解除しました", + "ToastUploaderFilepathExistsError": "ファイルパス「{0}」はサーバーにすでに存在しています", + "ToastUploaderItemExistsInSubdirectoryError": "アイテム「{0}」はアップロードパスのサブディレクトリーを使用しています。", + "ToastUserDeleteFailed": "ユーザーの削除に失敗しました", + "ToastUserDeleteSuccess": "ユーザーを削除しました", + "ToastUserPasswordChangeSuccess": "パスワードを変更しました", + "ToastUserPasswordMismatch": "パスワードが一致しません", + "ToastUserPasswordMustChange": "新しいパスワードは古いパスワードと同じにすることはできません", + "ToastUserRootRequireName": "ルートユーザー名を入力してください", + "TooltipAddChapters": "チャプターを追加", + "TooltipAddOneSecond": "1秒追加", + "TooltipAdjustChapterStart": "クリックして開始時刻を調整", + "TooltipLockAllChapters": "すべてのチャプターをロック", + "TooltipLockChapter": "チャプターをロック(Shift+クリックで範囲選択)", + "TooltipSubtractOneSecond": "1秒減算", + "TooltipUnlockAllChapters": "すべてのチャプターをロック解除", + "TooltipUnlockChapter": "チャプターをロック解除(Shift+クリックで範囲選択)" } diff --git a/client/strings/lt.json b/client/strings/lt.json index b5ca66630..2480cdf35 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -1,5 +1,6 @@ { "ButtonAdd": "Pridėti", + "ButtonAddApiKey": "Pridėti API raktą", "ButtonAddChapters": "Pridėti skyrius", "ButtonAddDevice": "Pridėti įrenginį", "ButtonAddLibrary": "Pridėti Biblioteką", diff --git a/client/strings/nl.json b/client/strings/nl.json index 708ea7a97..41a5a814b 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -2,7 +2,7 @@ "ButtonAdd": "Toevoegen", "ButtonAddApiKey": "API Key toevoegen", "ButtonAddChapters": "Hoofdstukken toevoegen", - "ButtonAddDevice": "Toestel toevoegen", + "ButtonAddDevice": "Apparaat toevoegen", "ButtonAddLibrary": "Bibliotheek toevoegen", "ButtonAddPodcasts": "Podcasts toevoegen", "ButtonAddUser": "Gebruiker toevoegen", @@ -139,7 +139,7 @@ "HeaderCustomMetadataProviders": "Aangepaste Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download-wachtrij", - "HeaderEbookFiles": "Ebook bestanden", + "HeaderEbookFiles": "E-book bestanden", "HeaderEmail": "E-mail", "HeaderEmailSettings": "E-mail instellingen", "HeaderEpisodes": "Afleveringen", @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Boeken", "LabelButtonText": "Knop Tekst", - "LabelByAuthor": "Door {0}", + "LabelByAuthor": "door {0}", "LabelChangePassword": "Wachtwoord wijzigen", "LabelChannels": "Kanalen", "LabelChapterCount": "{0} Hoofdstukken", @@ -383,7 +383,7 @@ "LabelFolders": "Mappen", "LabelFontBold": "Vetgedrukt", "LabelFontBoldness": "Lettertype Dikte", - "LabelFontFamily": "Lettertypefamilie", + "LabelFontFamily": "Letterfamilie", "LabelFontItalic": "Cursief", "LabelFontScale": "Lettertype schaal", "LabelFontStrikethrough": "Doorgestreept", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Nee {0}", "LabelLibraryItem": "Bibliotheekonderdeel", "LabelLibraryName": "Bibliotheeknaam", - "LabelLibrarySortByProgress": "Voortuigang geüpdatet", - "LabelLibrarySortByProgressFinished": "Datum voltooid", - "LabelLibrarySortByProgressStarted": "Datum gestart", + "LabelLibrarySortByProgress": "Voortgang: Laatst geüpdatet", + "LabelLibrarySortByProgressFinished": "Voortgang: Voltooid", + "LabelLibrarySortByProgressStarted": "Voortgang: Gestart", "LabelLimit": "Limiet", "LabelLineSpacing": "Regelruimte", "LabelListenAgain": "Opnieuw Beluisteren", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken", "LabelSettingsChromecastSupport": "Chromecast ondersteuning", "LabelSettingsDateFormat": "Datumnotatie", - "LabelSettingsEnableWatcher": "Bibliotheken automatisch scannen op wijzigingen", - "LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch scannen op wijzigingen", + "LabelSettingsEnableWatcher": "Bibliotheken automatisch monitoren op wijzigingen", + "LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch monitoren op wijzigingen", "LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server", "LabelSettingsEpubsAllowScriptedContent": "Sta scripted content toe in epubs", "LabelSettingsEpubsAllowScriptedContentHelp": "Sta toe dat epub-bestanden scripts uitvoeren. Het wordt aanbevolen om deze instelling uitgeschakeld te houden, tenzij u de bron van de epub-bestanden vertrouwt.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Weet je zeker dat je de hoofdstukken wil resetten en de wijzigingen die je gemaakt hebt ongedaan wil maken?", "MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op", "MessageRestoreBackupWarning": "Een back-up herstellen zal de volledige database in /config en de omslagen in /metadata/items & /metadata/authors overschrijven.

Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om omslagen en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.

Alle apparaten die je server gebruiken, worden automatisch ververst.", - "MessageScheduleLibraryScanNote": "Voor de meeste gebruikers is het raadzaam om deze functie uitgeschakeld te laten en de folder watcher-instelling ingeschakeld te houden. De folder watcher detecteert automatisch wijzigingen in uw bibliotheekmappen. De folder watcher werkt niet voor elk bestandssysteem (zoals NFS), dus geplande bibliotheekscans kunnen in plaats daarvan worden gebruikt.", + "MessageScheduleLibraryScanNote": "Voor de meeste gebruikers is het aangeraden om deze functie uitgeschakeld te laten en de \"Bibliotheek automatisch monitoren op wijzigingen\" instelling ingeschakeld te houden - deze detecteert automatisch wijzigingen in uw bibliotheekmappen. Activeer deze instelling als \"Bibliotheek automatisch monitoren op wijzigingen\" niet werkt voor uw bestandssysteem (zoals NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Elke {0} uitvoeren op {1}", "MessageSearchResultsFor": "Zoekresultaten voor", "MessageSelected": "{0} geselecteerd", @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Item(s) toegevoegd aan collectie mislukt", "ToastCollectionRemoveSuccess": "Collectie verwijderd", "ToastCollectionUpdateSuccess": "Collectie bijgewerkt", + "ToastConnectionNotAvailable": "Verbinding niet beschikbaar. Gelieve later opnieuw te proberen", + "ToastCoverSearchFailed": "Omslag zoeken mislukt", "ToastCoverUpdateFailed": "Omslag bijwerken mislukt", "ToastDateTimeInvalidOrIncomplete": "Datum en tijd ongeldig of onvolledig", "ToastDeleteFileFailed": "Bestand verwijderen mislukt", diff --git a/client/strings/no.json b/client/strings/no.json index 1a19cfa1e..8e1c9cfbb 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -34,7 +34,7 @@ "ButtonEditChapters": "Rediger kapittel", "ButtonEditPodcast": "Rediger podcast", "ButtonEnable": "Aktiver", - "ButtonFireAndFail": "Kjør ved feil", + "ButtonFireAndFail": "Utfør og feil", "ButtonFireOnTest": "Kjør onTest-kommando", "ButtonForceReScan": "Tving skann", "ButtonFullPath": "Full sti", @@ -113,7 +113,7 @@ "ButtonUploadOPMLFile": "Last opp OPML fil", "ButtonUserDelete": "Slett bruker {0}", "ButtonUserEdit": "Rediger bruker {0}", - "ButtonViewAll": "Vis alle", + "ButtonViewAll": "Vis alt", "ButtonYes": "Ja", "ErrorUploadFetchMetadataAPI": "Feil ved innhenting av metadata", "ErrorUploadFetchMetadataNoResults": "Kunne ikke hente metadata - forsøk å oppdatere tittel og/eller forfatter", @@ -309,6 +309,7 @@ "LabelDeleteFromFileSystemCheckbox": "Slett fra filsystemet (fjern haken for kun å ta bort fra databasen)", "LabelDescription": "Beskrivelse", "LabelDeselectAll": "Fjern valg", + "LabelDetectedPattern": "Oppdaget mønster:", "LabelDevice": "Enhet", "LabelDeviceInfo": "Enhetsinformasjon", "LabelDeviceIsAvailableTo": "Enheten er tilgjengelig for...", @@ -377,11 +378,12 @@ "LabelFilterByUser": "Filtrer etter bruker", "LabelFindEpisodes": "Finn episoder", "LabelFinished": "Fullført", + "LabelFinishedDate": "Fullført {0}", "LabelFolder": "Mappe", "LabelFolders": "Mapper", "LabelFontBold": "Fet", "LabelFontBoldness": "Skrifttykkelse", - "LabelFontFamily": "Fontfamilie", + "LabelFontFamily": "Skriftfamilie", "LabelFontItalic": "Kursiv", "LabelFontScale": "Font størrelse", "LabelFontStrikethrough": "Gjennomstreking", @@ -434,7 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ingen {0}", "LabelLibraryItem": "Bibliotek enhet", "LabelLibraryName": "Bibliotek navn", - "LabelLibrarySortByProgress": "Fremgang: Sist oppdatert", + "LabelLibrarySortByProgress": "Fremdrift: Sist oppdatert", + "LabelLibrarySortByProgressFinished": "Fremdrift: Fullført", + "LabelLibrarySortByProgressStarted": "Fremdrift: Startet", "LabelLimit": "Begrensning", "LabelLineSpacing": "Linjemellomrom", "LabelListenAgain": "Lytt igjen", @@ -443,8 +447,9 @@ "LabelLogLevelWarn": "Varsel", "LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen", "LabelLowestPriority": "Laveste prioritet", + "LabelMatchConfidence": "Konfidens", "LabelMatchExistingUsersBy": "Knytt sammen eksisterende brukere basert på", - "LabelMatchExistingUsersByDescription": "Brukes for å koble til eksisterende brukere. Når koblingen er i orden vil brukerne bli identifisert med en unik id fra SSO-tilbyderen.", + "LabelMatchExistingUsersByDescription": "Brukes for å koble til eksisterende brukere. Når koblingen er i orden vil brukerne bli identifisert med en unik id fra SSO-tilbyderen", "LabelMaxEpisodesToDownload": "Maksimalt antall episoder som skal lastes ned. Bruk 0 for ubegrenset.", "LabelMaxEpisodesToDownloadPerCheck": "Maksimalt antall nye episoder som skal lastes ned per sjekk", "LabelMaxEpisodesToKeep": "Maksimalt antall episoder som skal beholdes", @@ -453,7 +458,7 @@ "LabelMediaType": "Medie type", "LabelMetaTag": "Meta tag", "LabelMetaTags": "Meta tags", - "LabelMetadataOrderOfPrecedenceDescription": "Høyere prioritert kilder for metadata overstyrer laverer prioriterte kilder for metadata.", + "LabelMetadataOrderOfPrecedenceDescription": "Høyere prioritert kilder for metadata overstyrer laverer prioriterte kilder for metadata", "LabelMetadataProvider": "Metadata Leverandør", "LabelMinute": "Minutt", "LabelMinutes": "Minutter", @@ -472,7 +477,9 @@ "LabelNewestAuthors": "Nyeste forfattere", "LabelNewestEpisodes": "Nyeste episoder", "LabelNextBackupDate": "Neste sikkerhetskopi dato", + "LabelNextChapters": "Neste kapitler blir:", "LabelNextScheduledRun": "Neste planlagte kjøring", + "LabelNoApiKeys": "Ingen API-nøkler", "LabelNoCustomMetadataProviders": "Ingen egendefinerte tilbydere for metadata", "LabelNoEpisodesSelected": "Ingen episoder valgt", "LabelNotFinished": "Ikke fullført", @@ -488,10 +495,11 @@ "LabelNotificationsMaxQueueSize": "Maksimalt antall varslinger i kø", "LabelNotificationsMaxQueueSizeHelp": "Hendelser er begrenset til avfyre én gang per sekund. Hendelser blir ignorert om køen er full. Dette forhindrer overflod av varslinger.", "LabelNumberOfBooks": "Antall bøker", + "LabelNumberOfChapters": "Antall kapitler:", "LabelNumberOfEpisodes": "# episoder", "LabelOpenIDAdvancedPermsClaimDescription": "Navnet på OpenID claim'et som inneholder avanserte tilganger for brukerhandlinger i applikasjonen som vil brukes for ikke-administratorroller (hvis konfigurert). Hvis claim'et mangler fra responsen, nektes tilgang til ABS. Hvis en enkelt opsjon mangler, blir behandlet som false. Påse at identitetstilbyderens claim stemmer overens med den forventede strukturen:", "LabelOpenIDClaims": "La følge valg være tomme for å slå av avanserte gruppe og tillatelser. Gruppen \"Bruker\" vil da også automatisk legges til.", - "LabelOpenIDGroupClaimDescription": "Navn på OpenID-forespørsel som inneholder en lite over brukerens grupper. Vanligvis kalt grupper. Om konfigurert, vil applikasjonen tildele roller baseret på brukerens gruppemedlemsskaper, gitt disse grupper er navngitt (uten forbehold for store og små bokstaver) 'admin', 'user' eller 'guest' i forespørsel. Forespørselen burde inneholde en liste (og hvis brukeren tilhører flere grupper), applikasjonen vil tildele rolle med høyeste adgangsnivå. Hvis ingen grupper matcher vil adgang bli nektet.", + "LabelOpenIDGroupClaimDescription": "Navn på OpenID-forespørsel som inneholder en lite over brukerens grupper. Vanligvis kalt grupper. Om konfigurert, vil applikasjonen tildele roller baseret på brukerens gruppemedlemsskaper, gitt disse grupper er navngitt (uten forbehold for store og små bokstaver) 'admin', 'user' eller 'guest' i forespørsel. Forespørselen burde inneholde en liste (og hvis brukeren tilhører flere grupper), applikasjonen vil tildele rolle med høyeste adgangsnivå. Hvis ingen grupper matcher vil adgang bli nektet.", "LabelOpenRSSFeed": "Åpne RSS Feed", "LabelOverwrite": "Overskriv", "LabelPaginationPageXOfY": "Side {0} av {1}", @@ -509,6 +517,7 @@ "LabelPersonalYearReview": "Oppsummering av året ditt ({0})", "LabelPhotoPathURL": "Bilde sti/URL", "LabelPlayMethod": "Avspillingsmetode", + "LabelPlaybackRateIncrementDecrement": "Trinnstørrelse for økning/senking av avspillingshastighet", "LabelPlayerChapterNumberMarker": "{0} av {1}", "LabelPlaylists": "Spilleliste", "LabelPodcast": "Podcast", @@ -561,6 +570,7 @@ "LabelSelectAll": "Velg alt", "LabelSelectAllEpisodes": "Velg alle episoder", "LabelSelectEpisodesShowing": "Velg {0} episoder vist", + "LabelSelectUser": "Velg bruker", "LabelSelectUsers": "Velg brukere", "LabelSendEbookToDevice": "Send Ebok til...", "LabelSequence": "Sekvens", @@ -628,6 +638,7 @@ "LabelStartTime": "Start Tid", "LabelStarted": "Startet", "LabelStartedAt": "Startet", + "LabelStartedDate": "Startet {0}", "LabelStatsAudioTracks": "Lydspor", "LabelStatsAuthors": "Forfattere", "LabelStatsBestDay": "Beste dag", @@ -657,6 +668,7 @@ "LabelTheme": "Tema", "LabelThemeDark": "Mørk", "LabelThemeLight": "Lys", + "LabelThemeSepia": "Sepia", "LabelTimeBase": "Tidsbase", "LabelTimeDurationXHours": "{0} timer", "LabelTimeDurationXMinutes": "{0} minutter", @@ -725,24 +737,32 @@ "MessageAddToPlayerQueue": "Legg til i kø", "MessageAppriseDescription": "For å bruke denne funksjonen trenger du en instans av Apprise API kjørende eller et API som håndterer disse forespørslene.
Apprise API URL skal være hele URL-en til varslingen, f.eks., hvis din API-instans er på http://192.168.1.1:8337 så skal du bruke http://192.168.1.1:8337/notify.", "MessageAsinCheck": "Påse at du bruker ASIN fra den riktige Audible-regionen, ikke Amazon.", + "MessageAuthenticationLegacyTokenWarning": "Eldre API-tokener vil bli fjernet i fremtiden. Bruk API-nøkler i stedet.", "MessageAuthenticationOIDCChangesRestart": "Etter å ha lagret, start serveren din på nytt for at OIDC-endringene skal tre i kraft.", + "MessageAuthenticationSecurityMessage": "Autentisering er forbedret av sikkerhetshensyn. Alle brukere må logge inn på nytt.", "MessageBackupsDescription": "Sikkerhetskopier inkluderer, brukerfremgang, detaljer om bibliotekgjenstander, tjener instillinger og bilder lagret under /metadata/items og /metadata/authors. Sikkerhetskopier vil ikke inkludere filer som er lagret i bibliotek mappene.", "MessageBackupsLocationEditNote": "Viktig: Endring av mappen for sikkerhetskopi hverken endrer eller flytter eksisterende sikkerhetskopier!", - "MessageBackupsLocationNoEditNote": "NB: Mappen for sikkerhetskopi settes i en miljøvariabel og kan ikke endres her.", + "MessageBackupsLocationNoEditNote": "Viktig: Mappen for sikkerhetskopi satt i en miljøvariabel og kan ikke endres her.", "MessageBackupsLocationPathEmpty": "Mappen for sikkerhetskopiering må angis", + "MessageBatchEditPopulateMapDetailsAllHelp": "Fyll aktiverte felt med data fra alle elementer. Felt med flere verdier blir slått sammen", + "MessageBatchEditPopulateMapDetailsItemHelp": "Fyll aktiverte kartdetaljfelt med data fra dette elementet", "MessageBatchQuickMatchDescription": "Kjapt søk vil forsøke å legge til manglende omslag og metadata for de valgte gjenstandene. Aktiver dette valget for å tillate Kjapt søk til å overskrive eksisterende omslag og/eller metadata.", "MessageBookshelfNoCollections": "Du har ikke laget noen samlinger ennå", + "MessageBookshelfNoCollectionsHelp": "Samlinger er offentlige. Alle brukere med tilgang til biblioteket kan se dem.", "MessageBookshelfNoRSSFeeds": "Ingen RSS feed er åpen", "MessageBookshelfNoResultsForFilter": "Ingen resultat for filter \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Ingen resultater for søket", "MessageBookshelfNoSeries": "Du har ingen serier", + "MessageBulkChapterPattern": "Hvor mange kapitler vil du legge til med dette nummereringsmønsteret?", "MessageChapterEndIsAfter": "Kapittel slutt er etter slutt av lydboken", "MessageChapterErrorFirstNotZero": "Første kapittel starter på 0", "MessageChapterErrorStartGteDuration": "Feil start tid, må være mindre enn lengde på lydbok", "MessageChapterErrorStartLtPrev": "Feil start tid, må være større eller det samme som forrige kapittel start tid", "MessageChapterStartIsAfter": "Kapittel start er etter slutten av din lydbok", + "MessageChaptersNotFound": "Fant ikke kapitler", "MessageCheckingCron": "Sjekker cron...", "MessageConfirmCloseFeed": "Er du sikker på at du vil lukke denne feeden?", + "MessageConfirmDeleteApiKey": "Er du sikker på at du vil slette API-nøkkelen \"{0}\"?", "MessageConfirmDeleteBackup": "Er du sikker på at du vil slette sikkerhetskopi for {0}?", "MessageConfirmDeleteDevice": "Er du sikker på at du vil slette e-leser enheten \"{0}\"?", "MessageConfirmDeleteFile": "Dette vil slette filen fra filsystemet. Er du sikker?", @@ -761,7 +781,7 @@ "MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?", "MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?", "MessageConfirmNotificationTestTrigger": "Utløs dette varselet med test-data?", - "MessageConfirmPurgeCache": "Tømming av mellomlagring vil slette hele mappen /metadata/cache.

Er du sikker på at du du vil slette mappen?", + "MessageConfirmPurgeCache": "Tømming av mellomlagring vil slette hele mappen /metadata/cache.

Er du sikker på at du vil slette mappen?", "MessageConfirmPurgeItemsCache": "(Purge items cache) Dette vil sletter hele mappen /metadata/cache/items.
Er du sikker?", "MessageConfirmQuickEmbed": "Advarsel! Rask innbygging av metadata tar ikke backup av lyd-filene først. Forsikre deg om at du har sikkerhetskopi av filene.

Fortsett?", "MessageConfirmQuickMatchEpisodes": "Hurtig gjenkjenning av episoder overskriver detaljene hvis en match blir funnet. Kun episoder som ikke allerede er matchet blir oppdatert. Er du sikker?", @@ -770,6 +790,7 @@ "MessageConfirmRemoveAuthor": "Er du sikker på at du vil fjerne forfatteren \"{0}\"?", "MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?", "MessageConfirmRemoveEpisode": "Er du sikker på at du vil fjerne episode \"{0}\"?", + "MessageConfirmRemoveEpisodeNote": "Merk: Dette sletter ikke lydfilen med mindre du slår på \"Hard delete file\"", "MessageConfirmRemoveEpisodes": "Er du sikker på at du vil fjerne {0} episoder?", "MessageConfirmRemoveListeningSessions": "Er du sikker på at du vil fjerne {0} lytte-sesjoner?", "MessageConfirmRemoveMetadataFiles": "Er du sikker på at du vil fjerne alle metadata.{0}-filer i mappene for biblioteks-elementer?", @@ -795,8 +816,11 @@ "MessageFeedURLWillBe": "Feed URL vil bli {0}", "MessageFetching": "Henter...", "MessageForceReScanDescription": "vil skanne alle filene igjen som en ny skann. Lyd fil ID3 tagger, OPF filer og tekstfiler vil bli skannet som nye.", + "MessageHeatmapListeningTimeTooltip": "{0} lytter på {1}", + "MessageHeatmapNoListeningSessions": "Ingen lytteøkter på {0}", "MessageImportantNotice": "Viktig varsel!", "MessageInsertChapterBelow": "Sett inn kapittel under", + "MessageInvalidAsin": "Ugyldig ASIN", "MessageItemsSelected": "{0} Gjenstander valgt", "MessageItemsUpdated": "{0} Gjenstander oppdatert", "MessageJoinUsOn": "Følg oss nå", @@ -842,6 +866,7 @@ "MessageNoTasksRunning": "Ingen oppgaver kjører", "MessageNoUpdatesWereNecessary": "Ingen oppdatering var nødvendig", "MessageNoUserPlaylists": "Du har ingen spillelister", + "MessageNoUserPlaylistsHelp": "Spillelister er private. Bare brukeren som oppretter dem kan se dem.", "MessageNotYetImplemented": "Ikke implementert ennå", "MessageOpmlPreviewNote": "PS: Dette er en forhåndvisning av en OPML-fil. Den faktiske podcast-tittelen hentes direkte fra RSS-feeden.", "MessageOr": "eller", @@ -864,8 +889,10 @@ "MessageRestoreBackupConfirm": "Er du sikker på at du vil gjenopprette sikkerhetskopien som var laget", "MessageRestoreBackupWarning": "gjenoppretting av sikkerhetskopi vil overskrive hele databasen under /config og omslagsbilde under /metadata/items og /metadata/authors.

Sikkerhetskopier endrer ikke noen filer under dine bibliotekmapper. Hvis du har aktivert tjenerinstillingen for å lagre omslagsbilder og metadata i bibliotekmapper så vil ikke de filene bli tatt sikkerhetskopi eller overskrevet.

Alle klientene som bruker din tjener vil bli fornyet automatisk.", "MessageScheduleLibraryScanNote": "For de fleste brukere er det anbefalt å la denne funksjonen være slått av, og la mappeovervåkeren stå på. Mappeovervåkeren oppdager automatisk endringer i biblioteksmappene. Mappeovervåkeren fungerer ikke med alle filsystemer (f.eks. NFS) og da kan planlagt skanning av bibliotekene brukes i steden for.", + "MessageScheduleRunEveryWeekdayAtTime": "Kjør hver {0} kl. {1}", "MessageSearchResultsFor": "Søk resultat for", "MessageSelected": "{0} valgt", + "MessageSeriesSequenceCannotContainSpaces": "Serienummer kan ikke inneholde mellomrom", "MessageServerCouldNotBeReached": "Tjener kunne ikke bli nådd", "MessageSetChaptersFromTracksDescription": "Sett kapitler ved å bruke hver lydfil som kapittel og kapitteltittel som lydfilnavnet", "MessageShareExpirationWillBe": "Utløp vil være {0}", @@ -886,6 +913,27 @@ "MessageTaskFailedToMergeAudioFiles": "Kunne ikke slå sammen lydfiler", "MessageTaskFailedToMoveM4bFile": "Kunne ikke flytte M4B-fil", "MessageTaskFailedToWriteMetadataFile": "Kunne ikke lagre metadata-fil", + "MessageTaskMatchingBooksInLibrary": "Samsvarende bøker i biblioteket \"{0}\"", + "MessageTaskNoFilesToScan": "Ingen filer å skanne", + "MessageTaskOpmlImport": "OPML-import", + "MessageTaskOpmlImportDescription": "Oppretter podkaster fra {0} RSS-feeder", + "MessageTaskOpmlImportFeed": "OPML-importfeed", + "MessageTaskOpmlImportFeedDescription": "Importerer RSS-feed \"{0}\"", + "MessageTaskOpmlImportFeedFailed": "Kunne ikke hente podcast-feed", + "MessageTaskOpmlImportFeedPodcastDescription": "Oppretter podkast \"{0}\"", + "MessageTaskOpmlImportFeedPodcastExists": "Podkast finnes allerede på stien", + "MessageTaskOpmlImportFeedPodcastFailed": "Misslykkes å opprette podcast", + "MessageTaskOpmlImportFinished": "La til {0} podkaster", + "MessageTaskOpmlParseFailed": "Klarte ikke å tolke OPML-fil", + "MessageTaskOpmlParseFastFail": "Ugyldig OPML-fil: -tagg ble ikke funnet ELLER en -tagg ble ikke funnet", + "MessageTaskOpmlParseNoneFound": "Fant ingen feeder i OPML-filen", + "MessageTaskScanItemsAdded": "{0} lagt til", + "MessageTaskScanItemsMissing": "{0} mangler", + "MessageTaskScanItemsUpdated": "{0} oppdatert", + "MessageTaskScanNoChangesNeeded": "Ingen endringer nødvendig", + "MessageTaskScanningFileChanges": "Skanner filendringer i \"{0}\"", + "MessageTaskScanningLibrary": "Skanner biblioteket \"{0}\"", + "MessageTaskTargetDirectoryNotWritable": "Målkatalogen er ikke skrivbar", "MessageThinking": "Tenker...", "MessageUploaderItemFailed": "Opplastning mislykkes", "MessageUploaderItemSuccess": "Opplastning fullført!", @@ -903,13 +951,43 @@ "NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler vil bli behandlet som separate bibliotekgjenstander.", "NoteUploaderOnlyAudioFiles": "Om man laster opp kun lydfiler så vil hver lydfil bli behandlet som en separat lydbok.", "NoteUploaderUnsupportedFiles": "Filer som ikke er støttet vil bli ignorert. Når man velger eller slipper en mappe, filer som ikke er en mappe vil bli ignorert.", + "NotificationOnBackupCompletedDescription": "Utløses når en sikkerhetskopi er fullført", + "NotificationOnBackupFailedDescription": "Utløses når en sikkerhetskopi mislykkes", + "NotificationOnEpisodeDownloadedDescription": "Utløses når en podkastepisode lastes ned automatisk", + "NotificationOnRSSFeedDisabledDescription": "Utløses når automatiske episodenedlastinger deaktiveres på grunn av for mange mislykkede forsøk", + "NotificationOnRSSFeedFailedDescription": "Utløses når RSS-feedforespørselen mislykkes for en automatisk episodenedlasting", + "NotificationOnTestDescription": "Hendelse for testing av varslingssystemet", + "PlaceholderBulkChapterInput": "Skriv inn kapitteltittel eller bruk nummerering (f.eks. 'Episode 1', 'Kapittel 10', '1.')", "PlaceholderNewCollection": "Ny samlingsnavn", "PlaceholderNewFolderPath": "Ny mappesti", "PlaceholderNewPlaylist": "Ny spillelistenavn", "PlaceholderSearch": "Søk..", "PlaceholderSearchEpisode": "Søk episode..", + "StatsAuthorsAdded": "forfattere lagt til", + "StatsBooksAdded": "bøker lagt til", + "StatsBooksAdditional": "Noen av tilleggene inkluderer…", + "StatsBooksFinished": "bøker fullført", + "StatsBooksFinishedThisYear": "Noen bøker fullført i år…", + "StatsBooksListenedTo": "bøker lyttet til", + "StatsCollectionGrewTo": "Boksamlingen din vokste til…", + "StatsSessions": "økter", + "StatsSpentListening": "brukt på lytting", + "StatsTopAuthor": "BESTE FORFATTER", + "StatsTopAuthors": "BESTE FORFATTERE", + "StatsTopGenre": "BESTE SJANGER", + "StatsTopGenres": "BESTE SJANGRE", + "StatsTopMonth": "BESTE MÅNED", + "StatsTopNarrator": "BESTE FORTELLER", + "StatsTopNarrators": "BESTE FORTELLERE", + "StatsTotalDuration": "Med en total varighet på…", + "StatsYearInReview": "ÅRET SOM GIKK", "ToastAccountUpdateSuccess": "Konto oppdatert", + "ToastAppriseUrlRequired": "Du må angi en Apprise-URL", + "ToastAsinRequired": "ASIN er påkrevd", "ToastAuthorImageRemoveSuccess": "Forfatter bilde fjernet", + "ToastAuthorNotFound": "Fant ikke forfatter \"{0}\"", + "ToastAuthorRemoveSuccess": "Forfatter fjernet", + "ToastAuthorSearchNotFound": "Fant ikke forfatter", "ToastAuthorUpdateMerged": "Forfatter slått sammen", "ToastAuthorUpdateSuccess": "Forfatter oppdatert", "ToastAuthorUpdateSuccessNoImageFound": "Forfatter oppdater (ingen bilde funnet)", @@ -923,6 +1001,7 @@ "ToastBackupRestoreFailed": "Misslykkes å gjenopprette sikkerhetskopi", "ToastBackupUploadFailed": "Misslykkes å laste opp sikkerhetskopi", "ToastBackupUploadSuccess": "Sikkerhetskopi lastet opp", + "ToastBatchApplyDetailsToItemsSuccess": "Detaljer brukt på elementene", "ToastBatchDeleteFailed": "Sletting feilet på utvalget", "ToastBatchDeleteSuccess": "Sletting av samling utført", "ToastBatchQuickMatchFailed": "Feil ved rask integrering av metadata!", @@ -932,17 +1011,25 @@ "ToastBookmarkCreateFailed": "Misslykkes å opprette bokmerke", "ToastBookmarkCreateSuccess": "Bokmerke lagt til", "ToastBookmarkRemoveSuccess": "Bokmerke fjernet", + "ToastBulkChapterInvalidCount": "Skriv inn et tall mellom 1 og 150", "ToastCachePurgeFailed": "Kunne ikke å slette mellomlager", "ToastCachePurgeSuccess": "Mellomlager slettet", + "ToastChapterLocked": "Kapittelet er låst.", + "ToastChapterStartTimeAdjusted": "Kapittelstart ble justert med {0} sekunder", + "ToastChaptersAllLocked": "Alle kapitler er låst. Lås opp noen kapitler for å flytte tidene.", "ToastChaptersHaveErrors": "Kapittel har feil", + "ToastChaptersInvalidShiftAmountLast": "Ugyldig forskyvningsverdi. Starttid for siste kapittel vil gå utover varigheten til denne lydboken.", + "ToastChaptersInvalidShiftAmountStart": "Ugyldig forskyvningsverdi. Det første kapitlet ville fått null eller negativ lengde og blitt overskrevet av det andre kapitlet. Øk starttiden til det andre kapitlet.", "ToastChaptersMustHaveTitles": "Kapittel må ha titler", "ToastChaptersRemoved": "Kapitler fjernet", "ToastChaptersUpdated": "Kapitler oppdatert", "ToastCollectionItemsAddFailed": "Feil med å legge til element(er)", "ToastCollectionRemoveSuccess": "Samling fjernet", "ToastCollectionUpdateSuccess": "samlingupdated", + "ToastConnectionNotAvailable": "Tilkobling er ikke tilgjengelig. Prøv igjen senere", "ToastCoverSearchFailed": "Finner ikke bokomslag", "ToastCoverUpdateFailed": "Oppdatering av bilde feilet", + "ToastDateTimeInvalidOrIncomplete": "Dato og klokkeslett er ugyldig eller ufullstendig", "ToastDeleteFileFailed": "Kunne ikke slette fil", "ToastDeleteFileSuccess": "Fil slettet", "ToastDeviceAddFailed": "Kunne ikke legge til enhet", @@ -955,6 +1042,9 @@ "ToastEpisodeDownloadQueueClearFailed": "Kunne ikke tømme køen", "ToastEpisodeDownloadQueueClearSuccess": "Nedlastingskø for eposider tømt", "ToastEpisodeUpdateSuccess": "{0} episoder oppdatert", + "ToastErrorCannotShare": "Kan ikke dele direkte på denne enheten", + "ToastFailedToCreate": "Kunne ikke opprette", + "ToastFailedToDelete": "Kunne ikke slette", "ToastFailedToLoadData": "Kunne ikke laste inn data", "ToastFailedToMatch": "Kunne ikke matche", "ToastFailedToShare": "Deling feilet", @@ -962,6 +1052,7 @@ "ToastInvalidImageUrl": "Ugyldig URL for bilde", "ToastInvalidMaxEpisodesToDownload": "Ugyldig maksimalt antall for nedlasting av episoder", "ToastInvalidUrl": "Ugyldig URL", + "ToastInvalidUrls": "Én eller flere URL-er er ugyldige", "ToastItemCoverUpdateSuccess": "Omslag oppdatert", "ToastItemDeletedFailed": "Kunne ikke slette element", "ToastItemDeletedSuccess": "Element slettet", @@ -986,6 +1077,7 @@ "ToastMustHaveAtLeastOnePath": "Påkrevd med minst én mappe", "ToastNameEmailRequired": "Navn og e-post påkrevd", "ToastNameRequired": "Navn er påkrevd", + "ToastNewApiKeyUserError": "Du må velge en bruker", "ToastNewEpisodesFound": "{0} nye episoder funnet", "ToastNewUserCreatedFailed": "Kunne ikke opprette konto: \"{0}\"", "ToastNewUserCreatedSuccess": "Ny konto opprettet", @@ -994,6 +1086,7 @@ "ToastNewUserTagError": "Velg minst en tag", "ToastNewUserUsernameError": "Skriv inn brukernavn", "ToastNoNewEpisodesFound": "Ingen nye episoder funnet", + "ToastNoRSSFeed": "Podkasten har ikke en RSS-feed", "ToastNoUpdatesNecessary": "Ingen oppdateringer nødvendig", "ToastNotificationCreateFailed": "Kunne ikke opprette varsling", "ToastNotificationDeleteFailed": "Kunne ikke slette varsling", @@ -1009,6 +1102,7 @@ "ToastPlaylistUpdateSuccess": "Spilleliste oppdatert", "ToastPodcastCreateFailed": "Misslykkes å opprette podcast", "ToastPodcastCreateSuccess": "Podcast opprettet", + "ToastPodcastEpisodeUpdated": "Episode oppdatert", "ToastPodcastGetFeedFailed": "Kunne ikke hente podcast-feed", "ToastPodcastNoEpisodesInFeed": "Ingen episoder funnet i RSS-feed", "ToastPodcastNoRssFeed": "Podcast har ingen RSS-feed", @@ -1033,6 +1127,7 @@ "ToastSelectAtLeastOneUser": "Velg minst én bruker", "ToastSendEbookToDeviceFailed": "Misslykkes å sende ebok", "ToastSendEbookToDeviceSuccess": "Ebok sendt til \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Kan ikke legge til to serier med samme navn", "ToastSeriesUpdateFailed": "Misslykkes å oppdatere serie", "ToastSeriesUpdateSuccess": "Serie oppdatert", "ToastServerSettingsUpdateSuccess": "Server-innstillinger oppdatert", @@ -1051,10 +1146,20 @@ "ToastUnknownError": "Ukjent feil", "ToastUnlinkOpenIdFailed": "Kunne ikke koble bruker fra OpenID", "ToastUnlinkOpenIdSuccess": "Bruker koblet fra OpenID", + "ToastUploaderFilepathExistsError": "Filstien \"{0}\" finnes allerede på serveren", + "ToastUploaderItemExistsInSubdirectoryError": "Elementet \"{0}\" bruker en underkatalog av opplastingsstien.", "ToastUserDeleteFailed": "Misslykkes å slette bruker", "ToastUserDeleteSuccess": "Bruker slettet", "ToastUserPasswordChangeSuccess": "Passord ble endret", "ToastUserPasswordMismatch": "Passord må stemme overens", "ToastUserPasswordMustChange": "Nytt passord kan ikke være identisk med gammelt passord", - "ToastUserRootRequireName": "Root-brukernavn er påkrevd" + "ToastUserRootRequireName": "Root-brukernavn er påkrevd", + "TooltipAddChapters": "Legg til kapittel(er)", + "TooltipAddOneSecond": "Legg til 1 sekund", + "TooltipAdjustChapterStart": "Klikk for å justere starttid", + "TooltipLockAllChapters": "Lås alle kapitler", + "TooltipLockChapter": "Lås kapittel (Shift+klikk for område)", + "TooltipSubtractOneSecond": "Trekk fra 1 sekund", + "TooltipUnlockAllChapters": "Lås opp alle kapitler", + "TooltipUnlockChapter": "Lås opp kapittel (Shift+klikk for område)" } diff --git a/client/strings/pl.json b/client/strings/pl.json index 8b70a1341..d1bc6c062 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -11,7 +11,7 @@ "ButtonApplyChapters": "Zatwierdź rozdziały", "ButtonAuthors": "Autorzy", "ButtonBack": "Wstecz", - "ButtonBatchEditPopulateFromExisting": "Powiel z poprzednich", + "ButtonBatchEditPopulateFromExisting": "Uzupełnij na podstawie istniejących", "ButtonBatchEditPopulateMapDetails": "Powiel szczegóły mapy", "ButtonBrowseForFolder": "Wyszukaj folder", "ButtonCancel": "Anuluj", @@ -55,7 +55,7 @@ "ButtonNext": "Następny", "ButtonNextChapter": "Następny rozdział", "ButtonNextItemInQueue": "Następny element w kolejce", - "ButtonOk": "Ok", + "ButtonOk": "OK", "ButtonOpenFeed": "Otwórz feed", "ButtonOpenManager": "Otwórz menadżera", "ButtonPause": "Wstrzymaj", @@ -127,7 +127,7 @@ "HeaderAudiobookTools": "Narzędzia do zarządzania audiobookami", "HeaderAuthentication": "Uwierzytelnianie", "HeaderBackups": "Kopie zapasowe", - "HeaderBulkChapterModal": "Dodaj wiele rozdziałów", + "HeaderBulkChapterModal": "Dodaj kilka rozdziałów", "HeaderChangePassword": "Zmień hasło", "HeaderChapters": "Rozdziały", "HeaderChooseAFolder": "Wybierz folder", @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Książki", "LabelButtonText": "Tekst przycisku", - "LabelByAuthor": "autorstwa {0}", + "LabelByAuthor": "Autor {0}", "LabelChangePassword": "Zmień hasło", "LabelChannels": "Kanały", "LabelChapterCount": "{0} rozdziałów", @@ -286,7 +286,7 @@ "LabelClickToUseCurrentValue": "Kliknij by zastosować aktualną wartość", "LabelClosePlayer": "Zamknij odtwarzacz", "LabelCodec": "Kodek", - "LabelCollapseSeries": "Podsumuj serię", + "LabelCollapseSeries": "Zwiń serię", "LabelCollapseSubSeries": "Zwiń podserie", "LabelCollection": "Kolekcja", "LabelCollections": "Kolekcje", @@ -300,7 +300,7 @@ "LabelCoverImageURL": "URL okładki", "LabelCoverProvider": "Dostawca okładki", "LabelCreatedAt": "Utworzone", - "LabelCronExpression": "Wyrażenie CRON", + "LabelCronExpression": "Wyrażenie harmonogramowania zadań cron", "LabelCurrent": "Aktualny", "LabelCurrently": "Obecnie:", "LabelCustomCronExpression": "Niestandardowe wyrażenie Cron:", @@ -339,11 +339,11 @@ "LabelEnable": "Włącz", "LabelEncodingBackupLocation": "Kopia zapasowa twoich oryginalnych plików audio będzie się znajdować w:", "LabelEncodingChaptersNotEmbedded": "W audiobookach wielościeżkowych rozdziały nie są osadzone.", - "LabelEncodingClearItemCache": "Pamiętaj o okresowym czyszczeniu pamięci podręcznej elementów.", + "LabelEncodingClearItemCache": "Pamiętaj, aby okresowo czyścić pamięć podręczną elementów.", "LabelEncodingFinishedM4B": "Ukończony plik M4B zostanie umieszczony w folderze audiobooka pod adresem:", "LabelEncodingInfoEmbedded": "Metadane zostaną osadzone w ścieżkach audio w folderze z audiobookiem.", "LabelEncodingStartedNavigation": "Po uruchomieniu zadania możesz opuścić tę stronę.", - "LabelEncodingTimeWarning": "Konwersja może potrwać do 30 minut.", + "LabelEncodingTimeWarning": "Kodowanie może potrwać do 30 minut.", "LabelEncodingWarningAdvancedSettings": "Ostrzeżenie: Nie aktualizuj tych ustawień, jeśli nie jesteś zaznajomiony ze sposobem działania ffmpeg i opcji konwersji.", "LabelEncodingWatcherDisabled": "Jeśli monitorowanie folderów jest wyłączone, należy ponownie przeskanować audiobooka.", "LabelEnd": "Zakończ", @@ -359,8 +359,8 @@ "LabelExample": "Przykład", "LabelExpandSeries": "Rozwiń serie", "LabelExpandSubSeries": "Rozwiń podserie", - "LabelExpired": "Przeterminowane", - "LabelExpiresAt": "Wygasa w", + "LabelExpired": "Wygasły", + "LabelExpiresAt": "Wygasa o", "LabelExpiresInSeconds": "Wygasa za (sekund)", "LabelExpiresNever": "Nigdy", "LabelExplicit": "18+", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Brak {0}", "LabelLibraryItem": "Element biblioteki", "LabelLibraryName": "Nazwa biblioteki", - "LabelLibrarySortByProgress": "Postęp: Ostatnio zaktualizowane", - "LabelLibrarySortByProgressFinished": "Postęp: Ukończone", - "LabelLibrarySortByProgressStarted": "Postęp: Rozpoczęte", + "LabelLibrarySortByProgress": "Postęp: Ostatnia aktualizacja", + "LabelLibrarySortByProgressFinished": "Postęp: Ukończony", + "LabelLibrarySortByProgressStarted": "Postęp: Rozpoczęty", "LabelLimit": "Limit", "LabelLineSpacing": "Odstęp między wierszami", "LabelListenAgain": "Słuchaj ponownie", @@ -450,7 +450,7 @@ "LabelMatchConfidence": "Zaufanie", "LabelMatchExistingUsersBy": "Dopasuje istniejących użytkowników poprzez", "LabelMatchExistingUsersByDescription": "Służy do łączenia istniejących użytkowników. Po połączeniu użytkownicy zostaną dopasowani za pomocą unikalnego identyfikatora od dostawcy SSO", - "LabelMaxEpisodesToDownload": "Maksymalna liczba odcinków do pobrania. Użyj 0, aby wyłączyć ograniczenie.", + "LabelMaxEpisodesToDownload": "Maksymalna liczba odcinków do pobrania. Użyj 0, aby uzyskać nieograniczoną liczbę.", "LabelMaxEpisodesToDownloadPerCheck": "Maksymalna liczba nowych odcinków do pobrania na jedno sprawdzenie", "LabelMaxEpisodesToKeep": "Maksymalna liczba odcinków do zachowania", "LabelMaxEpisodesToKeepHelp": "Wartość 0 wyłącza maksymalny limit. Po automatycznym pobraniu nowego odcinka, najstarszy odcinek zostanie usunięty, jeśli masz ich więcej niż X. Spowoduje to usunięcie tylko 1 odcinka na nowe pobieranie.", @@ -498,7 +498,8 @@ "LabelNumberOfChapters": "Liczba rozdziałów:", "LabelNumberOfEpisodes": "# Odcinków", "LabelOpenIDAdvancedPermsClaimDescription": "Nazwa deklaracji OpenID zawierającej zaawansowane uprawnienia do działań użytkownika w aplikacji, które będą miały zastosowanie do ról innych niż administracyjne (jeśli skonfigurowano). Jeśli deklaracja nie zostanie uwzględniona w odpowiedzi, dostęp do ABS zostanie zablokowany. Brak jednej opcji zostanie uznany za fałsz. Upewnij się, że deklaracja dostawcy tożsamości jest zgodna z oczekiwaną strukturą:", - "LabelOpenIDClaims": "Pozostaw poniższe opcje puste, aby wyłączyć zaawansowane przypisywanie grup i uprawnień. Automatycznie zostanie przypisana grupa „Użytkownik”.", + "LabelOpenIDClaims": "Pozostaw poniższe opcje puste, aby wyłączyć zaawansowane przypisywanie grup i uprawnień. Wówczas automatycznie zostanie przypisana grupa „Użytkownik”.", + "LabelOpenIDGroupClaimDescription": "Nazwa roszczenia OpenID zawierającego listę grup użytkownika. Powszechnie nazywane grupami. Jeśli skonfigurowano, aplikacja automatycznie przypisze role na podstawie przynależności użytkownika do grup, pod warunkiem, że te grupy są nazwane bez uwzględniania wielkości liter „admin”, „user” lub „guest” w roszczeniu. Roszczenie powinno zawierać listę, a jeśli użytkownik należy do wielu grup, aplikacja przypisze rolę odpowiadającą najwyższemu poziomowi dostępu. Jeśli żadna grupa nie będzie pasować, dostęp zostanie odrzucony.", "LabelOpenRSSFeed": "Otwórz kanał RSS", "LabelOverwrite": "Nadpisz", "LabelPaginationPageXOfY": "Strona {0} z {1}", @@ -573,11 +574,11 @@ "LabelSelectUsers": "Wybór użytkowników", "LabelSendEbookToDevice": "Wyślij ebook do...", "LabelSequence": "Kolejność", - "LabelSerial": "Seria", + "LabelSerial": "Numer serii", "LabelSeries": "Serie", "LabelSeriesName": "Nazwy serii", "LabelSeriesProgress": "Postęp w serii", - "LabelServerLogLevel": "Poziom logowania servera", + "LabelServerLogLevel": "Poziom logów servera", "LabelServerYearReview": "Podsumowanie serwera w roku ({0})", "LabelSetEbookAsPrimary": "Ustaw jako pierwszy", "LabelSetEbookAsSupplementary": "Ustaw jako dodatkowy", @@ -621,7 +622,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana", "LabelSettingsTimeFormat": "Format czasu", "LabelShare": "Udostępnij", - "LabelShareDownloadableHelp": "Użytkownicy mogą przy pomocy linka ściągnąć archiwum ZIP pozycji biblioteki", + "LabelShareDownloadableHelp": "Zezwala użytkownikom z linkiem udostępniania na pobranie pliku zip elementu biblioteki.", "LabelShareOpen": "Otwórz udział", "LabelShareURL": "Link do udziału", "LabelShowAll": "Pokaż wszystko", @@ -660,11 +661,11 @@ "LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika", "LabelTagsNotAccessibleToUser": "Znaczniki niedostępne dla użytkownika", "LabelTasks": "Uruchomione zadania", - "LabelTextEditorBulletedList": "Lista punktowana", + "LabelTextEditorBulletedList": "Lista wypunktowana", "LabelTextEditorLink": "Link", "LabelTextEditorNumberedList": "Lista numerowana", "LabelTextEditorUnlink": "Usuń link", - "LabelTheme": "Kompozycja", + "LabelTheme": "Motyw", "LabelThemeDark": "Ciemny", "LabelThemeLight": "Jasny", "LabelThemeSepia": "Sepia", @@ -686,13 +687,13 @@ "LabelToolsMakeM4bDescription": "Tworzy plik w formacie .M4B, który zawiera metadane, okładkę oraz rozdziały.", "LabelToolsSplitM4b": "Podziel plik .M4B na pliki .MP3", "LabelToolsSplitM4bDescription": "Podziel plik .M4B na pliki .MP3 na rozdziały z załączonymi metadanymi oraz okładką.", - "LabelTotalDuration": "TCałkowita długość", + "LabelTotalDuration": "Całkowita długość", "LabelTotalTimeListened": "Całkowity czas odtwarzania", "LabelTrackFromFilename": "Ścieżka z nazwy pliku", "LabelTrackFromMetadata": "Ścieżka z metadanych", "LabelTracks": "Ścieżki", "LabelTracksMultiTrack": "Wielościeżkowy", - "LabelTracksNone": "Brak ścieżek", + "LabelTracksNone": "Brak utworów", "LabelTracksSingleTrack": "Pojedyncza ścieżka", "LabelTrailer": "Zwiastun", "LabelType": "Typ", @@ -709,7 +710,7 @@ "LabelUploaderDragAndDropFilesOnly": "Przeciągnij i upuść pliki", "LabelUploaderDropFiles": "Puść pliki", "LabelUploaderItemFetchMetadataHelp": "Automatycznie pobierz tytuł, autora i serie", - "LabelUseAdvancedOptions": "Opcje zaawansowane", + "LabelUseAdvancedOptions": "Użyj ustawień zaawansowanych", "LabelUseChapterTrack": "Użyj ścieżki rozdziału", "LabelUseFullTrack": "Użycie ścieżki rozdziału", "LabelUseZeroForUnlimited": "Użyj 0, aby wyłączyć ograniczenia", @@ -723,6 +724,7 @@ "LabelViewQueue": "Wyświetlaj kolejkę odtwarzania", "LabelVolume": "Głośność", "LabelWebRedirectURLsDescription": "Zezwól na te adresy URL w swoim dostawcy OAuth, aby umożliwić przekierowanie z powrotem do aplikacji internetowej po zalogowaniu:", + "LabelWebRedirectURLsSubfolder": "Podfolder dla adresów URL przekierowań", "LabelWeekdaysToRun": "Dni tygodnia", "LabelXBooks": "{0} książek", "LabelXItems": "{0} elementów", @@ -743,7 +745,7 @@ "MessageBackupsLocationNoEditNote": "Uwaga: Lokalizacja kopii zapasowej jest ustawiona poprzez zmienną środowiskową i nie może być tutaj zmieniona.", "MessageBackupsLocationPathEmpty": "Ścieżka do kopii zapasowej nie może być pusta", "MessageBatchEditPopulateMapDetailsAllHelp": "Wypełnij włączone pola danymi ze wszystkich elementów. Pola z wieloma wartościami zostaną scalone.", - "MessageBatchEditPopulateMapDetailsItemHelp": "Wypełnij pola szczegółów mapy włączonej danymi z tego elementu", + "MessageBatchEditPopulateMapDetailsItemHelp": "Wypełnia włączone pola szczegółów mapy danymi z tego elementu", "MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.", "MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji", "MessageBookshelfNoCollectionsHelp": "Kolekcje są publiczne. Wszyscy użytkownicy mający dostęp do biblioteki mogą je zobaczyć.", @@ -915,10 +917,22 @@ "MessageTaskNoFilesToScan": "Brak plików do skanowania", "MessageTaskOpmlImport": "Importuj OPML", "MessageTaskOpmlImportDescription": "Tworzenie {0} podcastów z kanałów RSS", + "MessageTaskOpmlImportFeed": "Importuje plik OPML", + "MessageTaskOpmlImportFeedDescription": "Importowanie kanału RSS „{0}”", + "MessageTaskOpmlImportFeedFailed": "Nie udało się pobrać kanału podcastowego", + "MessageTaskOpmlImportFeedPodcastDescription": "Tworzenie podcastu \"{0}\"", + "MessageTaskOpmlImportFeedPodcastExists": "Podcast już istnieje pod podaną ścieżką", + "MessageTaskOpmlImportFeedPodcastFailed": "Nie udało się utworzyć podcastu", + "MessageTaskOpmlImportFinished": "Dodano {0} podcastów", + "MessageTaskOpmlParseFailed": "Błąd parsowania pliku OPML", + "MessageTaskOpmlParseFastFail": "Nieprawidłowy plik OPML. Nie znaleziono tagu LUB nie znaleziono tagu .", + "MessageTaskOpmlParseNoneFound": "Nie znaleziono kanałów w pliku OPML", "MessageTaskScanItemsAdded": "Dodano {0}", "MessageTaskScanItemsMissing": "Brakuje {0}", "MessageTaskScanItemsUpdated": "Zaktualizowano {0}", "MessageTaskScanNoChangesNeeded": "Brak zmian", + "MessageTaskScanningFileChanges": "Skanowanie zmian w plikach w „{0}”", + "MessageTaskScanningLibrary": "Skanowanie biblioteki \"{0}\"", "MessageTaskTargetDirectoryNotWritable": "Brak prawa zapisu do folderu docelowego", "MessageThinking": "Myślę...", "MessageUploaderItemFailed": "Nie udało się przesłać", @@ -937,6 +951,8 @@ "NoteUploaderFoldersWithMediaFiles": "Foldery z plikami multimedialnymi będą traktowane jako osobne elementy w bibliotece.", "NoteUploaderOnlyAudioFiles": "Jeśli przesyłasz tylko pliki audio, każdy plik audio będzie traktowany jako osobny audiobook.", "NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.", + "NotificationOnTestDescription": "Zdarzenie używane do testowania systemu powiadomień", + "PlaceholderBulkChapterInput": "Wpisz tytuł rozdziału lub użyj numeracji (np. „Odcinek 1”, „Rozdział 10”, „1.”)", "PlaceholderNewCollection": "Nowa nazwa kolekcji", "PlaceholderNewFolderPath": "Nowa ścieżka folderu", "PlaceholderNewPlaylist": "Nowa nazwa playlisty", @@ -957,26 +973,50 @@ "StatsTopMonth": "TOPOWY MIESIĄC", "StatsTopNarrator": "TOPOWY NARRATOR", "StatsTopNarrators": "TOPOWI NARRATORZY", + "StatsTotalDuration": "O sumarycznej długości…", "StatsYearInReview": "PRZEGLĄD ROKU", "ToastAccountUpdateSuccess": "Zaktualizowano konto", + "ToastAsinRequired": "ASIN jest wymagany", "ToastAuthorImageRemoveSuccess": "Zdjęcie autora usunięte", + "ToastAuthorNotFound": "Autor \"{0}\" nie został znaleziony", + "ToastAuthorRemoveSuccess": "Autor usunięty", + "ToastAuthorSearchNotFound": "Autor nie odnaleziony", "ToastAuthorUpdateMerged": "Autor scalony", "ToastAuthorUpdateSuccess": "Autor zaktualizowany", "ToastAuthorUpdateSuccessNoImageFound": "Autor zaktualizowany (nie znaleziono obrazu)", + "ToastBackupAppliedSuccess": "Kopia zapasowa została przywrócona", "ToastBackupCreateFailed": "Nie udało się utworzyć kopii zapasowej", "ToastBackupCreateSuccess": "Utworzono kopię zapasową", "ToastBackupDeleteFailed": "Nie udało się usunąć kopii zapasowej", "ToastBackupDeleteSuccess": "Udało się usunąć kopie zapasowej", + "ToastBackupInvalidMaxKeep": "Nieprawidłowa ilość kopii zapasowych do przechowania", + "ToastBackupInvalidMaxSize": "Nieprawidłowy rozmiar maksymalny kopii zapasowej", "ToastBackupRestoreFailed": "Nie udało się przywrócić kopii zapasowej", "ToastBackupUploadFailed": "Nie udało się przesłać kopii zapasowej", "ToastBackupUploadSuccess": "Kopia zapasowa została przesłana", - "ToastBatchUpdateFailed": "Aktualizacja wsadowa nie powiodła się", - "ToastBatchUpdateSuccess": "Aktualizacja wsadowa powiodła się", + "ToastBatchDeleteFailed": "Usuwanie zbiorcze nie powiodło się", + "ToastBatchDeleteSuccess": "Usuwanie zbiorcze powiodło się", + "ToastBatchUpdateFailed": "Aktualizacja zbiorcza nie powiodła się", + "ToastBatchUpdateSuccess": "Aktualizacja zbiorcza powiodła się", "ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki", "ToastBookmarkCreateSuccess": "Dodano zakładkę", "ToastBookmarkRemoveSuccess": "Zakładka została usunięta", + "ToastBulkChapterInvalidCount": "Wprowadź liczbę z przedziału od 1 do 150", + "ToastCachePurgeFailed": "Nie udało się wyczyścić pamięci cache", + "ToastCachePurgeSuccess": "Wyczyszczono pamięć cache", + "ToastChapterLocked": "Rozdział jest zablokowany.", + "ToastChapterStartTimeAdjusted": "Czas rozpoczęcia rozdziału przesunięty o \"{0}\" sekund", + "ToastChaptersAllLocked": "Wszystkie rozdziały są zablokowane. Odblokuj edycję, aby użyć przesunięcia czasowego.", + "ToastChaptersHaveErrors": "Rozdziały posiadają błędy", + "ToastChaptersInvalidShiftAmountLast": "Niepoprawna wartość przesunięcia. Czas rozpoczęcia ostatniego rozdziału wykroczyłby poza długość tego audiobooka.", + "ToastChaptersInvalidShiftAmountStart": "Niepoprawna wartość przesunięcia. Pierwszy rozdział miałby długość mniejszą lub równą zeru oraz on zostałby nadpisany przez rozdział drugi. Ustaw późniejszy czas rozpoczęcia drugiego rozdziału.", + "ToastChaptersMustHaveTitles": "Rozdziały muszą posiadać tytuł", + "ToastChaptersRemoved": "Rozdziały usunięte", + "ToastChaptersUpdated": "Rozdziały zaktualizowane", + "ToastCollectionItemsAddFailed": "Dodanie elementów do kolekcji nie powiodło się", "ToastCollectionRemoveSuccess": "Kolekcja usunięta", "ToastCollectionUpdateSuccess": "Zaktualizowano kolekcję", + "ToastConnectionNotAvailable": "Brak połączenia. Spróbuj ponownie później", "ToastCoverSearchFailed": "Nieudane wyszukiwanie okładki", "ToastCoverUpdateFailed": "Nieudana aktualizacja okładki", "ToastDateTimeInvalidOrIncomplete": "Niepoprawna data i czas", @@ -987,12 +1027,18 @@ "ToastDeviceTestEmailFailed": "NIeudana próba wysłania testowego maila", "ToastDeviceTestEmailSuccess": "Testowy email został wysłany", "ToastEmailSettingsUpdateSuccess": "Ustawienia email zaktualizowane", + "ToastEncodeCancelFailed": "Nie udało się anulować kodowania", + "ToastEncodeCancelSucces": "Kodowanie anulowane", + "ToastEpisodeDownloadQueueClearFailed": "Nie udało się wyczyścić kolejki", "ToastEpisodeDownloadQueueClearSuccess": "Wyczyszczono kolejkę epizodów do ściągnięcia", "ToastEpisodeUpdateSuccess": "Zaktualizowano {0} odcinków", + "ToastErrorCannotShare": "Nie można udostępniać natywnie na tym urządzeniu.", "ToastInvalidImageUrl": "Nieprawidłowy URL obrazu", "ToastInvalidUrl": "Nieprawidłowy URL", "ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe", "ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę", + "ToastItemDeletedFailed": "Nie udało się usunąć elementu", + "ToastItemDeletedSuccess": "Element usunięty", "ToastItemDetailsUpdateSuccess": "Zaktualizowano szczegóły", "ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako ukończone", "ToastItemMarkedAsFinishedSuccess": "Pozycja oznaczona jako ukończona", @@ -1031,12 +1077,41 @@ "ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się", "ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji", "ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji", + "ToastRemoveItemsWithIssuesFailed": "Nie udało się usunąć wadliwych elementów z biblioteki", + "ToastRemoveItemsWithIssuesSuccess": "Usunięto wadliwe elementy z biblioteki", + "ToastRenameFailed": "Nie udało się zmienić nazwy", + "ToastRescanFailed": "Ponowne skanowanie nie powiodło się dla {0}", + "ToastRescanRemoved": "Ponowne skanowanie powiodło się – element został usunięty", + "ToastRescanUpToDate": "Ponowne skanowanie powiodło się – element był aktualny", + "ToastRescanUpdated": "Ponowne skanowanie powiodło się – element został zaktualizowany", + "ToastScanFailed": "Nie powiódł się skan elementu biblioteki", + "ToastSelectAtLeastOneUser": "Zaznacz co najmniej jednego użytkownika", "ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device", + "ToastSendEbookToDeviceSuccess": "Ebook wysłany na urządzenie \"{0}\"", + "ToastSeriesSubmitFailedSameName": "Nie można dodać dwóch serii pod tą samą nazwą", + "ToastSeriesUpdateFailed": "Aktualizacja serii nie powiodła się", + "ToastSeriesUpdateSuccess": "Aktualizacja serii powiodła się", + "ToastServerSettingsUpdateSuccess": "Zaktualizowano ustawienia serwera", + "ToastSessionCloseFailed": "Nie udało się zamknąć sesji", "ToastSessionDeleteFailed": "Nie udało się usunąć sesji", "ToastSessionDeleteSuccess": "Sesja usunięta", + "ToastSleepTimerDone": "Słodkich snów... zZzzZz", "ToastSocketConnected": "Nawiązano połączenie z serwerem", "ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte", "ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się", + "ToastTitleRequired": "Tytuł jest wymagany", + "ToastUnknownError": "Nieznany błąd", + "ToastUnlinkOpenIdFailed": "Nie udało się odpiąć użytkownika z OpenID", + "ToastUnlinkOpenIdSuccess": "Użytkownik odpięty z OpenID", + "ToastUploaderFilepathExistsError": "Ścieżka \"{0}\" już istnieje na serwerze", "ToastUserDeleteFailed": "Nie udało się usunąć użytkownika", - "ToastUserDeleteSuccess": "Użytkownik usunięty" + "ToastUserDeleteSuccess": "Użytkownik usunięty", + "TooltipAddChapters": "Dodaj rozdział(y)", + "TooltipAddOneSecond": "Dodaj sekundę", + "TooltipAdjustChapterStart": "Kliknij, aby skorygować czas początkowy", + "TooltipLockAllChapters": "Zablokuj wszystkie rozdziały", + "TooltipLockChapter": "Zablokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)", + "TooltipSubtractOneSecond": "Odejmij sekundę", + "TooltipUnlockAllChapters": "Odblokuj wszystkie rozdziały", + "TooltipUnlockChapter": "Odblokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)" } diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index c6c9781cf..d2189698c 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -436,8 +436,8 @@ "LabelLibraryFilterSublistEmpty": "Sem {0}", "LabelLibraryItem": "Item da Biblioteca", "LabelLibraryName": "Nome da Biblioteca", - "LabelLibrarySortByProgress": "Última Atualização", - "LabelLibrarySortByProgressFinished": "Concluído", + "LabelLibrarySortByProgress": "Progresso: Ultima Atualização", + "LabelLibrarySortByProgressFinished": "Progresso: Terminado", "LabelLibrarySortByProgressStarted": "Progresso: Iniciado", "LabelLimit": "Limite", "LabelLineSpacing": "Espaçamento entre linhas", diff --git a/client/strings/ro.json b/client/strings/ro.json index f6e8977ef..441ccc400 100644 --- a/client/strings/ro.json +++ b/client/strings/ro.json @@ -11,140 +11,581 @@ "ButtonApplyChapters": "Aplică Capitole", "ButtonAuthors": "Autori", "ButtonBack": "Înapoi", + "ButtonBatchEditPopulateFromExisting": "Populează din existente", + "ButtonBatchEditPopulateMapDetails": "Populează detaliile hărții", + "ButtonBrowseForFolder": "Caută un dosar", "ButtonCancel": "Anulează", + "ButtonCancelEncode": "Anulare codificare", + "ButtonChangeRootPassword": "Schimbare parolă de root", + "ButtonCheckAndDownloadNewEpisodes": "Verifică și descarcă episoade noi", + "ButtonChooseAFolder": "Alege un dosar", + "ButtonChooseFiles": "Alege fișiere", "ButtonClearFilter": "Șterge filtrul", "ButtonClose": "Închide", "ButtonCloseFeed": "Închide sursa", "ButtonCloseSession": "Închide Sesiunea Curentă", "ButtonCollections": "Colecții", + "ButtonConfigureScanner": "Configurare scaner", "ButtonCreate": "Creează", + "ButtonCreateBackup": "Creează backup", "ButtonDelete": "Șterge", + "ButtonDownloadQueue": "Coadă", + "ButtonEdit": "Editare", + "ButtonEditChapters": "Editare capitole", + "ButtonEditPodcast": "Editare podcast", + "ButtonEnable": "Activează", + "ButtonForceReScan": "Forțează rescanare", + "ButtonFullPath": "Calea completă", "ButtonHide": "Ascunde", "ButtonHome": "Acasă", - "ButtonIssues": "Erori", + "ButtonIssues": "Probleme", + "ButtonJumpBackward": "Sari înapoi", + "ButtonJumpForward": "Sari înainte", "ButtonLatest": "Noutăți", "ButtonLibrary": "Bibliotecă", + "ButtonLogout": "Deconectare", + "ButtonLookup": "Căutare", + "ButtonManageTracks": "Gestionează pista", + "ButtonMapChapterTitles": "Maparea titlurilor capitolelor", + "ButtonMatchAllAuthors": "Potriviește toți autorii", + "ButtonMatchBooks": "Potrivește Cărți", + "ButtonNevermind": "Anulează", + "ButtonNext": "Următorul", + "ButtonNextChapter": "Următorul Capitol", + "ButtonNextItemInQueue": "Următorul Articol în Coadă", "ButtonOk": "OK", "ButtonOpenFeed": "Vezi noutățile", + "ButtonOpenManager": "Deschide Managerul", "ButtonPause": "Pauză", "ButtonPlay": "Redă", + "ButtonPlayAll": "Redă tot", + "ButtonPlaying": "Redare", "ButtonPlaylists": "Liste", + "ButtonPrevious": "Anterior", + "ButtonPreviousChapter": "Capitolul Anterior", + "ButtonProbeAudioFile": "Analizare Fișier Audio", + "ButtonPurgeAllCache": "Golire Cache Completă", + "ButtonPurgeItemsCache": "Golire Cache Articole", + "ButtonQueueAddItem": "Adaugă la Coadă", + "ButtonQueueRemoveItem": "Sterge din Coadă", + "ButtonQuickEmbed": "Încorporare Rapidă", + "ButtonQuickEmbedMetadata": "Metadate pentru Încorporare Rapidă", + "ButtonQuickMatch": "Potrivire Rapidă", + "ButtonReScan": "Rescanare", "ButtonRead": "Citește", - "ButtonReadLess": "Mai puțin", + "ButtonReadLess": "Citește Mai Puțin", "ButtonReadMore": "Afișează mai mult", + "ButtonRefresh": "Reîmprospătare", "ButtonRemove": "Elimină", + "ButtonRemoveAll": "Eliminați Tot", + "ButtonRemoveAllLibraryItems": "Ștergerea tuturor Articolelor din Librărie", + "ButtonRemoveFromContinueListening": "Ștergere din \"Continuă să Asculți\"", + "ButtonRemoveFromContinueReading": "Ștergere din \"Continuă să citești\"", + "ButtonRemoveSeriesFromContinueSeries": "Ștergere Serie din \"Continuă Seria\"", + "ButtonReset": "Resetează", + "ButtonResetToDefault": "Resetează la valorile implicite", + "ButtonRestore": "Restaurare", "ButtonSave": "Salvează", + "ButtonSaveAndClose": "Salvează și Închide", + "ButtonSaveTracklist": "Salvare Pistă", + "ButtonScan": "Scanează", + "ButtonScanLibrary": "Scanează Librăria", + "ButtonScrollLeft": "Derulează spre stânga", + "ButtonScrollRight": "Derulează spre Dreapta", "ButtonSearch": "Caută", + "ButtonSelectFolderPath": "Selectează Calea către Dosar", "ButtonSeries": "Serii", + "ButtonSetChaptersFromTracks": "Setează capitole din piste", + "ButtonShare": "Distribuie", + "ButtonShiftTimes": "Aliniează timpi", + "ButtonShow": "Arată", + "ButtonStartM4BEncode": "Începe Codarea M4B", + "ButtonStartMetadataEmbed": "Începe Încorporarea Metadatelor", + "ButtonStats": "Statistici", "ButtonSubmit": "Trimite", + "ButtonTest": "Testează", + "ButtonUnlinkOpenId": "Deconectare OpenID", + "ButtonUpload": "Încarcă", + "ButtonUploadBackup": "Încarcă Backup", + "ButtonUploadCover": "Încarcă Copertă", + "ButtonUploadOPMLFile": "Încarcă Fișier OPML", + "ButtonUserDelete": "Șterge userul {0}", + "ButtonUserEdit": "Editează userul {0}", + "ButtonViewAll": "Vizualizează tot", "ButtonYes": "Da", + "ErrorUploadFetchMetadataAPI": "Eroare în descărcarea metadatelor", + "ErrorUploadFetchMetadataNoResults": "Nu s-au putut prelua metadatele - încearcă să editezi titlul și/sau autorul", + "ErrorUploadLacksTitle": "Trebuie să aibă un titlu", "HeaderAccount": "Cont", + "HeaderAddCustomMetadataProvider": "Adaugă Furnizor de Metadate Personalizat", "HeaderAdvanced": "Avansat", + "HeaderApiKeys": "Chei API", + "HeaderAppriseNotificationSettings": "Setări Notificări Apprise", "HeaderAudioTracks": "Înregistrări audio", + "HeaderAudiobookTools": "Instrumente pentru Gestionarea Fișierelor Audiobook", + "HeaderAuthentication": "Autentificare", + "HeaderBackups": "Copii de siguranță", + "HeaderBulkChapterModal": "Adaugă Multiple Capitole", + "HeaderChangePassword": "Schimbă Parola", "HeaderChapters": "Capitole", + "HeaderChooseAFolder": "Alege Dosar", "HeaderCollection": "Colecție", "HeaderCollectionItems": "Conținutul colecției", + "HeaderCover": "Copertă", + "HeaderCurrentDownloads": "Descărcări Curente", + "HeaderCustomMessageOnLogin": "Mesaj Personalizat la Autentificare", + "HeaderCustomMetadataProviders": "Furnizor de Metadate Personalizat", "HeaderDetails": "Detalii", + "HeaderDownloadQueue": "Coadă de Descărcare", "HeaderEbookFiles": "Ebook-uri", + "HeaderEmail": "Email", + "HeaderEmailSettings": "Setări Email", "HeaderEpisodes": "Episoade", + "HeaderEreaderDevices": "Dispozitive eReader", "HeaderEreaderSettings": "Setări eReader", + "HeaderFiles": "Fișiere", + "HeaderFindChapters": "Caută Capitol", + "HeaderIgnoredFiles": "Fișiere Ignorate", + "HeaderItemFiles": "Fișiere Articol", + "HeaderLastListeningSession": "Ultima Sesiune de Ascultare", "HeaderLatestEpisodes": "Episoade recente", "HeaderLibraries": "Biblioteci", + "HeaderLibraryFiles": "Fișiere in Librărie", + "HeaderLibraryStats": "Statistici Librărie", + "HeaderListeningSessions": "Sesiuni de Ascultare", + "HeaderListeningStats": "Statistici Ascultare", + "HeaderLogin": "Autentifică", + "HeaderLogs": "Loguri", + "HeaderManageGenres": "Gestionează Genuri", + "HeaderManageTags": "Gestionează Etichete", + "HeaderMapDetails": "Detaliile Hărții", + "HeaderMatch": "Potrivește", + "HeaderMetadataOrderOfPrecedence": "Prioritatea Metadatelor", + "HeaderMetadataToEmbed": "Metadate pentru Încorporare", + "HeaderNewAccount": "Cont nou", + "HeaderNewApiKey": "Cheie API Nouă", + "HeaderNewLibrary": "Librărie Nouă", + "HeaderNotificationCreate": "Creează Notificare", + "HeaderNotificationUpdate": "Actualizare Notificare", + "HeaderNotifications": "Notificări", + "HeaderOpenIDConnectAuthentication": "Autentificare prin OpenID", + "HeaderOpenListeningSessions": "Deschide Sesiuni de Ascultare", "HeaderOpenRSSFeed": "Deschide flux RSS", + "HeaderOtherFiles": "Alte Fișiere", + "HeaderPasswordAuthentication": "Autentificare cu Parolă", + "HeaderPermissions": "Permisiuni", + "HeaderPlayerQueue": "Coadă Player", + "HeaderPlayerSettings": "Setări Player", "HeaderPlaylist": "Listă de redare", "HeaderPlaylistItems": "Conținut listă", + "HeaderPodcastsToAdd": "Podcast de Adăugat", + "HeaderPresets": "Presetări", + "HeaderPreviewCover": "Previzualizare Copertă", "HeaderRSSFeedGeneral": "Date RSS", "HeaderRSSFeedIsOpen": "RSS activ", + "HeaderRSSFeeds": "Fluxuri RSS", + "HeaderRemoveEpisode": "Elimină Episod", + "HeaderRemoveEpisodes": "Elimină {0} Episoade", + "HeaderSavedMediaProgress": "Progres Media Salvat", + "HeaderSchedule": "Planifică", + "HeaderScheduleEpisodeDownloads": "Planifică Descărcare Automată a Episoadelor", + "HeaderScheduleLibraryScans": "Planifică Scanarea Automată a Librăriei", + "HeaderSession": "Sesiuni", + "HeaderSetBackupSchedule": "Planifică Backup", "HeaderSettings": "Setări", + "HeaderSettingsDisplay": "Afișaj", + "HeaderSettingsExperimental": "Caracteristici Experimentale", + "HeaderSettingsGeneral": "General", + "HeaderSettingsScanner": "Scaner", + "HeaderSettingsSecurity": "Securitate", + "HeaderSettingsWebClient": "Client Web", "HeaderSleepTimer": "Timer de somn", + "HeaderStatsLargestItems": "Cele mai mari articole", + "HeaderStatsLongestItems": "Cele mai lungi articole (ore)", "HeaderStatsMinutesListeningChart": "Minute ascultate (ultimele 7 zile)", "HeaderStatsRecentSessions": "Sesiuni recente", + "HeaderStatsTop10Authors": "Top 10 Autori", + "HeaderStatsTop5Genres": "Top 5 Genuri", "HeaderTableOfContents": "Cuprins", + "HeaderTools": "Unelte", + "HeaderUpdateAccount": "Actualizare Cont", + "HeaderUpdateApiKey": "Actualizare Cheie API", + "HeaderUpdateAuthor": "Actualizare Autor", + "HeaderUpdateDetails": "Actualizare Detalii", + "HeaderUpdateLibrary": "Actualizare Librărie", + "HeaderUsers": "Utilizatori", + "HeaderYearReview": "Trecere în revistă a anului {0}", "HeaderYourStats": "Progresul tău", + "LabelAbridged": "Abreviat", + "LabelAbridgedChecked": "Abreviat (verificat)", + "LabelAbridgedUnchecked": "Neprescurtat (neverificat)", + "LabelAccessibleBy": "Accesibil prin", + "LabelAccountType": "Tip de Cont", + "LabelAccountTypeAdmin": "Administrator", + "LabelAccountTypeGuest": "Oaspete", + "LabelAccountTypeUser": "Utilizator", + "LabelActivities": "Activități", + "LabelActivity": "Activitate", + "LabelAddToCollection": "Adaugă la Colecție", + "LabelAddToCollectionBatch": "Adaugare {0} Cărți la Colecție", "LabelAddToPlaylist": "Adaugă în listă", + "LabelAddToPlaylistBatch": "Adaugare {0} Articole la Listă", "LabelAddedAt": "Adăugat la", "LabelAddedDate": "Adăugat {0}", + "LabelAdminUsersOnly": "Doar Administratori", "LabelAll": "Toate", + "LabelAllEpisodesDownloaded": "Toate episoadele descărcate", + "LabelAllUsers": "Toți Utilizatorii", + "LabelAllUsersExcludingGuests": "Toți utilizatorii cu excepția oaspeților", + "LabelAllUsersIncludingGuests": "Toți utilizatorii inclusiv oaspeții", + "LabelAlreadyInYourLibrary": "Deja în bibliotecă", + "LabelApiKeyCreated": "Cheia API \"{0}\" creată cu succes.", + "LabelApiKeyCreatedDescription": "Copiază cheia API acum deoarece nu va mai fi disponibilă pentru vizualizare.", + "LabelApiKeyUser": "Acționează în numele utilizatorului", + "LabelApiKeyUserDescription": "Această cheie API va avea aceleași permisiuni ca utilizatorul în numele căruia acționează. In loguri va părea că utilizatorul lansa cererile.", + "LabelApiToken": "Token API", + "LabelAppend": "Atașează", + "LabelAudioBitrate": "Rata de Biți Audio (e.g. 128k)", + "LabelAudioChannels": "Canale Audio (1 sau 2)", + "LabelAudioCodec": "Codec Audio", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (Prenume Nume)", "LabelAuthorLastFirst": "Autor (Nume, Prenume)", "LabelAuthors": "Autori", "LabelAutoDownloadEpisodes": "Descarcă automat episoadele", + "LabelAutoFetchMetadata": "Descarcă Automat Metadate", + "LabelAutoFetchMetadataHelp": "Descarcă metadate pentru titlu, autor si serii pentru eficientizarea încărcării. Metadatele suplimentare s-ar putea să trebuiască potrivite după încărcare.", + "LabelAutoLaunch": "Lansare automată", + "LabelAutoLaunchDescription": "Redirecționează automat către furnizorul de autentificare când navighez la pagina de autentificare (cale de suprascriere manuală /login?autoLaunch=0)", + "LabelAutoRegister": "Înregistrare Automată", + "LabelAutoRegisterDescription": "Creează utilizatori automat dupa autentificare", + "LabelBackToUser": "Înapoi la Utilizator", + "LabelBackupAudioFiles": "Copii de rezervă a Fișierelor Audio", + "LabelBackupLocation": "Locația Copiilor de Rezervă", + "LabelBackupsEnableAutomaticBackups": "Copii de Rezervă Automate", + "LabelBackupsEnableAutomaticBackupsHelp": "Copiile de Rezervă au fost salvate în /metadata/backups", + "LabelBackupsMaxBackupSize": "Dimensiunea maximă a copiilor de rezervă (în GB) (0 pentru nelimitat)", + "LabelBackupsMaxBackupSizeHelp": "Ca protecție împotriva configurațiilor greșite, backup-ul va eșua dacă trece de limita de dimensiune configurată.", + "LabelBackupsNumberToKeep": "Numărul copiilor de siguranță de păstrat", + "LabelBackupsNumberToKeepHelp": "Doar 1 copie de siguranță va fi ștearsă odata deci dacă există mai multe copii de siguranță vor trebui șterse manual.", + "LabelBitrate": "Rată de biți", + "LabelBonus": "Bonus", "LabelBooks": "Cărți", + "LabelButtonText": "Textul Butonului", + "LabelByAuthor": "de {0}", + "LabelChangePassword": "Schimbare Parolă", + "LabelChannels": "Canale", + "LabelChapterCount": "{0} Capitole", + "LabelChapterTitle": "Titlul Capitolului", "LabelChapters": "Capitole", + "LabelChaptersFound": "capitole găsite", + "LabelClickForMoreInfo": "Click pentru mai multe informații", + "LabelClickToUseCurrentValue": "Click pentru a folosi valoarea curentă", "LabelClosePlayer": "Închide playerul", + "LabelCodec": "Codec", "LabelCollapseSeries": "Restrânge seriile", + "LabelCollapseSubSeries": "Restrânge Sub-Seriile", + "LabelCollection": "Colecție", + "LabelCollections": "Colecții", "LabelComplete": "Finalizat", + "LabelConfirmPassword": "Confirmare Parolă", "LabelContinueListening": "Ascultă în continuare", "LabelContinueReading": "Continuă lectura", "LabelContinueSeries": "Continuă seria", + "LabelCorsAllowed": "Origini CORS Permise", + "LabelCover": "Copertă", + "LabelCoverImageURL": "URL-ul Imaginii de Copertă", + "LabelCoverProvider": "Furnizor Copertă", + "LabelCreatedAt": "Creat la", + "LabelCronExpression": "Expresie Cron", + "LabelCurrent": "Curent", + "LabelCurrently": "Acum:", + "LabelCustomCronExpression": "Expresie Cron Personalizată:", + "LabelDatetime": "Data și ora", + "LabelDays": "Zile", + "LabelDeleteFromFileSystemCheckbox": "Șterge fișierele din sistem (debifeaza pentru a șterge doar din baza de date)", "LabelDescription": "Descriere", + "LabelDeselectAll": "Deselectați Tot", + "LabelDetectedPattern": "Tipar Identificat:", + "LabelDevice": "Dispozitiv", + "LabelDeviceInfo": "Informații Dispozitiv", + "LabelDeviceIsAvailableTo": "Dispozitiv accesibil lui...", + "LabelDirectory": "Dosar", + "LabelDiscFromFilename": "Disc din Numele Fișierului", + "LabelDiscFromMetadata": "Disc din Metadate", "LabelDiscover": "Descoperă", "LabelDownload": "Descarcă", + "LabelDownloadNEpisodes": "Descarcă {0} episoade", + "LabelDownloadable": "Descărcabil", "LabelDuration": "Durată", + "LabelDurationComparisonExactMatch": "(potrivire exactă)", + "LabelDurationComparisonLonger": "({0} mai lung)", + "LabelDurationComparisonShorter": "({0} mai scurt)", + "LabelDurationFound": "Durată identificată:", "LabelEbook": "Carte electronică", "LabelEbooks": "Cărți electronice", + "LabelEdit": "Editare", + "LabelEmail": "Email", + "LabelEmailSettingsFromAddress": "De la Adresa", + "LabelEmailSettingsRejectUnauthorized": "Respingere certificate neautorizate", + "LabelEmailSettingsRejectUnauthorizedHelp": "Dezactivarea verificării certificatelor SSL vă poate expune conexiunea la riscuri de securitate, cum ar fi atacuri de tip man-in-the-middle. Dezactivați această opțiune dacă înțelegeti implicațiile și aveți încredere în serverul de mail la care vă conectați.", + "LabelEmailSettingsSecure": "Sigur", + "LabelEmailSettingsSecureHelp": "Dacă e adevărat, conexiunea se va realiza prin TLS către server. Dacă e fals, TLS este folosit dacă serverul suporta extensia STARTTLS. În majoritatea cazurilor setati adevărat dacă folosiți portul 465. Pentru portul 587 sau 25 setati fals. (referinta nodemailer.com/smtp/#authentication)", + "LabelEmailSettingsTestAddress": "Adresă de Test", + "LabelEmbeddedCover": "Încorporează Copertă", "LabelEnable": "Activează", + "LabelEncodingBackupLocation": "O copie de siguranță a fișierului audio original va fi salvată în:", + "LabelEncodingChaptersNotEmbedded": "Capitolele nu sunt încorporate în cărțile audio cu mai multe track-uri.", + "LabelEncodingClearItemCache": "Asigurați-vă că ștergeți articolele din cache periodic.", + "LabelEncodingFinishedM4B": "Fișierul M4B va fi adaugat în dosarul dvs. de cărți audio când codificarea e terminată:", + "LabelEncodingInfoEmbedded": "Metadatele vor fi încorporate în fișierele audio din interiorul dosarului dvs. cu cărți audio.", + "LabelEncodingStartedNavigation": "Odată pornită sarcina poti naviga din această pagină.", + "LabelEncodingTimeWarning": "Codificarea poate dura până la 30 de minute.", + "LabelEncodingWarningAdvancedSettings": "Avertizare: Nu modificați aceste setări dacă nu sunteți familiar cu opțiunile de codare ffmpeg .", + "LabelEncodingWatcherDisabled": "Dacă ați dezactivat funcția de urmările va trebui sa rescanați acestă carte audio la ulterior.", "LabelEnd": "Sfârșit", "LabelEndOfChapter": "Sfârșitul capitolului", "LabelEpisode": "Episod", + "LabelEpisodeNotLinkedToRssFeed": "Episoade nelegate de un flux RSS", + "LabelEpisodeNumber": "Episodul #{0}", + "LabelEpisodeTitle": "Titlul Episodului", + "LabelEpisodeType": "Tipul Episodului", + "LabelEpisodeUrlFromRssFeed": "URL-ul Episodului din Fluxul RSS", + "LabelEpisodes": "Episoade", + "LabelEpisodic": "Episodic", + "LabelExample": "Exemplu", + "LabelExpandSeries": "Extinde Seriile", + "LabelExpandSubSeries": "Extinde Sub-Seriile", + "LabelExpired": "Expirat", + "LabelExpiresAt": "Expiră La", + "LabelExpiresInSeconds": "Expiră în (secunde)", + "LabelExpiresNever": "Niciodată", "LabelExplicit": "Explicit", - "LabelFeedURL": "URL flux", + "LabelExplicitChecked": "Explicit (verificat)", + "LabelExplicitUnchecked": "Neexplicit (neverificat)", + "LabelExportOPML": "Exportă OPML", + "LabelFeedURL": "Flux URL", + "LabelFetchingMetadata": "Aducere Metadate", "LabelFile": "Fișier", "LabelFileBirthtime": "Data creării fișierului", + "LabelFileBornDate": "Creat {0}", "LabelFileModified": "Fișier modificat", + "LabelFileModifiedDate": "Modificat {0}", "LabelFilename": "Nume fișier", + "LabelFilterByUser": "Filtrare după Utilizator", + "LabelFindEpisodes": "Găsire Episoade", "LabelFinished": "Finalizat", + "LabelFinishedDate": "Finalizat {0}", "LabelFolder": "Dosar", + "LabelFolders": "Dosare", + "LabelFontBold": "Îngroșat", "LabelFontBoldness": "Grosimea fontului", + "LabelFontFamily": "Familia Fontului", + "LabelFontItalic": "Cursiv", "LabelFontScale": "Mărimea fontului", + "LabelFontStrikethrough": "Tăiat cu o linie", + "LabelFormat": "Format", + "LabelFull": "Întreg", "LabelGenre": "Gen", "LabelGenres": "Genuri", + "LabelHardDeleteFile": "Ștergere definitivă a fișierului", "LabelHasEbook": "Are carte electronică", "LabelHasSupplementaryEbook": "Are carte electronică suplimentară", + "LabelHideSubtitles": "Ascunde Subtitrări", + "LabelHighestPriority": "Prioritatea cea mai ridicată", "LabelHost": "Gazdă", + "LabelHour": "Ora", + "LabelHours": "Ore", + "LabelIcon": "Pictogramă", + "LabelImageURLFromTheWeb": "URL-ul imaginii de pe web", "LabelInProgress": "În desfășurare", + "LabelIncludeInTracklist": "Include în Lista de Melodii", "LabelIncomplete": "Incomplet", + "LabelInterval": "Interval", + "LabelIntervalCustomDailyWeekly": "Personalizat zilnic/saptămânal", + "LabelIntervalEvery12Hours": "La fiecare 12 ore", + "LabelIntervalEvery15Minutes": "La fiecare 15 minute", + "LabelIntervalEvery2Hours": "La fiecare 2 ore", + "LabelIntervalEvery30Minutes": "La fiecare 30 minute", + "LabelIntervalEvery6Hours": "La fiecare 6 ore", + "LabelIntervalEveryDay": "În fiecare zi", + "LabelIntervalEveryHour": "În fiecare oră", + "LabelIntervalEveryMinute": "La fiecare minut", + "LabelInvert": "Inversează", + "LabelItem": "Articol", + "LabelJumpBackwardAmount": "Sari înapoi cu", + "LabelJumpForwardAmount": "Sari înainte cu", "LabelLanguage": "Limbă", + "LabelLanguageDefaultServer": "Limba Prestabilită a Serverului", + "LabelLanguages": "Limbi", + "LabelLastBookAdded": "Ultima Carte Adăugată", + "LabelLastBookUpdated": "Ultima Carte Actualizată", + "LabelLastProgressDate": "Ultimul progres: {0}", + "LabelLastSeen": "Ultima dată văzut", + "LabelLastTime": "Ultima dată", + "LabelLastUpdate": "Ultima actualizare", "LabelLayout": "Aspect", "LabelLayoutSinglePage": "Pagină unică", + "LabelLayoutSplitPage": "Pagină împărțită", + "LabelLess": "Mai puțin", + "LabelLibrariesAccessibleToUser": "Biblioteci Accesibile Utilizatorului", + "LabelLibrary": "Bibliotecă", + "LabelLibraryFilterSublistEmpty": "Numărul {0}", + "LabelLibraryItem": "Articol din Bibliotecă", + "LabelLibraryName": "Numele Bibliotecii", + "LabelLibrarySortByProgress": "Progres: Ultima Actualizare", + "LabelLibrarySortByProgressFinished": "Progres: Finalizat", + "LabelLibrarySortByProgressStarted": "Progres: Început", + "LabelLimit": "Limită", "LabelLineSpacing": "Spațiere între rânduri", "LabelListenAgain": "Ascultă din nou", + "LabelLogLevelDebug": "Depanare", + "LabelLogLevelInfo": "Informații", + "LabelLogLevelWarn": "Avertizare", + "LabelLookForNewEpisodesAfterDate": "Caută episoade noi după această dată", + "LabelLowestPriority": "Cea Mai Scăzută Prioritate", + "LabelMatchConfidence": "Încredere", + "LabelMatchExistingUsersBy": "Potrivire utilizatori existenți prin", + "LabelMatchExistingUsersByDescription": "Folosit pentru a conecta utilizatorii existenți. Odata conectați, utilizatorii vor fi potriviți după un ID unic trimis de furnizorul SSO", + "LabelMaxEpisodesToDownload": "Numarul maxim # de episoade de descărcat. Folosiți 0 pentru nelimitat.", + "LabelMaxEpisodesToDownloadPerCheck": "Numărul maxim # de episoade de descărcat per verificare", + "LabelMaxEpisodesToKeep": "Numarul maxim # de episoade păstrate", + "LabelMaxEpisodesToKeepHelp": "Valorea 0 nu stabilește o limită maximă. După ce un episod nou a fost descărcat automat, cel mai vechi episod va fi șters dacă aveți mai mult de X episoade. Se va șterge câte un episod vechi pentru fiecare episod nou descărcat.", + "LabelMediaPlayer": "Player Media", "LabelMediaType": "Tip media", + "LabelMetaTag": "Etichetă Meta", + "LabelMetaTags": "Etichete Meta", + "LabelMetadataOrderOfPrecedenceDescription": "Sursele de metadate cu prioritate mai mare o să suprascrie sursele de metadate cu prioritate mai mică", + "LabelMetadataProvider": "Furnizor Metadate", + "LabelMinute": "Minut", + "LabelMinutes": "Minute", "LabelMissing": "Lipsă", + "LabelMissingEbook": "Nu are carte electronică", + "LabelMissingSupplementaryEbook": "Nu are carte electronică adițională", + "LabelMobileRedirectURIs": "URL-uri de redirecționare Mobile Permise", + "LabelMobileRedirectURIsDescription": "Aceasta este o listă cu URI-uri valide pentru redirectionare a aplicațiilor mobile. URI-ul predefinit este audiobookshelf://oauth,, care poate fi sters sau suplimentat cu URI-uri adiționale pentru integrarea cu alte aplicații. Folosirea unui asterisc (*) ca singur element permite orice URI.", "LabelMore": "Mai multe", "LabelMoreInfo": "Mai multe informații", "LabelName": "Nume", "LabelNarrator": "Narator", "LabelNarrators": "Naratori", + "LabelNew": "Nou", + "LabelNewPassword": "Parolă Nouă", "LabelNewestAuthors": "Autori noi", "LabelNewestEpisodes": "Episoade noi", + "LabelNextBackupDate": "Următoarea dată a copiilor de siguranță", + "LabelNextChapters": "Următoarele capitole vor fi:", + "LabelNextScheduledRun": "Urmatoarea rulare programată", + "LabelNoApiKeys": "Nu exista chei API", + "LabelNoCustomMetadataProviders": "Nu există furnizori de metadate personalizați", + "LabelNoEpisodesSelected": "Nici un episod selectat", "LabelNotFinished": "Nefinalizat", "LabelNotStarted": "Neînceput", + "LabelNotes": "Note", + "LabelNotificationAppriseURL": "URL-ul(urile) Apprise", + "LabelNotificationAvailableVariables": "Variabile disponibile", + "LabelNotificationBodyTemplate": "Corpul Șablonului", + "LabelNotificationEvent": "Eveniment de notificare", + "LabelNotificationTitleTemplate": "Titlul Șablonului", + "LabelNotificationsMaxFailedAttempts": "Număr de încercări eșuate maxim", + "LabelNotificationsMaxFailedAttemptsHelp": "Notificările sunt dezactivate dacă nu reușesc să fie trimise de acest număr de ori", + "LabelNotificationsMaxQueueSize": "Dimensiunea maximă a cozii pentru evenimentele de notificare", + "LabelNotificationsMaxQueueSizeHelp": "Evenimentele sunt limitate la 1 per secunda. Evenimentele vor fi ignorate dacă coada este plină. Previne spamarea cu notificări.", + "LabelNumberOfBooks": "Numărul de Cărți", + "LabelNumberOfChapters": "Număr de capitole:", "LabelNumberOfEpisodes": "# de Episoade", + "LabelOpenIDAdvancedPermsClaimDescription": "Numele revendicării OpenID care conține permisiuni avansate pentru acțiunile utilizatorului în interiorul aplicației care vor fi aplicate rolurilor non-administrator (dacă e configurat). Dacă revendicarea nu e prezentă în răspunsul primit, accesul către ABS e refuzat. Dacă o singură opțiune lipsește, va fi tratată ca falsă. Asigurați-vă că revendicările furnizorului de securitate corespund structurii așteptate:", + "LabelOpenIDClaims": "Lăsați urmatoarele opțiuni goale pentru a dezactiva atribuirea avansată de grupuri și permisiuni, asignând grupul \"Utilizatori\" automat.", + "LabelOpenRSSFeed": "Flux Open RSS", + "LabelOverwrite": "Suprascrie", + "LabelPaginationPageXOfY": "Pagina {0} din {1}", "LabelPassword": "Parolă", "LabelPath": "Cale", + "LabelPermanent": "Permanent", + "LabelPermissionsAccessAllLibraries": "Poate accesa toate bibliotecile", + "LabelPermissionsAccessAllTags": "Poate accesa toate etichetele", + "LabelPermissionsAccessExplicitContent": "Poate Accesa Conținut Explicit", + "LabelPermissionsCreateEreader": "Poate Crea Cititoare Electronice", + "LabelPermissionsDelete": "Poate Șterge", + "LabelPermissionsDownload": "Poate Descărca", + "LabelPermissionsUpdate": "Poate Actualiza", + "LabelPermissionsUpload": "Poate Încărca", + "LabelPersonalYearReview": "Recapitularea Anului tău ({0})", + "LabelPhotoPathURL": "Calea/URL-ul Fotografiei", + "LabelPlayMethod": "Metoda de Redare", + "LabelPlaybackRateIncrementDecrement": "Incrementare/Decrementare a Ratei de Redare cu", + "LabelPlayerChapterNumberMarker": "{0} din {1}", + "LabelPlaylists": "Liste de redare", "LabelPodcast": "Podcast", + "LabelPodcastSearchRegion": "Regiunea căutării podcastului", + "LabelPodcastType": "Tipul Podcastului", "LabelPodcasts": "Podcasturi", + "LabelPort": "Portul", + "LabelPrefixesToIgnore": "Prefix de ignorat (fără a ține cont de majuscule)", "LabelPreventIndexing": "Împiedică indexarea fluxului în directoarele iTunes și Google Podcasts", + "LabelPrimaryEbook": "eCarte Principală", "LabelProgress": "Progres", + "LabelProvider": "Furnizor", + "LabelProviderAuthorizationValue": "Valoarea Antetului de Autorizare", "LabelPubDate": "Data publicării", "LabelPublishYear": "Anul publicării", "LabelPublishedDate": "Publicat la {0}", + "LabelPublishedDecade": "Deceniul Publicării", + "LabelPublishedDecades": "Deceniile Publicării", + "LabelPublisher": "Editor", + "LabelPublishers": "Editori", "LabelRSSFeedCustomOwnerEmail": "Email personalizat al proprietarului", "LabelRSSFeedCustomOwnerName": "Nume personalizat al proprietarului", "LabelRSSFeedOpen": "Flux RSS deschis", "LabelRSSFeedPreventIndexing": "Previne indexarea", "LabelRSSFeedSlug": "Identificator flux RSS", + "LabelRSSFeedURL": "URL-ul Fluxului RSS", "LabelRandomly": "Aleatoriu", + "LabelReAddSeriesToContinueListening": "Readăugare serie la \"Continuă să asculți\"", "LabelRead": "Citește", "LabelReadAgain": "Citește din nou", + "LabelReadEbookWithoutProgress": "Citire eCarte fără a memora progresul", "LabelRecentSeries": "Serii recente", "LabelRecentlyAdded": "Adăugate recent", + "LabelRecommended": "Recomandat", + "LabelRedo": "Refă", + "LabelRegion": "Regiune", + "LabelReleaseDate": "Data Lansării", + "LabelRemoveAllMetadataAbs": "Ștergerea tuturor fișierelor metadata.abs", + "LabelRemoveAllMetadataJson": "Ștergerea tuturor fișierelor metadata.json", + "LabelRemoveAudibleBranding": "Ștergerea Audible intro și outro din capitole", + "LabelRemoveCover": "Șterge coperta", + "LabelRemoveMetadataFile": "Șterge fisierele metadate din dosarele bibliotecii", + "LabelRemoveMetadataFileHelp": "Șterge toate fișierele metadata.json și metadata.abs din {0} dosare.", + "LabelRowsPerPage": "Rânduri pe pagină", + "LabelSearchTerm": "Termen de căutat", + "LabelSearchTitle": "Titlu de căutat", + "LabelSearchTitleOrASIN": "Titlu de căutat sau ASN", "LabelSeason": "Sezon", + "LabelSeasonNumber": "Sezonul #{0}", + "LabelSelectAll": "Selectează tot", + "LabelSelectAllEpisodes": "Selectează toate episoadele", + "LabelSelectEpisodesShowing": "Selectează {0} episoade dintre cele afișate", + "LabelSelectUser": "Selectare utilizator", + "LabelSelectUsers": "Selectare utilizatori", + "LabelSendEbookToDevice": "Trimite eCarte către...", + "LabelSequence": "Secvență", + "LabelSerial": "Serie", "LabelSeries": "Serii", + "LabelSeriesName": "Numele Seriilor", + "LabelSeriesProgress": "Progresul Seriilor", + "LabelServerLogLevel": "Nivelul de Jurnal al Serverului", + "LabelServerYearReview": "Anul Serverului în Retrospectivă ({0})", "LabelSetEbookAsPrimary": "Setează ca principală", "LabelSetEbookAsSupplementary": "Setează ca suplimentară", + "LabelSettingsAllowIframe": "Permite încorporarea intr-un iframe", + "LabelSettingsAudiobooksOnly": "Doar cărți audio", + "LabelSettingsAudiobooksOnlyHelp": "Activarea acestei set[ri va ignora fișierele eBook daca acestea nu se află într-un dosar al unei cărți audio, caz în care vor fi setate ca eBook suplimentar", + "LabelSettingsBookshelfViewHelp": "Design scheumorf cu rafturi de lemn", + "LabelSettingsChromecastSupport": "Suport Chromecast", + "LabelSettingsDateFormat": "Formatul Datei", + "LabelSettingsEnableWatcher": "Urmărește în mod automat bibliotecile pentru schimbări", + "LabelSettingsEnableWatcherForLibrary": "Urmărește în mod automat biblioteca pentru schimbări", "LabelShowAll": "Afișează tot", "LabelSize": "Dimensiune", "LabelSleepTimer": "Timer de somn", diff --git a/client/strings/ru.json b/client/strings/ru.json index c84fe9dcf..ed3f18b89 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -392,7 +392,7 @@ "LabelGenre": "Жанр", "LabelGenres": "Жанры", "LabelHardDeleteFile": "Жесткое удаление файла", - "LabelHasEbook": "Есть e-книга", + "LabelHasEbook": "Есть электронная книга", "LabelHasSupplementaryEbook": "Есть дополнительная e-книга", "LabelHideSubtitles": "Скрыть серии", "LabelHighestPriority": "Наивысший приоритет", @@ -437,8 +437,8 @@ "LabelLibraryItem": "Элемент библиотеки", "LabelLibraryName": "Имя библиотеки", "LabelLibrarySortByProgress": "Прогресс: Последнее обновление", - "LabelLibrarySortByProgressFinished": "Прогресс: Завершено", - "LabelLibrarySortByProgressStarted": "Прогресс: Начато", + "LabelLibrarySortByProgressFinished": "Прогресс: Закончена", + "LabelLibrarySortByProgressStarted": "Прогресс: Начата", "LabelLimit": "Лимит", "LabelLineSpacing": "Межстрочный интервал", "LabelListenAgain": "Послушать снова", diff --git a/client/strings/sk.json b/client/strings/sk.json index e0d02898c..23ed276e8 100644 --- a/client/strings/sk.json +++ b/client/strings/sk.json @@ -23,7 +23,7 @@ "ButtonClearFilter": "Zrušiť filter", "ButtonClose": "Uzavrieť", "ButtonCloseFeed": "Zatvoriť zdroj", - "ButtonCloseSession": "Ukončiť otvorené pripojenie", + "ButtonCloseSession": "Ukončiť aktívne relácie", "ButtonCollections": "Kolekcie", "ButtonConfigureScanner": "Nastaviť skener", "ButtonCreate": "Vytvoriť", @@ -150,12 +150,12 @@ "HeaderIgnoredFiles": "Ignorované súbory", "HeaderItemFiles": "Položka Súbory", "HeaderItemMetadataUtils": "Položka Nástroje metadát", - "HeaderLastListeningSession": "Posledné pripojenie", + "HeaderLastListeningSession": "Posledná relácia", "HeaderLatestEpisodes": "Posledné epizódy", "HeaderLibraries": "Knižnice", "HeaderLibraryFiles": "Súbory knižnice", "HeaderLibraryStats": "Štatistiky knižnice", - "HeaderListeningSessions": "Pripojenia", + "HeaderListeningSessions": "Relácie", "HeaderListeningStats": "Štatistiky počúvania", "HeaderLogin": "Prihlásenie", "HeaderLogs": "Záznamy udalostí", @@ -172,7 +172,7 @@ "HeaderNotificationUpdate": "Aktualizovať notifikáciu", "HeaderNotifications": "Notifikácie", "HeaderOpenIDConnectAuthentication": "Overenie pripojenia OpenID", - "HeaderOpenListeningSessions": "Aktívne pripojenia", + "HeaderOpenListeningSessions": "Aktívne relácie", "HeaderOpenRSSFeed": "Otvoriť RSS zdroj", "HeaderOtherFiles": "Ostatné súbory", "HeaderPasswordAuthentication": "Overenie heslom", @@ -189,7 +189,7 @@ "HeaderRSSFeeds": "RSS zdroje", "HeaderRemoveEpisode": "Odstrániť epizódu", "HeaderRemoveEpisodes": "Odstrániť {0} epizód", - "HeaderSavedMediaProgress": "Priebeh uložených médií", + "HeaderSavedMediaProgress": "Stav uložených médií", "HeaderSchedule": "Plán", "HeaderScheduleEpisodeDownloads": "Naplánovať automatické sťahovanie epizód", "HeaderScheduleLibraryScans": "Naplánovanovať automatické skenovanie knižnice", @@ -222,7 +222,7 @@ "LabelAbridged": "Skrátená verzia", "LabelAbridgedChecked": "Skrátená verzia (zaškrtnuté)", "LabelAbridgedUnchecked": "Neskrátená verzia (nezaškrtnuté)", - "LabelAccessibleBy": "Prístupné pre", + "LabelAccessibleBy": "Dostupné pre", "LabelAccountType": "Typ účtu", "LabelAccountTypeAdmin": "Administrátor", "LabelAccountTypeGuest": "Hosť", @@ -230,7 +230,7 @@ "LabelActivities": "Aktivity", "LabelActivity": "Aktivita", "LabelAddToCollection": "Pridať do zbierky", - "LabelAddToCollectionBatch": "Pridať {0} kníh do kolekcie", + "LabelAddToCollectionBatch": "Pridať {0} kníh do zbierky", "LabelAddToPlaylist": "Pridať do playlistu", "LabelAddToPlaylistBatch": "Pridať {0} položie do playlistu", "LabelAddedAt": "Pridané", @@ -288,8 +288,8 @@ "LabelCodec": "Kodek", "LabelCollapseSeries": "Zbaliť série", "LabelCollapseSubSeries": "Zbaliť podsérie", - "LabelCollection": "Kolekcia", - "LabelCollections": "Kolekcie", + "LabelCollection": "Zbierka", + "LabelCollections": "Zbierky", "LabelComplete": "Hotovo", "LabelConfirmPassword": "Potvrdiť heslo", "LabelContinueListening": "Pokračovať v počúvaní", @@ -383,7 +383,7 @@ "LabelFolders": "Priečinky", "LabelFontBold": "Tučné", "LabelFontBoldness": "Hrúbka písma", - "LabelFontFamily": "Rodina písiem", + "LabelFontFamily": "písmo", "LabelFontItalic": "Kurzíva", "LabelFontScale": "Veľkosť písma", "LabelFontStrikethrough": "Preškrtnuté", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Žiadne {0}", "LabelLibraryItem": "Položka knižnice", "LabelLibraryName": "Názov knižnice", - "LabelLibrarySortByProgress": "Pokrok: Aktualizované", - "LabelLibrarySortByProgressFinished": "Pokrok: Dokončené", - "LabelLibrarySortByProgressStarted": "Pokrok: Začiatok", + "LabelLibrarySortByProgress": "Stav: Naposledy aktualizované", + "LabelLibrarySortByProgressFinished": "Stav: Dokončené", + "LabelLibrarySortByProgressStarted": "Stav: Začal", "LabelLimit": "Limit", "LabelLineSpacing": "Riadkovanie", "LabelListenAgain": "Počúvať znova", @@ -528,7 +528,7 @@ "LabelPrefixesToIgnore": "Ignorované predpony (bez ohľadu na veľkosť písmen)", "LabelPreventIndexing": "Zabráni indexácii vašich zdrojov službami iTunes a Google podcast directories", "LabelPrimaryEbook": "Primárny e-book", - "LabelProgress": "Stav", + "LabelProgress": "Aktuálny stav", "LabelProvider": "Poskytovateľ", "LabelProviderAuthorizationValue": "Obsah hlavičky autorizácie", "LabelPubDate": "Dátum publikovania", @@ -548,7 +548,7 @@ "LabelReAddSeriesToContinueListening": "Znova pridať série do pokračujúceho počúvania", "LabelRead": "Načítať", "LabelReadAgain": "Čítať znova", - "LabelReadEbookWithoutProgress": "Čítať e-knihu bez sledovania pokroku", + "LabelReadEbookWithoutProgress": "Čítať e-knihu bez zmeny stavu", "LabelRecentSeries": "Posledné série", "LabelRecentlyAdded": "Posledné pridané", "LabelRecommended": "Odporúčané", @@ -577,7 +577,7 @@ "LabelSerial": "Na pokračovanie", "LabelSeries": "Série", "LabelSeriesName": "Názov série", - "LabelSeriesProgress": "Pokrok série", + "LabelSeriesProgress": "Aktuálny stav série", "LabelServerLogLevel": "Úroveň logovania servera", "LabelServerYearReview": "Rok servera v prehľade ({0})", "LabelSetEbookAsPrimary": "Nastaviť ako primárny", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Skeuomorfný dizajn s drevenými poličkami", "LabelSettingsChromecastSupport": "Podpora chromecastu", "LabelSettingsDateFormat": "Formát dátumu", - "LabelSettingsEnableWatcher": "Automatické skenovanie knižníc pre zmeny", - "LabelSettingsEnableWatcherForLibrary": "Automaticky skenovať knižnicu pre zmeny", + "LabelSettingsEnableWatcher": "Automatické sledovanie zmien v knižniciach", + "LabelSettingsEnableWatcherForLibrary": "Automatické sledovanie zmien v knižnici", "LabelSettingsEnableWatcherHelp": "Povoliť automatické pridávanie/aktualizácie položiek pri zmene súborov. *Vyžaduje reštart servera", "LabelSettingsEpubsAllowScriptedContent": "Povoliť v e-knihách skriptovaný obsah", "LabelSettingsEpubsAllowScriptedContentHelp": "Povoliť e-knihám spúšťanie skriptov. Odporúča sa túto voľbu nepovolovať, pokiaľ plne nedôverujete zdrojom súborov e-kníh.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Ste si istý, že chcete resetnúť kapitoly a zahodiť zmeny, ktoré ste vykonali?", "MessageRestoreBackupConfirm": "Ste si istí, že chcete obnoviť zálohu vytvorenú", "MessageRestoreBackupWarning": "Obnovenie zálohy spôsobí kompletný prepis databázy umiestnenej v /config a obrázkov prebalov a autorov v /metadata/items a /metadata/authors.

Zálohy nemenia žiadne súbory v priečinkoch vašej knižnice. Ak ste povolili v nastaveniach servera ukladanie obrázkov prebalov a metadát v priečinkoch knižnice, tieto nie sú zálohované a teda ani prepisované.

Všetky klienti používajúci váš server budú automaticky obnovené.", - "MessageScheduleLibraryScanNote": "Pre väčšinu používateľov sa odporúča ponechať túto funkciu vypnutú a povoliť nastavenia funkcie sledovania obsahu priečinku. Funkcia sledovania priečinku bude automaticky detekovať zmeny v priečinkoch knižnice. Táto funkcia však nefunguje pre všetky súborové systémy (ako napr. NFS), v tom prípade využite funkciu plánovaného skenovania knižnice.", + "MessageScheduleLibraryScanNote": "Pre väčšinu používateľov sa odporúča nechať túto funkciu vypnutú a ponechať zapnuté nastavenie „Automatické sledovanie zmien v knižnici“ – táto funkcia automaticky zistí zmeny vo vašich priečinkoch knižnice. Túto funkciu zapnite, ak „Automatické sledovanie zmien v knižnici“ nefunguje vo vašom súborovom systéme (napr. NFS).", "MessageScheduleRunEveryWeekdayAtTime": "Spustiť každú {0} o {1}", "MessageSearchResultsFor": "Výsledky vyhľadávania pre", "MessageSelected": "{0} vybrané", @@ -1015,7 +1015,7 @@ "ToastCachePurgeFailed": "Vyčistenie vyrovnávacej pamäte zlyhalo", "ToastCachePurgeSuccess": "Vyrovnávacia pamäť vyčistená", "ToastChapterLocked": "Kapitola je zamknutá.", - "ToastChapterStartTimeAdjusted": "Čas začiatku kapitoly upravený o {0} sek.", + "ToastChapterStartTimeAdjusted": "Čas začiatku kapitoly upravený o {0} sekúnd", "ToastChaptersAllLocked": "Všetky kapitoly sú zamknuté. Odomknite niektoré kapitoly, aby ste posunuli ich časy.", "ToastChaptersHaveErrors": "Kapitoly obsahujú chyby", "ToastChaptersInvalidShiftAmountLast": "Neplatná hodnota veľkosti posunutia. Začiatok poslednej kapitoly by ležal za koncom audioknihy.", @@ -1026,6 +1026,8 @@ "ToastCollectionItemsAddFailed": "Pridanie položky/-iek do zbierky zlyhalo", "ToastCollectionRemoveSuccess": "Zbierka odstránená", "ToastCollectionUpdateSuccess": "Zbierka aktualizovaná", + "ToastConnectionNotAvailable": "Pripojenie je nedostupné. Skúste to neskôr", + "ToastCoverSearchFailed": "Vyhľadanie obalu sa nepodarilo", "ToastCoverUpdateFailed": "Aktualizácia prebalu zlyhala", "ToastDateTimeInvalidOrIncomplete": "Dátum a čas sú neplatné alebo neúplné", "ToastDeleteFileFailed": "Odstránenie súboru zlyhalo", diff --git a/client/strings/sl.json b/client/strings/sl.json index 3b7ce53a2..c88be4ba1 100644 --- a/client/strings/sl.json +++ b/client/strings/sl.json @@ -104,7 +104,7 @@ "ButtonStartM4BEncode": "Zaženi M4B prekodiranje", "ButtonStartMetadataEmbed": "Začni vdelavo metapodatkov", "ButtonStats": "Statistika", - "ButtonSubmit": "Potrdi", + "ButtonSubmit": "Pošlji", "ButtonTest": "Test", "ButtonUnlinkOpenId": "Prekini povezavo OpenID", "ButtonUpload": "Naloži", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ne {0}", "LabelLibraryItem": "Element knjižnice", "LabelLibraryName": "Ime knjižnice", - "LabelLibrarySortByProgress": "Napredek: Nazadnje posodobljen", - "LabelLibrarySortByProgressFinished": "Napredej: Končano", - "LabelLibrarySortByProgressStarted": "Napredek: Začeto", + "LabelLibrarySortByProgress": "Napredek: Zadnja posodobitev", + "LabelLibrarySortByProgressFinished": "Napredek: Končano", + "LabelLibrarySortByProgressStarted": "Napredek: Začelo se je", "LabelLimit": "Omejitev", "LabelLineSpacing": "Vrstični razmak", "LabelListenAgain": "Poslušaj znova", diff --git a/client/strings/sv.json b/client/strings/sv.json index d2fb254ed..fa0267e82 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -1,6 +1,6 @@ { "ButtonAdd": "Lägg till", - "ButtonAddApiKey": "Addera API-nyckel", + "ButtonAddApiKey": "Lägg till API-nyckel", "ButtonAddChapters": "Lägg till kapitel", "ButtonAddDevice": "Lägg till enhet", "ButtonAddLibrary": "Lägg till bibliotek", @@ -48,7 +48,7 @@ "ButtonLogout": "Logga ut", "ButtonLookup": "Sök", "ButtonManageTracks": "Hantera spår", - "ButtonMapChapterTitles": "Karta kapitelrubriker", + "ButtonMapChapterTitles": "Mappa kapitelrubriker", "ButtonMatchAllAuthors": "Matcha alla författare", "ButtonMatchBooks": "Matcha böcker", "ButtonNevermind": "Glöm det", @@ -104,7 +104,7 @@ "ButtonStartM4BEncode": "Starta M4B-omkodning", "ButtonStartMetadataEmbed": "Infoga metadata", "ButtonStats": "Statistik", - "ButtonSubmit": "Spara", + "ButtonSubmit": "Skicka", "ButtonTest": "Testa", "ButtonUnlinkOpenId": "Koppla ifrån OpenID", "ButtonUpload": "Ladda upp", @@ -123,7 +123,7 @@ "HeaderAdvanced": "Avancerad", "HeaderApiKeys": "API-nyckel", "HeaderAppriseNotificationSettings": "Inställningar av meddelanden med Apprise", - "HeaderAudioTracks": "Ljudfiler", + "HeaderAudioTracks": "Ljudspår", "HeaderAudiobookTools": "Hantering av ljudboksfiler", "HeaderAuthentication": "Autentisering", "HeaderBackups": "Säkerhetskopior", @@ -202,7 +202,7 @@ "HeaderSettingsScanner": "Skanner", "HeaderSettingsSecurity": "Säkerhet", "HeaderSettingsWebClient": "Webklient", - "HeaderSleepTimer": "Timer för att sova", + "HeaderSleepTimer": "Insomningstimer", "HeaderStatsLargestItems": "Största objekten", "HeaderStatsLongestItems": "Längsta objekten (timmar)", "HeaderStatsMinutesListeningChart": "Minuters lyssning (senaste 7 dagarna)", @@ -231,10 +231,10 @@ "LabelActivity": "Aktivitet", "LabelAddToCollection": "Lägg till i en samling", "LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen", - "LabelAddToPlaylist": "Lägg till i en spellista", + "LabelAddToPlaylist": "Lägg till i spellista", "LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan", "LabelAddedAt": "Datum adderad", - "LabelAddedDate": "Adderad {0}", + "LabelAddedDate": "Tillagd {0}", "LabelAdminUsersOnly": "Endast administratörer", "LabelAll": "Alla", "LabelAllEpisodesDownloaded": "Alla avsnitt är nedladdade", @@ -363,14 +363,14 @@ "LabelExpiresAt": "Gäller till och med", "LabelExpiresInSeconds": "Upphör om (sekunder)", "LabelExpiresNever": "Aldrig", - "LabelExplicit": "Bestämd", + "LabelExplicit": "Vuxeninnehåll", "LabelExplicitChecked": "Explicit version (markerad)", "LabelExplicitUnchecked": "Ej Explicit version (ej markerad)", "LabelExportOPML": "Exportera OPML-information", "LabelFeedURL": "URL-adress för flödet", "LabelFetchingMetadata": "Hämtar metadata", "LabelFile": "Fil", - "LabelFileBirthtime": "Tidpunkt, adderad", + "LabelFileBirthtime": "Tidpunkt, tillagd", "LabelFileBornDate": "Skapad {0}", "LabelFileModified": "Tidpunkt, ändrad", "LabelFileModifiedDate": "Ändrad {0}", @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "Ingen {0}", "LabelLibraryItem": "Objekt", "LabelLibraryName": "Biblioteksnamn", - "LabelLibrarySortByProgress": "Framsteg: senast uppdaterat", - "LabelLibrarySortByProgressFinished": "Framsteg: avslutad", - "LabelLibrarySortByProgressStarted": "Framsteg: påbörjad", + "LabelLibrarySortByProgress": "Status: Senast uppdaterad", + "LabelLibrarySortByProgressFinished": "Status: Avslutad", + "LabelLibrarySortByProgressStarted": "Status: Startad", "LabelLimit": "Begränsning", "LabelLineSpacing": "Radavstånd", "LabelListenAgain": "Lyssna igen", @@ -629,7 +629,7 @@ "LabelShowSeconds": "Visa i sekunder", "LabelShowSubtitles": "Visa underrubriker", "LabelSize": "Storlek", - "LabelSleepTimer": "Sovtimer", + "LabelSleepTimer": "Insomningstimer", "LabelSlug": "Kortnamn", "LabelSortAscending": "Stigande", "LabelSortDescending": "Fallande", diff --git a/client/strings/tr.json b/client/strings/tr.json index af64a618d..770521cb8 100644 --- a/client/strings/tr.json +++ b/client/strings/tr.json @@ -275,7 +275,7 @@ "LabelBonus": "Bonus", "LabelBooks": "Kitaplar", "LabelButtonText": "Buton Metni", - "LabelByAuthor": "Yazar: {0}", + "LabelByAuthor": "{0} tarafından", "LabelChangePassword": "Şifreyi Değiştir", "LabelChannels": "Kanallar", "LabelChapterCount": "{0} Bölüm", @@ -383,7 +383,7 @@ "LabelFolders": "Klasörler", "LabelFontBold": "Kalın", "LabelFontBoldness": "Yazı Tipi Kalınlığı", - "LabelFontFamily": "Yazı Tipi Ailesi", + "LabelFontFamily": "Yazı tipi ailesi", "LabelFontItalic": "İtalik", "LabelFontScale": "Yazı Tipi Ölçeği", "LabelFontStrikethrough": "Üstü Çizili", @@ -588,8 +588,8 @@ "LabelSettingsBookshelfViewHelp": "Ahşap raflı skeuomorfik tasarım", "LabelSettingsChromecastSupport": "Chromecast desteği", "LabelSettingsDateFormat": "Tarih Formatı", - "LabelSettingsEnableWatcher": "Değişiklikler için kütüphaneleri otomatik olarak tara", - "LabelSettingsEnableWatcherForLibrary": "Değişiklikler için kütüphaneyi otomatik olarak tara", + "LabelSettingsEnableWatcher": "Kütüphanelerdeki değişiklikleri otomatik olarak izle", + "LabelSettingsEnableWatcherForLibrary": "Kütüphanedeki değişiklikleri otomatik olarak izle", "LabelSettingsEnableWatcherHelp": "Dosya değişiklikleri algılandığında öğelerin otomatik olarak eklenmesini/güncellenmesini sağlar. *Sunucunun yeniden başlatılmasını gerektirir", "LabelSettingsEpubsAllowScriptedContent": "Epub'larda betiklenmiş içeriğe izin ver", "LabelSettingsEpubsAllowScriptedContentHelp": "Epub dosyalarının betik çalıştırmasına izin verin. Epub dosyalarının kaynağına güvenmiyorsanız bu ayarı devre dışı bırakmanız önerilir.", @@ -888,7 +888,7 @@ "MessageResetChaptersConfirm": "Bölümleri sıfırlamak ve yaptığınız değişiklikleri geri almak istediğinizden emin misiniz?", "MessageRestoreBackupConfirm": "Şu tarihte oluşturulan yedeği geri yüklemek istediğinizden emin misiniz", "MessageRestoreBackupWarning": "Bir yedeği geri yüklemek, /config konumundaki tüm veritabanının ve /metadata/items & /metadata/authors içindeki kapak resimlerinin üzerine yazacaktır.

Yedekler, kütüphane klasörlerinizdeki hiçbir dosyayı değiştirmez. Sunucu ayarlarını kütüphane klasörlerinizde kapak resmi ve üst veri saklamak için etkinleştirdiyseniz, bunlar yedeklenmez veya üzerine yazılmaz.

Sunucunuzu kullanan tüm istemciler otomatik olarak yenilenecektir.", - "MessageScheduleLibraryScanNote": "Çoğu kullanıcı için, bu özelliği devre dışı bırakıp klasör izleyici ayarını etkin tutmaları önerilir. Klasör izleyici, kütüphane klasörlerinizdeki değişiklikleri otomatik olarak algılayacaktır. Klasör izleyici her dosya sistemi için (NFS gibi) çalışmaz, bu nedenle bunun yerine zamanlanmış kütüphane taramaları kullanılabilir.", + "MessageScheduleLibraryScanNote": "Çoğu kullanıcı için bu ayarı pasif bırakması ve \"Kütüphanedeki değişiklikleri otomatik olarak izle\" seçeneğini aktif etmesi önerilir. O seçenek kütüphane dizinlerindeki herhangi bir değişikliği otomatik olarak tespit edecektir. Eğer dosya sisteminiz \"Kütüphanedeki değişiklikleri otomatik olarak izle\" yöntemini desteklemiyorsa (örn; NFS dosya sistemi) bu özelliği aktif edebilirsiniz.", "MessageScheduleRunEveryWeekdayAtTime": "Her {0} günü saat {1}'de çalıştır", "MessageSearchResultsFor": "Arama sonuçları", "MessageSelected": "{0} seçildi", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 64c41619e..14c70cb10 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -436,9 +436,9 @@ "LabelLibraryFilterSublistEmpty": "没有 {0}", "LabelLibraryItem": "媒体库项目", "LabelLibraryName": "媒体库名称", - "LabelLibrarySortByProgress": "收听进度: 上次收听时间", - "LabelLibrarySortByProgressFinished": "收听进度: 已完成的", - "LabelLibrarySortByProgressStarted": "收听进度: 已开始的", + "LabelLibrarySortByProgress": "进度: 上次更新", + "LabelLibrarySortByProgressFinished": "进度: 已完成", + "LabelLibrarySortByProgressStarted": "进度: 已开始", "LabelLimit": "限制", "LabelLineSpacing": "行间距", "LabelListenAgain": "再次收听", diff --git a/custom-metadata-provider-specification.yaml b/custom-metadata-provider-specification.yaml index 71cbba23a..a0ec172ae 100644 --- a/custom-metadata-provider-specification.yaml +++ b/custom-metadata-provider-specification.yaml @@ -127,7 +127,7 @@ components: duration: type: integer format: int64 - description: Duration in seconds + description: Duration in minutes SeriesMetadata: type: object diff --git a/package-lock.json b/package-lock.json index 08707893d..8950b4903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.32.1", + "version": "2.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.32.1", + "version": "2.34.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 3ee3fb391..10ba26f65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.32.1", + "version": "2.34.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 12a92903c..7306411ef 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -258,6 +258,13 @@ class TokenManager { } const user = await Database.userModel.getUserById(apiKey.userId) + + if (!user?.isActive) { + // deny login + done(null, null) + return + } + done(null, user) } else { // JWT based authentication diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 50eeda31a..80471ec47 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -10,7 +10,7 @@ const CacheManager = require('../managers/CacheManager') const CoverManager = require('../managers/CoverManager') const AuthorFinder = require('../finders/AuthorFinder') -const { reqSupportsWebp, isValidASIN } = require('../utils/index') +const { reqSupportsWebp, isValidASIN, clampPositiveInt } = require('../utils/index') const naturalSort = createNewSortInstance({ comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare @@ -113,7 +113,7 @@ class AuthorController { payload.lastFirst = Database.authorModel.getLastFirst(payload.name) } - // Check if author name matches another author and merge the authors + // Check if author name matches another author in the same library and merge the authors let existingAuthor = null if (authorNameUpdate) { existingAuthor = await Database.authorModel.findOne({ @@ -121,7 +121,8 @@ class AuthorController { id: { [sequelize.Op.not]: req.author.id }, - name: payload.name + name: payload.name, + libraryId: req.author.libraryId } }) } @@ -411,8 +412,8 @@ class AuthorController { const options = { format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'), - height: height ? parseInt(height) : null, - width: width ? parseInt(width) : null + height: clampPositiveInt(height ? parseInt(height) : null, 4096), + width: clampPositiveInt(width ? parseInt(width) : null, 4096) } return CacheManager.handleAuthorCache(res, authorId, options) } diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 475adfe0f..bb00ea346 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -3,6 +3,7 @@ const Sequelize = require('sequelize') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +const htmlSanitizer = require('../utils/htmlSanitizer') const RssFeedManager = require('../managers/RssFeedManager') @@ -31,13 +32,19 @@ class CollectionController { async create(req, res) { const reqBody = req.body || {} + const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name) + // Validation - if (!reqBody.name || !reqBody.libraryId) { + if (!nameCleaned || !reqBody.libraryId) { return res.status(400).send('Invalid collection data') } if (reqBody.description && typeof reqBody.description !== 'string') { return res.status(400).send('Invalid collection description') } + if (!req.user.checkCanAccessLibrary(reqBody.libraryId)) { + Logger.warn(`[CollectionController] User "${req.user.username}" attempted to create collection in inaccessible library ${reqBody.libraryId}`) + return res.sendStatus(403) + } const libraryItemIds = (reqBody.books || []).filter((b) => !!b && typeof b == 'string') if (!libraryItemIds.length) { return res.status(400).send('Invalid collection data. No books') @@ -65,7 +72,7 @@ class CollectionController { newCollection = await Database.collectionModel.create( { libraryId: reqBody.libraryId, - name: reqBody.name, + name: nameCleaned, description: reqBody.description || null }, { transaction } @@ -106,8 +113,9 @@ class CollectionController { */ async findAll(req, res) { const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user) + const accessibleCollections = collectionsExpanded.filter((c) => req.user.checkCanAccessLibrary(c.libraryId)) res.json({ - collections: collectionsExpanded + collections: accessibleCollections }) } @@ -145,9 +153,12 @@ class CollectionController { collectionUpdatePayload.description = req.body.description wasUpdated = true } - if (req.body.name !== undefined && req.body.name !== req.collection.name) { - collectionUpdatePayload.name = req.body.name - wasUpdated = true + if (req.body.name !== undefined && typeof req.body.name === 'string') { + const nameCleaned = htmlSanitizer.stripAllTags(req.body.name) + if (nameCleaned !== req.collection.name) { + collectionUpdatePayload.name = nameCleaned + wasUpdated = true + } } if (wasUpdated) { @@ -425,6 +436,10 @@ class CollectionController { if (!collection) { return res.status(404).send('Collection not found') } + if (!req.user.checkCanAccessLibrary(collection.libraryId)) { + Logger.warn(`[CollectionController] User "${req.user.username}" attempted to access collection ${collection.id} in inaccessible library ${collection.libraryId}`) + return res.status(404).send('Collection not found') + } req.collection = collection } diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index 4b0a94b39..41e082fd4 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -117,7 +117,7 @@ class FileSystemController { filepath = fileUtils.filePathToPOSIX(filepath) // Ensure filepath is inside library folder (prevents directory traversal) - if (!filepath.startsWith(libraryFolder.path)) { + if (!fileUtils.isSameOrSubPath(libraryFolder.path, filepath)) { Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`) return res.sendStatus(400) } diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 55ef45690..73b3d5c60 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -462,7 +462,7 @@ class LibraryController { } } Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`) - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id) } if (authorIds.length) { @@ -563,7 +563,7 @@ class LibraryController { mediaItemIds.push(libraryItem.mediaId) } Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`) - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id) } // Set PlaybackSessions libraryId to null @@ -714,7 +714,7 @@ class LibraryController { } } Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" with issue`) - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id) } if (authorIds.length) { @@ -1435,10 +1435,15 @@ class LibraryController { const libraryItems = await Database.libraryItemModel.findAll({ attributes: ['id', 'libraryId', 'path', 'isFile'], where: { - id: itemIds + id: itemIds, + libraryId: req.library.id } }) + if (libraryItems.length < itemIds.length) { + Logger.warn(`[LibraryController] User "${req.user.username}" requested ${itemIds.length} items but only ${libraryItems.length} are in library "${req.library.id}"`) + } + Logger.info(`[LibraryController] User "${req.user.username}" requested download for items "${itemIds}"`) const filename = `LibraryItems-${Date.now()}.zip` diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 5247dbb06..07b4ee67f 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -1,13 +1,14 @@ const { Request, Response, NextFunction } = require('express') const Path = require('path') const fs = require('../libs/fsExtra') +const cron = require('../libs/nodeCron') const uaParserJs = require('../libs/uaParser') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const zipHelpers = require('../utils/zipHelpers') -const { reqSupportsWebp } = require('../utils/index') +const { reqSupportsWebp, clampPositiveInt } = require('../utils/index') const { ScanResult, AudioMimeType } = require('../utils/constants') const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils') const LibraryItemScanner = require('../scanner/LibraryItemScanner') @@ -36,6 +37,24 @@ const ShareManager = require('../managers/ShareManager') * @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile */ +/** + * Enforce per-item access for batch item routes + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {import('../models/LibraryItem')[]} libraryItems + * @returns {boolean} true if the user may access every item; false if 403 was sent + */ +function ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems) { + for (const libraryItem of libraryItems) { + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + res.sendStatus(403) + return false + } + } + return true +} + class LibraryItemController { constructor() {} @@ -111,7 +130,7 @@ class LibraryItemController { } } - await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds, req.libraryItem.libraryId) if (hardDelete) { Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) await fs.remove(libraryItemPath).catch((error) => { @@ -202,6 +221,11 @@ class LibraryItemController { } else if (mediaPayload.autoDownloadSchedule !== undefined && req.libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) { isPodcastAutoDownloadUpdated = true } + + if (mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) { + Logger.error(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${req.libraryItem.media.title}"`) + return res.status(400).send('Invalid auto download schedule cron expression') + } } let hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url @@ -398,8 +422,8 @@ class LibraryItemController { const options = { format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'), - height: height ? parseInt(height) : null, - width: width ? parseInt(width) : null + height: clampPositiveInt(height ? parseInt(height) : null, 4096), + width: clampPositiveInt(width ? parseInt(width) : null, 4096) } return CacheManager.handleCoverCache(res, libraryItemId, options) } @@ -547,7 +571,13 @@ class LibraryItemController { return res.sendStatus(404) } + // Ensure user has permission to delete these library items + if (!ensureUserCanAccessLibraryItemsForBatch(req, res, itemsToDelete)) { + return + } + const libraryId = itemsToDelete[0].libraryId + for (const libraryItem of itemsToDelete) { const libraryItemPath = libraryItem.path Logger.info(`[LibraryItemController] (${hardDelete ? 'Hard' : 'Soft'}) deleting Library Item "${libraryItem.media.title}" with id "${libraryItem.id}"`) @@ -565,7 +595,7 @@ class LibraryItemController { authorIds.push(...libraryItem.media.authors.map((au) => au.id)) } } - await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds) + await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, libraryItem.libraryId) if (hardDelete) { Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`) await fs.remove(libraryItemPath).catch((error) => { @@ -581,6 +611,7 @@ class LibraryItemController { } await Database.resetLibraryIssuesFilterData(libraryId) + res.sendStatus(200) } @@ -593,6 +624,11 @@ class LibraryItemController { * @param {Response} res */ async batchUpdate(req, res) { + if (!req.user.canUpdate) { + Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to batch update without permission`) + return res.sendStatus(403) + } + const updatePayloads = req.body if (!Array.isArray(updatePayloads) || !updatePayloads.length) { Logger.error(`[LibraryItemController] Batch update failed. Invalid payload`) @@ -615,6 +651,11 @@ class LibraryItemController { return res.sendStatus(404) } + // Ensure user has permission to update these library items + if (!ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems)) { + return + } + let itemsUpdated = 0 const seriesIdsRemoved = [] @@ -624,6 +665,11 @@ class LibraryItemController { const mediaPayload = updatePayload.mediaPayload const libraryItem = libraryItems.find((li) => li.id === updatePayload.id) + if (libraryItem.isPodcast && mediaPayload.autoDownloadSchedule && !cron.validate(mediaPayload.autoDownloadSchedule)) { + Logger.warn(`[LibraryItemController] Invalid auto download schedule cron expression "${mediaPayload.autoDownloadSchedule}" for library item "${libraryItem.media.title}" - skipping update`) + continue + } + let hasUpdates = await libraryItem.media.updateFromRequest(mediaPayload) if (libraryItem.isBook && Array.isArray(mediaPayload.metadata?.series)) { @@ -695,6 +741,10 @@ class LibraryItemController { const libraryItems = await Database.libraryItemModel.findAllExpandedWhere({ id: libraryItemIds }) + // Ensure user has permission to access these library items + if (!ensureUserCanAccessLibraryItemsForBatch(req, res, libraryItems)) { + return + } res.json({ libraryItems: libraryItems.map((li) => li.toOldJSONExpanded()) }) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 51773a5ad..c5968f521 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -63,7 +63,7 @@ class MeController { * @param {Response} res */ async getItemListeningSessions(req, res) { - const libraryItem = await Database.libraryItemModel.findByPk(req.params.libraryItemId) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.libraryItemId) const episode = await Database.podcastEpisodeModel.findByPk(req.params.episodeId) if (!libraryItem || (libraryItem.isPodcast && !episode)) { @@ -71,6 +71,12 @@ class MeController { return res.sendStatus(404) } + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to access listening sessions for library item "${req.params.libraryItemId}" without access`) + return res.sendStatus(403) + } + const mediaItemId = episode?.id || libraryItem.mediaId let listeningSessions = await this.getUserItemListeningSessionsHelper(req.user.id, mediaItemId) @@ -125,6 +131,13 @@ class MeController { * @param {Response} res */ async removeMediaProgress(req, res) { + // Verify the media progress belongs to the current user + const mediaProgress = req.user.mediaProgresses.find((mp) => mp.id === req.params.id) + if (!mediaProgress) { + Logger.error(`[MeController] Media progress not found or does not belong to user "${req.user.username}"`) + return res.sendStatus(404) + } + await Database.mediaProgressModel.removeById(req.params.id) req.user.mediaProgresses = req.user.mediaProgresses.filter((mp) => mp.id !== req.params.id) @@ -192,7 +205,16 @@ class MeController { * @param {Response} res */ async createBookmark(req, res) { - if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id) + if (!libraryItem) { + return res.sendStatus(404) + } + + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to create bookmark for library item "${req.params.id}" without access`) + return res.sendStatus(403) + } const { time, title } = req.body if (isNullOrNaN(time)) { @@ -216,7 +238,16 @@ class MeController { * @param {Response} res */ async updateBookmark(req, res) { - if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id) + if (!libraryItem) { + return res.sendStatus(404) + } + + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to update bookmark for library item "${req.params.id}" without access`) + return res.sendStatus(403) + } const { time, title } = req.body if (isNullOrNaN(time)) { @@ -245,7 +276,16 @@ class MeController { * @param {Response} res */ async removeBookmark(req, res) { - if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) + const libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id) + if (!libraryItem) { + return res.sendStatus(404) + } + + // Check if user has access to this library item + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[MeController] User "${req.user.username}" attempted to remove bookmark for library item "${req.params.id}" without access`) + return res.sendStatus(403) + } const time = Number(req.params.time) if (isNaN(time)) { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 490cb27d2..591f8ccf2 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -8,7 +8,7 @@ const Database = require('../Database') const Watcher = require('../Watcher') const libraryItemFilters = require('../utils/queries/libraryItemFilters') -const patternValidation = require('../libs/nodeCron/pattern-validation') +const cron = require('../libs/nodeCron') const { isObject, getTitleIgnorePrefix } = require('../utils/index') const { sanitizeFilename } = require('../utils/fileUtils') @@ -605,13 +605,11 @@ class MiscController { return res.sendStatus(400) } - try { - patternValidation(expression) - res.sendStatus(200) - } catch (error) { - Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message) - res.status(400).send(error.message) + if (!cron.validate(expression)) { + Logger.warn(`[MiscController] Invalid cron expression ${expression}`) + return res.status(400).send('Invalid cron expression') } + res.sendStatus(200) } /** diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 972c352a4..6ad7cff9e 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -2,6 +2,7 @@ const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +const htmlSanitizer = require('../utils/htmlSanitizer') /** * @typedef RequestUserObject @@ -29,12 +30,17 @@ class PlaylistController { const reqBody = req.body || {} // Validation - if (!reqBody.name || !reqBody.libraryId) { + const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name) + if (!nameCleaned || !reqBody.libraryId) { return res.status(400).send('Invalid playlist data') } if (reqBody.description && typeof reqBody.description !== 'string') { return res.status(400).send('Invalid playlist description') } + if (!req.user.checkCanAccessLibrary(reqBody.libraryId)) { + Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to create playlist in inaccessible library ${reqBody.libraryId}`) + return res.sendStatus(403) + } const items = reqBody.items || [] const isPodcast = items.some((i) => i.episodeId) const libraryItemIds = new Set() @@ -84,7 +90,7 @@ class PlaylistController { { libraryId: reqBody.libraryId, userId: req.user.id, - name: reqBody.name, + name: nameCleaned, description: reqBody.description || null }, { transaction } @@ -131,8 +137,9 @@ class PlaylistController { */ async findAllForUser(req, res) { const playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id) + const accessiblePlaylists = playlistsForUser.filter((p) => req.user.checkCanAccessLibrary(p.libraryId)) res.json({ - playlists: playlistsForUser + playlists: accessiblePlaylists }) } @@ -174,7 +181,11 @@ class PlaylistController { } const playlistUpdatePayload = {} - if (reqBody.name) playlistUpdatePayload.name = reqBody.name + + const nameCleaned = htmlSanitizer.stripAllTags(reqBody.name) + if (nameCleaned) { + playlistUpdatePayload.name = nameCleaned + } if (reqBody.description) playlistUpdatePayload.description = reqBody.description // Update name and description @@ -502,6 +513,10 @@ class PlaylistController { if (!collection) { return res.status(404).send('Collection not found') } + if (!req.user.checkCanAccessLibrary(collection.libraryId)) { + Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to create playlist from collection ${collection.id} in inaccessible library ${collection.libraryId}`) + return res.status(404).send('Collection not found') + } // Expand collection to get library items const collectionExpanded = await collection.getOldJsonExpanded(req.user) if (!collectionExpanded) { @@ -567,6 +582,10 @@ class PlaylistController { Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`) return res.sendStatus(403) } + if (!req.user.checkCanAccessLibrary(playlist.libraryId)) { + Logger.warn(`[PlaylistController] User "${req.user.username}" attempted to access playlist ${playlist.id} in inaccessible library ${playlist.libraryId}`) + return res.status(404).send('Playlist not found') + } req.playlist = playlist } diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 1ebe1d110..85173e622 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -5,9 +5,10 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const fs = require('../libs/fsExtra') +const cron = require('../libs/nodeCron') const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils') -const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils') +const { getFileTimestampsWithIno, filePathToPOSIX, isSameOrSubPath } = require('../utils/fileUtils') const { validateUrl } = require('../utils/index') const htmlSanitizer = require('../utils/htmlSanitizer') @@ -46,6 +47,11 @@ class PodcastController { return res.status(400).send('Invalid request body. "media" and "media.metadata" are required') } + if (payload.media.autoDownloadSchedule && !cron.validate(payload.media.autoDownloadSchedule)) { + Logger.error(`[PodcastController] Invalid auto download schedule cron expression "${payload.media.autoDownloadSchedule}"`) + return res.status(400).send('Invalid auto download schedule cron expression') + } + const library = await Database.libraryModel.findByIdWithFolders(payload.libraryId) if (!library) { Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) @@ -58,8 +64,18 @@ class PodcastController { return res.status(404).send('Folder not found') } + if (typeof payload.path !== 'string' || !payload.path.trim()) { + return res.status(400).send('Invalid request body. "path" must be a non-empty string') + } + + const libraryFolderPath = filePathToPOSIX(folder.path) const podcastPath = filePathToPOSIX(payload.path) + if (!isSameOrSubPath(libraryFolderPath, podcastPath)) { + Logger.error(`[PodcastController] Create: Podcast path is outside library folder "${libraryFolderPath}": "${podcastPath}"`) + return res.status(400).send('Podcast path must be inside the selected library folder') + } + // Check if a library item with this podcast folder exists already const existingLibraryItem = (await Database.libraryItemModel.count({ @@ -83,7 +99,7 @@ class PodcastController { const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath) - let relPath = payload.path.replace(folder.fullPath, '') + let relPath = podcastPath.replace(libraryFolderPath, '') if (relPath.startsWith('/')) relPath = relPath.slice(1) let newLibraryItem = null @@ -412,6 +428,12 @@ class PodcastController { Logger.debug(`[PodcastController] Sanitized description from "${req.body[key]}" to "${sanitizedDescription}"`) req.body[key] = sanitizedDescription } + } else if (key === 'subtitle' && req.body[key]) { + const sanitizedSubtitle = htmlSanitizer.sanitize(req.body[key]) + if (sanitizedSubtitle !== req.body[key]) { + Logger.debug(`[PodcastController] Sanitized subtitle from "${req.body[key]}" to "${sanitizedSubtitle}"`) + req.body[key] = sanitizedSubtitle + } } updatePayload[key] = req.body[key] diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 3e7ea1deb..73da84a8a 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -53,6 +53,10 @@ class ShareController { if (playbackSession) { if (mediaItemShare.id === playbackSession.mediaItemShareId) { Logger.debug(`[ShareController] Found share playback session ${req.cookies.share_session_id}`) + // If ?t was provided, override the cached currentTime + if (startTime > 0 && startTime < playbackSession.duration) { + playbackSession.currentTime = startTime + } mediaItemShare.playbackSession = playbackSession.toJSONForClient() return res.json(mediaItemShare) } else { diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index 6d31b2ab9..90fed951f 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -227,7 +227,7 @@ class BookFinder { title = this.#removeAuthorFromTitle(title) const titleTransformers = [ - [/([,:;_]| by ).*/g, ''], // Remove subtitle + [/(: |[,;_]| by ).*/g, ''], // Remove subtitle [/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate [/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition [/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type @@ -646,11 +646,11 @@ class BookFinder { module.exports = new BookFinder() function hasSubtitle(title) { - return title.includes(':') || title.includes(' - ') + return title.includes(': ') || title.includes(' - ') } function stripSubtitle(title) { - if (title.includes(':')) { - return title.split(':')[0].trim() + if (title.includes(': ')) { + return title.split(': ')[0].trim() } else if (title.includes(' - ')) { return title.split(' - ')[0].trim() } diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index 2d8eece85..e34e4c055 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -5,6 +5,9 @@ const Database = require('../Database') class ApiCacheManager { defaultCacheOptions = { max: 1000, maxSize: 10 * 1000 * 1000, sizeCalculation: (item) => item.body.length + JSON.stringify(item.headers).length } defaultTtlOptions = { ttl: 30 * 60 * 1000 } + highChurnModels = new Set(['session', 'mediaProgress', 'playbackSession', 'device']) + modelsInvalidatingPersonalized = new Set(['mediaProgress']) + modelsInvalidatingMe = new Set(['session', 'mediaProgress', 'playbackSession', 'device']) constructor(cache = new LRUCache(this.defaultCacheOptions), ttlOptions = this.defaultTtlOptions) { this.cache = cache @@ -16,8 +19,47 @@ class ApiCacheManager { hooks.forEach((hook) => database.sequelize.addHook(hook, (model) => this.clear(model, hook))) } + getModelName(model) { + if (typeof model?.name === 'string') return model.name + if (typeof model?.model?.name === 'string') return model.model.name + if (typeof model?.constructor?.name === 'string' && model.constructor.name !== 'Object') return model.constructor.name + return 'unknown' + } + + clearByUrlPattern(urlPattern) { + let removed = 0 + for (const key of this.cache.keys()) { + try { + const parsed = JSON.parse(key) + if (typeof parsed?.url === 'string' && urlPattern.test(parsed.url)) { + if (this.cache.delete(key)) removed++ + } + } catch { + if (this.cache.delete(key)) removed++ + } + } + return removed + } + + clearUserProgressSlices(modelName, hook) { + let removedPersonalized = 0 + let removedRecentEpisodes = 0 + if (this.modelsInvalidatingPersonalized.has(modelName)) { + removedPersonalized = this.clearByUrlPattern(/^\/libraries\/[^/]+\/personalized/) + removedRecentEpisodes = this.clearByUrlPattern(/^\/libraries\/[^/]+\/recent-episodes/) + } + const removedMe = this.modelsInvalidatingMe.has(modelName) ? this.clearByUrlPattern(/^\/me(\/|\?|$)/) : 0 + Logger.debug(`[ApiCacheManager] ${modelName}.${hook}: cleared user-progress cache slices (personalized=${removedPersonalized}, recentEpisodes=${removedRecentEpisodes}, me=${removedMe})`) + } + clear(model, hook) { - Logger.debug(`[ApiCacheManager] ${model.constructor.name}.${hook}: Clearing cache`) + const modelName = this.getModelName(model) + if (this.highChurnModels.has(modelName)) { + this.clearUserProgressSlices(modelName, hook) + return + } + + Logger.debug(`[ApiCacheManager] ${modelName}.${hook}: Clearing cache`) this.cache.clear() } diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index 2697f94ea..a7b531e62 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -126,13 +126,31 @@ class BackupManager { } catch (error) { // Not a valid zip file Logger.error('[BackupManager] Failed to read backup file - backup might not be a valid .zip file', tempPath, error) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(400).send('Failed to read backup file - backup might not be a valid .zip file') } - if (!Object.keys(entries).includes('absdatabase.sqlite')) { + if (!entries['absdatabase.sqlite']) { Logger.error(`[BackupManager] Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(500).send('Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.') } + const detailsEntry = entries['details'] + if (!detailsEntry) { + Logger.error('[BackupManager] Invalid backup - missing details entry') + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) + return res.status(400).send('Invalid backup file - missing details entry') + } + if (detailsEntry.size > 1024 * 1024) { + Logger.error(`[BackupManager] Backup details entry too large: ${detailsEntry.size} bytes`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) + return res.status(400).send('Invalid backup file - details entry too large') + } + const data = await zip.entryData('details') const details = data.toString('utf8').split('\n') @@ -140,9 +158,13 @@ class BackupManager { if (!backup.serverVersion) { Logger.error(`[BackupManager] Invalid backup with no server version - might be a backup created before version 2.0.0`) + await zip.close().catch(() => {}) + await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err)) return res.status(500).send('Invalid backup. Might be a backup created before version 2.0.0.') } + await zip.close().catch(() => {}) + backup.fileSize = await getFileSize(backup.fullPath) const existingBackupIndex = this.backups.findIndex((b) => b.id === backup.id) @@ -257,9 +279,24 @@ class BackupManager { let data = null try { zip = new StreamZip.async({ file: fullFilePath }) + const entries = await zip.entries() + + const detailsEntry = entries['details'] + if (!detailsEntry) { + Logger.error(`[BackupManager] Backup "${fullFilePath}" missing details entry - skipping`) + await zip.close().catch(() => {}) + continue + } + if (detailsEntry.size > 1024 * 1024) { + Logger.error(`[BackupManager] Backup "${fullFilePath}" details entry too large (${detailsEntry.size} bytes) - skipping`) + await zip.close().catch(() => {}) + continue + } + data = await zip.entryData('details') } catch (error) { Logger.error(`[BackupManager] Failed to unzip backup "${fullFilePath}"`, error) + if (zip) await zip.close().catch(() => {}) continue } diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index d3e652129..7138d26a8 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -153,6 +153,11 @@ class CronManager { startPodcastCron(expression, libraryItemIds) { try { + if (!cron.validate(expression)) { + Logger.error(`[CronManager] Invalid auto download schedule cron expression "${expression}" - not starting podcast episode check cron`) + return + } + Logger.debug(`[CronManager] Scheduling podcast episode check cron "${expression}" for ${libraryItemIds.length} item(s)`) const task = cron.schedule(expression, () => { if (this.podcastCronExpressionsExecuting.includes(expression)) { @@ -167,7 +172,7 @@ class CronManager { task }) } catch (error) { - Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.podcastEpisodeSchedule}`, error) + Logger.error(`[PodcastManager] Failed to schedule podcast cron ${expression}`, error) } } diff --git a/server/migrations/v2.33.0-add-discover-query-indexes.js b/server/migrations/v2.33.0-add-discover-query-indexes.js new file mode 100644 index 000000000..ebd92bbaa --- /dev/null +++ b/server/migrations/v2.33.0-add-discover-query-indexes.js @@ -0,0 +1,74 @@ +/** + * @typedef MigrationContext + * @property {import('sequelize').QueryInterface} queryInterface + * @property {import('../Logger')} logger + * + * @typedef MigrationOptions + * @property {MigrationContext} context + */ + +const migrationVersion = '2.33.0' +const migrationName = `${migrationVersion}-add-discover-query-indexes` +const loggerPrefix = `[${migrationVersion} migration]` + +const indexes = [ + { + table: 'mediaProgresses', + name: 'media_progresses_user_item_finished_time', + fields: ['userId', 'mediaItemId', 'isFinished', 'currentTime'] + }, + { + table: 'bookSeries', + name: 'book_series_series_book', + fields: ['seriesId', 'bookId'] + } +] + +async function up({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) + + for (const index of indexes) { + await addIndexIfMissing(queryInterface, logger, index) + } + + logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) +} + +async function down({ context: { queryInterface, logger } }) { + logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) + + for (const index of indexes) { + await removeIndexIfExists(queryInterface, logger, index) + } + + logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) +} + +async function addIndexIfMissing(queryInterface, logger, index) { + const existing = await queryInterface.showIndex(index.table) + if (existing.some((i) => i.name === index.name)) { + logger.info(`${loggerPrefix} index ${index.name} already exists on ${index.table}`) + return + } + + logger.info(`${loggerPrefix} adding index ${index.name} on ${index.table}(${index.fields.join(', ')})`) + await queryInterface.addIndex(index.table, { + name: index.name, + fields: index.fields + }) + logger.info(`${loggerPrefix} added index ${index.name}`) +} + +async function removeIndexIfExists(queryInterface, logger, index) { + const existing = await queryInterface.showIndex(index.table) + if (!existing.some((i) => i.name === index.name)) { + logger.info(`${loggerPrefix} index ${index.name} does not exist on ${index.table}`) + return + } + + logger.info(`${loggerPrefix} removing index ${index.name}`) + await queryInterface.removeIndex(index.table, index.name) + logger.info(`${loggerPrefix} removed index ${index.name}`) +} + +module.exports = { up, down } diff --git a/server/models/Author.js b/server/models/Author.js index 287b66976..65561e211 100644 --- a/server/models/Author.js +++ b/server/models/Author.js @@ -111,16 +111,17 @@ class Author extends Model { * * @param {string} name * @param {string} libraryId - * @returns {Promise} + * @returns {Promise<{ author: Author, created: boolean }>} */ static async findOrCreateByNameAndLibrary(name, libraryId) { const author = await this.getByNameAndLibrary(name, libraryId) - if (author) return author - return this.create({ + if (author) return { author, created: false } + const newAuthor = await this.create({ name, lastFirst: this.getLastFirst(name), libraryId }) + return { author: newAuthor, created: true } } /** diff --git a/server/models/Book.js b/server/models/Book.js index 96371f3a2..d9f2ff132 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -4,6 +4,7 @@ const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') const parseNameString = require('../utils/parsers/parseNameString') const htmlSanitizer = require('../utils/htmlSanitizer') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') +const SocketAuthority = require('../SocketAuthority') /** * @typedef EBookFileObject @@ -470,13 +471,23 @@ class Book extends Model { for (const author of authorsRemoved) { await bookAuthorModel.removeByIds(author.id, this.id) + const numBooks = await bookAuthorModel.getCountForAuthor(author.id) + if (numBooks > 0) { + SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks)) + } Logger.debug(`[Book] "${this.title}" Removed author "${author.name}"`) this.authors = this.authors.filter((au) => au.id !== author.id) } const authorsAdded = [] for (const authorName of newAuthorNames) { - const author = await authorModel.findOrCreateByNameAndLibrary(authorName, libraryId) + const { author, created } = await authorModel.findOrCreateByNameAndLibrary(authorName, libraryId) await bookAuthorModel.create({ bookId: this.id, authorId: author.id }) + if (created) { + SocketAuthority.emitter('author_added', author.toOldJSON()) + } else { + const numBooks = await bookAuthorModel.getCountForAuthor(author.id) + SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks)) + } Logger.debug(`[Book] "${this.title}" Added author "${author.name}"`) this.authors.push(author) authorsAdded.push(author) diff --git a/server/models/BookSeries.js b/server/models/BookSeries.js index 31eccb9fa..e71f28a8e 100644 --- a/server/models/BookSeries.js +++ b/server/models/BookSeries.js @@ -48,6 +48,10 @@ class BookSeries extends Model { { name: 'bookSeries_seriesId', fields: ['seriesId'] + }, + { + name: 'book_series_series_book', + fields: ['seriesId', 'bookId'] } ] } diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 16a521615..b33fa585c 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -340,6 +340,15 @@ class LibraryItem extends Model { const shelves = [] + const timed = async (loader) => { + const start = Date.now() + const payload = await loader() + return { + payload, + elapsedSeconds: ((Date.now() - start) / 1000).toFixed(2) + } + } + // "Continue Listening" shelf const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, false) if (itemsInProgressPayload.items.length) { @@ -371,11 +380,18 @@ class LibraryItem extends Model { } Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) - let start = Date.now() if (library.isBook) { - start = Date.now() + const [continueSeriesResult, mostRecentResult, seriesMostRecentResult, discoverResult, mediaFinishedResult, newestAuthorsResult] = await Promise.all([ + timed(() => libraryFilters.getLibraryItemsContinueSeries(library, user, include, limit)), + timed(() => libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit)), + timed(() => libraryFilters.getSeriesMostRecentlyAdded(library, user, include, 5)), + timed(() => libraryFilters.getLibraryItemsToDiscover(library, user, include, limit)), + timed(() => libraryFilters.getMediaFinished(library, user, include, limit)), + timed(() => libraryFilters.getNewestAuthors(library, user, limit)) + ]) + + const continueSeriesPayload = continueSeriesResult.payload // "Continue Series" shelf - const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, user, include, limit) if (continueSeriesPayload.libraryItems.length) { shelves.push({ id: 'continue-series', @@ -386,42 +402,24 @@ class LibraryItem extends Model { total: continueSeriesPayload.count }) } - Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } else if (library.isPodcast) { - // "Newest Episodes" shelf - const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit) - if (newestEpisodesPayload.libraryItems.length) { + Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${continueSeriesResult.elapsedSeconds}s`) + + const mostRecentPayload = mostRecentResult.payload + // "Recently Added" shelf + if (mostRecentPayload.libraryItems.length) { shelves.push({ - id: 'newest-episodes', - label: 'Newest Episodes', - labelStringKey: 'LabelNewestEpisodes', - type: 'episode', - entities: newestEpisodesPayload.libraryItems, - total: newestEpisodesPayload.count + id: 'recently-added', + label: 'Recently Added', + labelStringKey: 'LabelRecentlyAdded', + type: library.mediaType, + entities: mostRecentPayload.libraryItems, + total: mostRecentPayload.count }) } - Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } + Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${mostRecentResult.elapsedSeconds}s`) - start = Date.now() - // "Recently Added" shelf - const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit) - if (mostRecentPayload.libraryItems.length) { - shelves.push({ - id: 'recently-added', - label: 'Recently Added', - labelStringKey: 'LabelRecentlyAdded', - type: library.mediaType, - entities: mostRecentPayload.libraryItems, - total: mostRecentPayload.count - }) - } - Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - if (library.isBook) { - start = Date.now() + const seriesMostRecentPayload = seriesMostRecentResult.payload // "Recent Series" shelf - const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, user, include, 5) if (seriesMostRecentPayload.series.length) { shelves.push({ id: 'recent-series', @@ -432,11 +430,10 @@ class LibraryItem extends Model { total: seriesMostRecentPayload.count }) } - Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${seriesMostRecentResult.elapsedSeconds}s`) - start = Date.now() + const discoverLibraryItemsPayload = discoverResult.payload // "Discover" shelf - const discoverLibraryItemsPayload = await libraryFilters.getLibraryItemsToDiscover(library, user, include, limit) if (discoverLibraryItemsPayload.libraryItems.length) { shelves.push({ id: 'discover', @@ -447,45 +444,41 @@ class LibraryItem extends Model { total: discoverLibraryItemsPayload.count }) } - Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - } + Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${discoverResult.elapsedSeconds}s`) - start = Date.now() - // "Listen Again" shelf - const mediaFinishedPayload = await libraryFilters.getMediaFinished(library, user, include, limit) - if (mediaFinishedPayload.items.length) { - const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) - const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast') + const mediaFinishedPayload = mediaFinishedResult.payload + // "Listen Again" shelf + if (mediaFinishedPayload.items.length) { + const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) + const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast') - if (audioItemsInProgress.length) { - shelves.push({ - id: 'listen-again', - label: 'Listen Again', - labelStringKey: 'LabelListenAgain', - type: library.isPodcast ? 'episode' : 'book', - entities: audioItemsInProgress, - total: mediaFinishedPayload.count - }) + if (audioItemsInProgress.length) { + shelves.push({ + id: 'listen-again', + label: 'Listen Again', + labelStringKey: 'LabelListenAgain', + type: library.isPodcast ? 'episode' : 'book', + entities: audioItemsInProgress, + total: mediaFinishedPayload.count + }) + } + + if (ebookOnlyItemsInProgress.length) { + // "Read Again" shelf + shelves.push({ + id: 'read-again', + label: 'Read Again', + labelStringKey: 'LabelReadAgain', + type: 'book', + entities: ebookOnlyItemsInProgress, + total: mediaFinishedPayload.count + }) + } } + Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${mediaFinishedResult.elapsedSeconds}s`) - // "Read Again" shelf - if (ebookOnlyItemsInProgress.length) { - shelves.push({ - id: 'read-again', - label: 'Read Again', - labelStringKey: 'LabelReadAgain', - type: 'book', - entities: ebookOnlyItemsInProgress, - total: mediaFinishedPayload.count - }) - } - } - Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`) - - if (library.isBook) { - start = Date.now() + const newestAuthorsPayload = newestAuthorsResult.payload // "Newest Authors" shelf - const newestAuthorsPayload = await libraryFilters.getNewestAuthors(library, user, limit) if (newestAuthorsPayload.authors.length) { shelves.push({ id: 'newest-authors', @@ -496,7 +489,72 @@ class LibraryItem extends Model { total: newestAuthorsPayload.count }) } - Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`) + Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${newestAuthorsResult.elapsedSeconds}s`) + } else if (library.isPodcast) { + const [newestEpisodesResult, mostRecentResult, mediaFinishedResult] = await Promise.all([ + timed(() => libraryFilters.getNewestPodcastEpisodes(library, user, limit)), + timed(() => libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit)), + timed(() => libraryFilters.getMediaFinished(library, user, include, limit)) + ]) + + const newestEpisodesPayload = newestEpisodesResult.payload + // "Newest Episodes" shelf + if (newestEpisodesPayload.libraryItems.length) { + shelves.push({ + id: 'newest-episodes', + label: 'Newest Episodes', + labelStringKey: 'LabelNewestEpisodes', + type: 'episode', + entities: newestEpisodesPayload.libraryItems, + total: newestEpisodesPayload.count + }) + } + Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${newestEpisodesResult.elapsedSeconds}s`) + + const mostRecentPayload = mostRecentResult.payload + // "Recently Added" shelf + if (mostRecentPayload.libraryItems.length) { + shelves.push({ + id: 'recently-added', + label: 'Recently Added', + labelStringKey: 'LabelRecentlyAdded', + type: library.mediaType, + entities: mostRecentPayload.libraryItems, + total: mostRecentPayload.count + }) + } + Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${mostRecentResult.elapsedSeconds}s`) + + const mediaFinishedPayload = mediaFinishedResult.payload + // "Listen Again" shelf + if (mediaFinishedPayload.items.length) { + const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks) + const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast') + + if (audioItemsInProgress.length) { + shelves.push({ + id: 'listen-again', + label: 'Listen Again', + labelStringKey: 'LabelListenAgain', + type: 'episode', + entities: audioItemsInProgress, + total: mediaFinishedPayload.count + }) + } + + if (ebookOnlyItemsInProgress.length) { + // "Read Again" shelf + shelves.push({ + id: 'read-again', + label: 'Read Again', + labelStringKey: 'LabelReadAgain', + type: 'book', + entities: ebookOnlyItemsInProgress, + total: mediaFinishedPayload.count + }) + } + } + Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${mediaFinishedResult.elapsedSeconds}s`) } Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`) diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 0ebe2f598..9c0269a9e 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -80,6 +80,10 @@ class MediaProgress extends Model { indexes: [ { fields: ['updatedAt'] + }, + { + name: 'media_progresses_user_item_finished_time', + fields: ['userId', 'mediaItemId', 'isFinished', 'currentTime'] } ] } diff --git a/server/models/Podcast.js b/server/models/Podcast.js index a96e1dd02..6dbe1cd19 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -78,6 +78,7 @@ class Podcast extends Model { */ static async createFromRequest(payload, transaction) { const title = typeof payload.metadata.title === 'string' ? payload.metadata.title : null + // cron expression validated in controller const autoDownloadSchedule = typeof payload.autoDownloadSchedule === 'string' ? payload.autoDownloadSchedule : null const genres = Array.isArray(payload.metadata.genres) && payload.metadata.genres.every((g) => typeof g === 'string' && g.length) ? payload.metadata.genres : [] const tags = Array.isArray(payload.tags) && payload.tags.every((t) => typeof t === 'string' && t.length) ? payload.tags : [] @@ -89,6 +90,9 @@ class Podcast extends Model { } }) + const rawDescription = typeof payload.metadata.description === 'string' ? payload.metadata.description : null + const description = rawDescription ? htmlSanitizer.sanitize(rawDescription) : null + return this.create( { title, @@ -97,7 +101,7 @@ class Podcast extends Model { releaseDate: typeof payload.metadata.releaseDate === 'string' ? payload.metadata.releaseDate : null, feedURL: typeof payload.metadata.feedUrl === 'string' ? payload.metadata.feedUrl : null, imageURL: typeof payload.metadata.imageUrl === 'string' ? payload.metadata.imageUrl : null, - description: typeof payload.metadata.description === 'string' ? payload.metadata.description : null, + description, itunesPageURL: typeof payload.metadata.itunesPageUrl === 'string' ? payload.metadata.itunesPageUrl : null, itunesId: typeof payload.metadata.itunesId === 'string' ? payload.metadata.itunesId : null, itunesArtistId: typeof payload.metadata.itunesArtistId === 'string' ? payload.metadata.itunesArtistId : null, @@ -270,6 +274,7 @@ class Podcast extends Model { hasUpdates = true } if (typeof payload.autoDownloadSchedule === 'string' && payload.autoDownloadSchedule !== this.autoDownloadSchedule) { + // cron expression validated in controller this.autoDownloadSchedule = payload.autoDownloadSchedule hasUpdates = true } diff --git a/server/models/User.js b/server/models/User.js index 36b2eca98..936efde13 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -782,7 +782,14 @@ class User extends Model { error: 'Library item not found', statusCode: 404 } + } else if (libraryItem.mediaType !== 'book') { + Logger.error(`[User] createUpdateMediaProgress: library item ${progressPayload.libraryItemId} is not a book`) + return { + error: 'Library item is not a book', + statusCode: 400 + } } + mediaItemId = libraryItem.media.id mediaProgress = libraryItem.media.mediaProgresses?.[0] } diff --git a/server/objects/DeviceInfo.js b/server/objects/DeviceInfo.js index ceff6c32e..22ebfbea4 100644 --- a/server/objects/DeviceInfo.js +++ b/server/objects/DeviceInfo.js @@ -1,6 +1,10 @@ -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 +const { stripAllTags } = require('../utils/htmlSanitizer') class DeviceInfo { + /** @type {string[]} Fields to sanitize when loading from stored data */ + static stringFields = ['deviceId', 'clientVersion', 'manufacturer', 'model', 'sdkVersion', 'clientName', 'deviceName'] + constructor(deviceInfo = null) { this.id = null this.userId = null @@ -31,7 +35,7 @@ class DeviceInfo { construct(deviceInfo) { for (const key in deviceInfo) { if (deviceInfo[key] !== undefined && this[key] !== undefined) { - this[key] = deviceInfo[key] + this[key] = DeviceInfo.stringFields.includes(key) ? stripAllTags(deviceInfo[key]) : deviceInfo[key] } } } @@ -63,7 +67,8 @@ class DeviceInfo { } get deviceDescription() { - if (this.model) { // Set from mobile apps + if (this.model) { + // Set from mobile apps if (this.sdkVersion) return `${this.model} SDK ${this.sdkVersion} / v${this.clientVersion}` return `${this.model} / v${this.clientVersion}` } @@ -72,18 +77,7 @@ class DeviceInfo { // When client doesn't send a device id getTempDeviceId() { - const keys = [ - this.userId, - this.browserName, - this.browserVersion, - this.osName, - this.osVersion, - this.clientVersion, - this.manufacturer, - this.model, - this.sdkVersion, - this.ipAddress - ].map(k => k || '') + const keys = [this.userId, this.browserName, this.browserVersion, this.osName, this.osVersion, this.clientVersion, this.manufacturer, this.model, this.sdkVersion, this.ipAddress].map((k) => k || '') return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') } @@ -99,12 +93,12 @@ class DeviceInfo { this.osVersion = ua?.os.version || null this.deviceType = ua?.device.type || null - this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion - this.manufacturer = clientDeviceInfo?.manufacturer || null - this.model = clientDeviceInfo?.model || null - this.sdkVersion = clientDeviceInfo?.sdkVersion || null + this.clientVersion = stripAllTags(clientDeviceInfo?.clientVersion) || serverVersion + this.manufacturer = stripAllTags(clientDeviceInfo?.manufacturer) || null + this.model = stripAllTags(clientDeviceInfo?.model) || null + this.sdkVersion = stripAllTags(clientDeviceInfo?.sdkVersion) || null - this.clientName = clientDeviceInfo?.clientName || null + this.clientName = stripAllTags(clientDeviceInfo?.clientName) || null if (this.sdkVersion) { if (!this.clientName) this.clientName = 'Abs Android' this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}` @@ -149,4 +143,4 @@ class DeviceInfo { return hasUpdates } } -module.exports = DeviceInfo \ No newline at end of file +module.exports = DeviceInfo diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js index ba031b665..ace0256e8 100644 --- a/server/objects/PlaybackSession.js +++ b/server/objects/PlaybackSession.js @@ -110,7 +110,8 @@ class PlaybackSession { startedAt: this.startedAt, updatedAt: this.updatedAt, audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }), - libraryItem: libraryItem?.toOldJSONExpanded() || null + libraryItem: libraryItem?.toOldJSONExpanded() || null, + coverAspectRatio: this.coverAspectRatio !== null ? this.coverAspectRatio : undefined // Used for share sessions } } diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 5aa013e8e..70361463f 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -73,7 +73,7 @@ class Stream extends EventEmitter { return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF] } get codecsToForceAAC() { - return ['alac', 'ac3', 'eac3'] + return ['alac', 'ac3', 'eac3', 'opus'] } get userToken() { return this.user.token diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index a03e17c75..99f5b76aa 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -3,6 +3,7 @@ const packageJson = require('../../../package.json') const { BookshelfView } = require('../../utils/constants') const Logger = require('../../Logger') const User = require('../../models/User') +const { sanitize } = require('../../utils/htmlSanitizer') class ServerSettings { constructor(settings) { @@ -126,7 +127,7 @@ class ServerSettings { this.version = settings.version || null this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 - this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0 + this.authLoginCustomMessage = sanitize(settings.authLoginCustomMessage) || null // Added v2.8.0 this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null @@ -309,7 +310,7 @@ class ServerSettings { get authFormData() { const clientFormData = { - authLoginCustomMessage: this.authLoginCustomMessage + authLoginCustomMessage: sanitize(this.authLoginCustomMessage) } if (this.authActiveAuthMethods.includes('openid')) { clientFormData.authOpenIDButtonText = this.authOpenIDButtonText @@ -327,6 +328,9 @@ class ServerSettings { update(payload) { let hasUpdates = false for (const key in payload) { + if (key === 'authLoginCustomMessage') { + payload[key] = sanitize(payload[key]) + } if (key === 'sortingPrefixes') { // Sorting prefixes are updated with the /api/sorting-prefixes endpoint continue diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index db04bf5ec..e89b364f3 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -363,8 +363,9 @@ class ApiRouter { * Remove library item and associated entities * @param {string} libraryItemId * @param {string[]} mediaItemIds array of bookId or podcastEpisodeId + * @param {string} libraryId */ - async handleDeleteLibraryItem(libraryItemId, mediaItemIds) { + async handleDeleteLibraryItem(libraryItemId, mediaItemIds, libraryId) { const numProgressRemoved = await Database.mediaProgressModel.destroy({ where: { mediaItemId: mediaItemIds @@ -395,7 +396,8 @@ class ApiRouter { await Database.libraryItemModel.removeById(libraryItemId) SocketAuthority.emitter('item_removed', { - id: libraryItemId + id: libraryItemId, + libraryId }) } diff --git a/server/utils/constants.js b/server/utils/constants.js index cc5217f41..925035e17 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -48,6 +48,8 @@ module.exports.AudioMimeType = { AIF: 'audio/x-aiff', WEBM: 'audio/webm', WEBMA: 'audio/webm', + // TODO: Switch to `audio/matroska`? marked as deprecated in IANA registry + // ref: https://datatracker.ietf.org/doc/html/rfc9559 MKA: 'audio/x-matroska', AWB: 'audio/amr-wb', CAF: 'audio/x-caf', diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 80832cc77..7ad2a3aee 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -1,4 +1,5 @@ const axios = require('axios') +const ssrfFilter = require('ssrf-req-filter') const Ffmpeg = require('../libs/fluentFfmpeg') const ffmpgegUtils = require('../libs/fluentFfmpeg/utils') const fs = require('../libs/fsExtra') @@ -97,6 +98,8 @@ async function resizeImage(filePath, outputPath, width, height) { module.exports.resizeImage = resizeImage /** + * Download podcast episode + * Uses SSRF filter to prevent internal URLs * * @param {import('../objects/PodcastEpisodeDownload')} podcastEpisodeDownload * @returns {Promise<{success: boolean, isRequestError?: boolean}>} @@ -121,7 +124,9 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { Accept: '*/*', 'User-Agent': userAgent }, - timeout: global.PodcastDownloadTimeout + timeout: global.PodcastDownloadTimeout, + httpAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : ssrfFilter(podcastEpisodeDownload.url), + httpsAgent: global.DisableSsrfRequestFilter?.(podcastEpisodeDownload.url) ? null : ssrfFilter(podcastEpisodeDownload.url) }) Logger.debug(`[ffmpegHelpers] Successfully connected with User-Agent: ${userAgent}`) diff --git a/server/utils/htmlSanitizer.js b/server/utils/htmlSanitizer.js index 4ed30e727..be839b7c2 100644 --- a/server/utils/htmlSanitizer.js +++ b/server/utils/htmlSanitizer.js @@ -5,11 +5,10 @@ const { entities } = require('./htmlEntities') * * @param {string} html * @returns {string} - * @throws {Error} if input is not a string */ function sanitize(html) { if (typeof html !== 'string') { - throw new Error('sanitizeHtml: input must be a string') + return '' } const sanitizerOptions = { @@ -27,6 +26,8 @@ function sanitize(html) { module.exports.sanitize = sanitize function stripAllTags(html, shouldDecodeEntities = true) { + if (typeof html !== 'string') return '' + const sanitizerOptions = { allowedTags: [], disallowedTagsMode: 'discard' diff --git a/server/utils/index.js b/server/utils/index.js index c7700a783..49a7c8e67 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -54,6 +54,16 @@ module.exports.isNullOrNaN = (num) => { return num === null || isNaN(num) } +/** + * @param {number|null|undefined} value + * @param {number} max + * @returns {number|null} + */ +module.exports.clampPositiveInt = (value, max) => { + if (value == null || !Number.isFinite(value) || value <= 0) return null + return Math.min(Math.floor(value), max) +} + const xmlToJSON = (xml) => { return new Promise((resolve, reject) => { parseString(xml, (err, results) => { diff --git a/server/utils/parsers/parseNfoMetadata.js b/server/utils/parsers/parseNfoMetadata.js index 6682a0078..4276443d0 100644 --- a/server/utils/parsers/parseNfoMetadata.js +++ b/server/utils/parsers/parseNfoMetadata.js @@ -22,7 +22,7 @@ function parseNfoMetadata(nfoText) { switch (key) { case 'title': { - const titleMatch = value.match(/^(.*?):(.*)$/) + const titleMatch = value.match(/^(.*?): (.*)$/) if (titleMatch) { metadata.title = titleMatch[1].trim() metadata.subtitle = titleMatch[2].trim() diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 2042a8e39..1cb0c4cb4 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -217,6 +217,10 @@ function extractEpisodeData(item) { episode[cleanKey] = extractFirstArrayItemString(item, key) }) + if (episode.subtitle) { + episode.subtitle = htmlSanitizer.sanitize(episode.subtitle.trim()) + } + // Extract psc:chapters if duration is set episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 7ae1dc866..fbe0c4f0d 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -888,28 +888,79 @@ module.exports = { }) } - // Step 2: Get books not started and not in a series OR is the first book of a series not started (ordered randomly) - const { rows: books, count } = await Database.bookModel.findAndCountAll({ - where: [ - { - '$mediaProgresses.isFinished$': { - [Sequelize.Op.or]: [null, 0] - }, - '$mediaProgresses.currentTime$': { - [Sequelize.Op.or]: [null, 0] - }, - [Sequelize.Op.or]: [ - Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0), - { - id: { - [Sequelize.Op.in]: booksFromSeriesToInclude - } - } - ] + const discoverWhere = [ + { + '$mediaProgresses.isFinished$': { + [Sequelize.Op.or]: [null, 0] }, - ...userPermissionBookWhere.bookWhere - ], + '$mediaProgresses.currentTime$': { + [Sequelize.Op.or]: [null, 0] + }, + [Sequelize.Op.or]: [ + Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0), + { + id: { + [Sequelize.Op.in]: booksFromSeriesToInclude + } + } + ] + }, + ...userPermissionBookWhere.bookWhere + ] + + const baseDiscoverInclude = [ + { + model: Database.libraryItemModel, + where: { + libraryId + } + }, + { + model: Database.mediaProgressModel, + where: { + userId: user.id + }, + required: false + } + ] + + // Step 2a: Count with lightweight includes only + const count = await Database.bookModel.count({ + where: discoverWhere, replacements: userPermissionBookWhere.replacements, + include: baseDiscoverInclude, + distinct: true, + col: 'id', + subQuery: false + }) + + // Step 2b: Select random IDs with lightweight includes only + const randomSelection = await Database.bookModel.findAll({ + attributes: ['id'], + where: discoverWhere, + replacements: userPermissionBookWhere.replacements, + include: baseDiscoverInclude, + subQuery: false, + distinct: true, + limit, + order: Database.sequelize.random() + }) + + const selectedIds = randomSelection.map((b) => b.id).filter(Boolean) + if (!selectedIds.length) { + return { + libraryItems: [], + count + } + } + + // Step 2c: Hydrate selected IDs with full metadata for API response + const books = await Database.bookModel.findAll({ + where: { + id: { + [Sequelize.Op.in]: selectedIds + } + }, include: [ { model: Database.libraryItemModel, @@ -918,13 +969,6 @@ module.exports = { }, include: libraryItemIncludes }, - { - model: Database.mediaProgressModel, - where: { - userId: user.id - }, - required: false - }, { model: Database.bookAuthorModel, attributes: ['authorId'], @@ -942,14 +986,14 @@ module.exports = { separate: true } ], - subQuery: false, - distinct: true, - limit, - order: Database.sequelize.random() + subQuery: false }) + const booksById = new Map(books.map((b) => [b.id, b])) + const orderedBooks = selectedIds.map((id) => booksById.get(id)).filter(Boolean) + // Step 3: Map books to library items - const libraryItems = books.map((bookExpanded) => { + const libraryItems = orderedBooks.map((bookExpanded) => { const libraryItem = bookExpanded.libraryItem const book = bookExpanded delete book.libraryItem diff --git a/test/server/controllers/LibraryItemController.test.js b/test/server/controllers/LibraryItemController.test.js index 5a0422392..1d57219d3 100644 --- a/test/server/controllers/LibraryItemController.test.js +++ b/test/server/controllers/LibraryItemController.test.js @@ -123,7 +123,9 @@ describe('LibraryItemController', () => { const fakeReq = { query: {}, user: { - canDelete: true + username: 'test', + canDelete: true, + checkCanAccessLibraryItem: () => true }, body: { libraryItemIds: [libraryItem1Id] @@ -199,4 +201,102 @@ describe('LibraryItemController', () => { expect(series2Exists).to.be.true }) }) + + describe('batch item access control', () => { + let lib1Id + let itemLib1Id + let itemLib2Id + + beforeEach(async () => { + const lib1 = await Database.libraryModel.create({ name: 'Lib 1', mediaType: 'book' }) + const folder1 = await Database.libraryFolderModel.create({ path: '/l1', libraryId: lib1.id }) + const book1 = await Database.bookModel.create({ title: 'B1', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const li1 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book1.id, + mediaType: 'book', + libraryId: lib1.id, + libraryFolderId: folder1.id + }) + lib1Id = lib1.id + itemLib1Id = li1.id + + const lib2 = await Database.libraryModel.create({ name: 'Lib 2', mediaType: 'book' }) + const folder2 = await Database.libraryFolderModel.create({ path: '/l2', libraryId: lib2.id }) + const book2 = await Database.bookModel.create({ title: 'B2', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const li2 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book2.id, + mediaType: 'book', + libraryId: lib2.id, + libraryFolderId: folder2.id + }) + itemLib2Id = li2.id + }) + + const userLimitedToLib1 = () => ({ + username: 'limited', + canDelete: true, + canUpdate: true, + checkCanAccessLibraryItem(li) { + return li.libraryId === lib1Id + } + }) + + it('batchGet returns 403 for a library item the user cannot access', async () => { + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + body: { libraryItemIds: [itemLib2Id] }, + user: userLimitedToLib1() + } + await LibraryItemController.batchGet.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + + it('batchGet returns items when the user can access them', async () => { + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + body: { libraryItemIds: [itemLib1Id] }, + user: userLimitedToLib1() + } + await LibraryItemController.batchGet.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.json.calledOnce).to.be.true + const payload = fakeRes.json.firstCall.args[0] + expect(payload.libraryItems).to.have.length(1) + expect(payload.libraryItems[0].id).to.equal(itemLib1Id) + }) + + it('batchUpdate returns 403 for a library item the user cannot access', async () => { + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + user: userLimitedToLib1(), + body: [{ id: itemLib2Id, mediaPayload: {} }] + } + await LibraryItemController.batchUpdate.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + + it('batchUpdate returns 403 when the user lacks canUpdate', async () => { + const u = userLimitedToLib1() + u.canUpdate = false + const fakeRes = { sendStatus: sinon.spy(), json: sinon.spy() } + const fakeReq = { + user: u, + body: [{ id: itemLib1Id, mediaPayload: {} }] + } + await LibraryItemController.batchUpdate.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + + it('batchDelete returns 403 for a library item the user cannot access', async () => { + const fakeRes = { sendStatus: sinon.spy() } + const fakeReq = { + query: {}, + user: userLimitedToLib1(), + body: { libraryItemIds: [itemLib2Id] } + } + await LibraryItemController.batchDelete.bind(apiRouter)(fakeReq, fakeRes) + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + }) + }) }) diff --git a/test/server/controllers/MeController.test.js b/test/server/controllers/MeController.test.js new file mode 100644 index 000000000..3cc5496d9 --- /dev/null +++ b/test/server/controllers/MeController.test.js @@ -0,0 +1,636 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') +const sinon = require('sinon') + +const Database = require('../../../server/Database') +const ApiRouter = require('../../../server/routers/ApiRouter') +const MeController = require('../../../server/controllers/MeController') +const Auth = require('../../../server/Auth') +const Logger = require('../../../server/Logger') +const SocketAuthority = require('../../../server/SocketAuthority') + +describe('MeController - IDOR Security Tests', () => { + /** @type {ApiRouter} */ + let apiRouter + + beforeEach(async () => { + global.ServerSettings = {} + Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + await Database.buildModels() + + // Create mock server object with required dependencies + const mockServer = { + auth: new Auth(), + playbackSessionManager: { sessions: [] }, + abMergeManager: {}, + backupManager: {}, + podcastManager: {}, + audioMetadataManager: {}, + cronManager: {}, + emailManager: {}, + apiCacheManager: { middleware: (req, res, next) => next() } + } + + apiRouter = new ApiRouter(mockServer) + + sinon.stub(Logger, 'info') + sinon.stub(Logger, 'error') + sinon.stub(SocketAuthority, 'clientEmitter') + }) + + afterEach(async () => { + sinon.restore() + + // Clear all tables + await Database.sequelize.sync({ force: true }) + }) + + describe('removeMediaProgress - IDOR Protection', () => { + let user1, user2 + let mediaProgress1, mediaProgress2 + + beforeEach(async () => { + // Create two users + user1 = await Database.userModel.create({ + username: 'user1', + pash: 'hashed_password_1', + type: 'user', + isActive: true + }) + + user2 = await Database.userModel.create({ + username: 'user2', + pash: 'hashed_password_2', + type: 'user', + isActive: true + }) + + // Create library and book + const library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + const libraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: library.id }) + const book = await Database.bookModel.create({ title: 'Test Book', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const libraryItem = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book.id, + mediaType: 'book', + libraryId: library.id, + libraryFolderId: libraryFolder.id + }) + + // Create media progress for each user + mediaProgress1 = await Database.mediaProgressModel.create({ + userId: user1.id, + mediaItemId: book.id, + mediaItemType: 'book', + duration: 1000, + currentTime: 500, + isFinished: false + }) + + mediaProgress2 = await Database.mediaProgressModel.create({ + userId: user2.id, + mediaItemId: book.id, + mediaItemType: 'book', + duration: 1000, + currentTime: 300, + isFinished: false + }) + + // Load media progresses into users + user1.mediaProgresses = await user1.getMediaProgresses() + user2.mediaProgresses = await user2.getMediaProgresses() + }) + + it('should allow user to delete their own media progress', async () => { + const fakeReq = { + user: user1, + params: { id: mediaProgress1.id } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + await MeController.removeMediaProgress(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(200)).to.be.true + + // Verify media progress was deleted + const deletedProgress = await Database.mediaProgressModel.findByPk(mediaProgress1.id) + expect(deletedProgress).to.be.null + }) + + it('should prevent user from deleting another users media progress (IDOR)', async () => { + const fakeReq = { + user: user1, + params: { id: mediaProgress2.id } // Trying to delete user2's progress + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + await MeController.removeMediaProgress(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + + // Verify media progress was NOT deleted + const existingProgress = await Database.mediaProgressModel.findByPk(mediaProgress2.id) + expect(existingProgress).to.not.be.null + expect(existingProgress.userId).to.equal(user2.id) + }) + + it('should return 404 for non-existent media progress', async () => { + const fakeReq = { + user: user1, + params: { id: 'non-existent-id' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + await MeController.removeMediaProgress(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + }) + }) + + describe('Bookmark Operations - Authorization Checks', () => { + let user1, user2 + let library1, library2 + let libraryItem1, libraryItem2 + + beforeEach(async () => { + // Create two users with different library access + user1 = await Database.userModel.create({ + username: 'user1', + pash: 'hashed_password_1', + type: 'user', + isActive: true, + librariesAccessible: null // Access to all libraries + }) + + user2 = await Database.userModel.create({ + username: 'user2', + pash: 'hashed_password_2', + type: 'user', + isActive: true, + librariesAccessible: [] // Will be set to specific library + }) + + // Create two libraries + library1 = await Database.libraryModel.create({ name: 'Library 1', mediaType: 'book' }) + library2 = await Database.libraryModel.create({ name: 'Library 2', mediaType: 'book' }) + + // User2 only has access to library1 + user2.librariesAccessible = [library1.id] + await user2.save() + + const libraryFolder1 = await Database.libraryFolderModel.create({ path: '/test1', libraryId: library1.id }) + const libraryFolder2 = await Database.libraryFolderModel.create({ path: '/test2', libraryId: library2.id }) + + const book1 = await Database.bookModel.create({ title: 'Book 1', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const book2 = await Database.bookModel.create({ title: 'Book 2', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + + libraryItem1 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book1.id, + mediaType: 'book', + libraryId: library1.id, + libraryFolderId: libraryFolder1.id + }) + + libraryItem2 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book2.id, + mediaType: 'book', + libraryId: library2.id, + libraryFolderId: libraryFolder2.id + }) + + // Initialize bookmarks + user1.bookmarks = [] + user2.bookmarks = [] + }) + + describe('createBookmark', () => { + it('should allow user to create bookmark for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const bookmark = { libraryItemId: libraryItem1.id, time: 100, title: 'Test Bookmark', createdAt: Date.now() } + + const fakeReq = { + user: { + ...user2.toJSON(), + id: user2.id, + username: user2.username, + checkCanAccessLibraryItem: () => true, + createBookmark: sinon.stub().resolves(bookmark), + toOldJSONForBrowser: () => ({ id: user2.id, username: user2.username }) + }, + params: { id: libraryItem1.id }, + body: { time: 100, title: 'Test Bookmark' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.json.calledOnce).to.be.true + expect(fakeRes.json.calledWith(bookmark)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should prevent user from creating bookmark for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { id: libraryItem2.id }, + body: { time: 100, title: 'Test Bookmark' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + // Mock getExpandedById + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + expect(fakeRes.json.called).to.be.false + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should return 404 for non-existent library item', async () => { + const fakeReq = { + user: user1, + params: { id: 'non-existent-id' }, + body: { time: 100, title: 'Test Bookmark' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + // Mock getExpandedById to return null + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(null) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should validate bookmark time parameter', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true + }, + params: { id: libraryItem1.id }, + body: { time: null, title: 'Test Bookmark' } // null time is invalid + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.createBookmark(fakeReq, fakeRes) + + expect(fakeRes.status.calledWith(400)).to.be.true + expect(fakeRes.send.calledWith('Invalid time')).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) + + describe('updateBookmark', () => { + beforeEach(async () => { + // Add existing bookmark to user1 + user1.bookmarks = [{ libraryItemId: libraryItem1.id, time: 100, title: 'Original Title' }] + await user1.save() + }) + + it('should allow user to update bookmark for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const bookmark = { libraryItemId: libraryItem1.id, time: 100, title: 'Updated Title' } + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true, + updateBookmark: sinon.stub().resolves(bookmark), + toOldJSONForBrowser: () => ({ id: user1.id, username: user1.username }) + }, + params: { id: libraryItem1.id }, + body: { time: 100, title: 'Updated Title' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.updateBookmark(fakeReq, fakeRes) + + expect(fakeRes.json.calledOnce).to.be.true + expect(fakeRes.json.calledWith(bookmark)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should prevent user from updating bookmark for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { id: libraryItem2.id }, + body: { time: 100, title: 'Updated Title' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.updateBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) + + describe('removeBookmark', () => { + beforeEach(async () => { + // Add existing bookmark to user1 + user1.bookmarks = [{ libraryItemId: libraryItem1.id, time: 100, title: 'Test Bookmark' }] + await user1.save() + }) + + it('should allow user to remove bookmark for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true, + findBookmark: sinon.stub().returns({ libraryItemId: libraryItem1.id, time: 100, title: 'Test Bookmark' }), + removeBookmark: sinon.stub().resolves(true), + toOldJSONForBrowser: () => ({ id: user1.id, username: user1.username }) + }, + params: { id: libraryItem1.id, time: '100' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.removeBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(200)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should prevent user from removing bookmark for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { id: libraryItem2.id, time: '100' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.removeBookmark(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + + it('should validate time parameter is a number', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true + }, + params: { id: libraryItem1.id, time: 'not-a-number' } + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + + await MeController.removeBookmark(fakeReq, fakeRes) + + expect(fakeRes.status.calledWith(400)).to.be.true + expect(fakeRes.send.calledWith('Invalid time')).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) + }) + + describe('getItemListeningSessions - Authorization Check', () => { + let user1, user2 + let library1, library2 + let libraryItem1, libraryItem2 + + beforeEach(async () => { + // Create two users with different library access + user1 = await Database.userModel.create({ + username: 'user1', + pash: 'hashed_password_1', + type: 'user', + isActive: true, + librariesAccessible: null // Access to all libraries + }) + + user2 = await Database.userModel.create({ + username: 'user2', + pash: 'hashed_password_2', + type: 'user', + isActive: true, + librariesAccessible: [] // Will be set to specific library + }) + + // Create two libraries + library1 = await Database.libraryModel.create({ name: 'Library 1', mediaType: 'book' }) + library2 = await Database.libraryModel.create({ name: 'Library 2', mediaType: 'book' }) + + // User2 only has access to library1 + user2.librariesAccessible = [library1.id] + await user2.save() + + const libraryFolder1 = await Database.libraryFolderModel.create({ path: '/test1', libraryId: library1.id }) + const libraryFolder2 = await Database.libraryFolderModel.create({ path: '/test2', libraryId: library2.id }) + + const book1 = await Database.bookModel.create({ title: 'Book 1', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const book2 = await Database.bookModel.create({ title: 'Book 2', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + + libraryItem1 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book1.id, + mediaType: 'book', + libraryId: library1.id, + libraryFolderId: libraryFolder1.id + }) + + libraryItem2 = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book2.id, + mediaType: 'book', + libraryId: library2.id, + libraryFolderId: libraryFolder2.id + }) + }) + + it('should allow user to view listening sessions for accessible library item', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem1.id) + + // Create mock context with getUserItemListeningSessionsHelper + const mockContext = { + getUserItemListeningSessionsHelper: sinon.stub().resolves([{ id: 'session1', timeListening: 300, startedAt: Date.now() }]) + } + + const fakeReq = { + user: { + ...user1.toJSON(), + id: user1.id, + username: user1.username, + checkCanAccessLibraryItem: () => true + }, + params: { libraryItemId: libraryItem1.id }, + query: {} + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + sinon.stub(Database.podcastEpisodeModel, 'findByPk').resolves(null) + + await MeController.getItemListeningSessions.bind(mockContext)(fakeReq, fakeRes) + + expect(fakeRes.json.calledOnce).to.be.true + expect(fakeRes.sendStatus.called).to.be.false + + // Verify the payload structure + const payload = fakeRes.json.firstCall.args[0] + expect(payload).to.have.property('total') + expect(payload).to.have.property('sessions') + + Database.libraryItemModel.getExpandedById.restore() + Database.podcastEpisodeModel.findByPk.restore() + }) + + it('should prevent user from viewing listening sessions for inaccessible library item (IDOR)', async () => { + const expandedItem = await Database.libraryItemModel.getExpandedById(libraryItem2.id) + + const fakeReq = { + user: user2, // user2 doesn't have access to library2 + params: { libraryItemId: libraryItem2.id }, + query: {} + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(expandedItem) + sinon.stub(Database.podcastEpisodeModel, 'findByPk').resolves(null) + + await MeController.getItemListeningSessions.bind(apiRouter)(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(403)).to.be.true + expect(fakeRes.json.called).to.be.false + + Database.libraryItemModel.getExpandedById.restore() + Database.podcastEpisodeModel.findByPk.restore() + }) + + it('should return 404 for non-existent library item', async () => { + const fakeReq = { + user: user1, + params: { libraryItemId: 'non-existent-id' }, + query: {} + } + const fakeRes = { + sendStatus: sinon.spy(), + status: sinon.stub().returnsThis(), + send: sinon.spy(), + json: sinon.spy() + } + + sinon.stub(Database.libraryItemModel, 'getExpandedById').resolves(null) + + await MeController.getItemListeningSessions.bind(apiRouter)(fakeReq, fakeRes) + + expect(fakeRes.sendStatus.calledWith(404)).to.be.true + + Database.libraryItemModel.getExpandedById.restore() + }) + }) +}) diff --git a/test/server/finders/BookFinder.test.js b/test/server/finders/BookFinder.test.js index 6578ca82a..cb697b81a 100644 --- a/test/server/finders/BookFinder.test.js +++ b/test/server/finders/BookFinder.test.js @@ -35,7 +35,10 @@ describe('TitleCandidates', () => { ['adds candidate, removing author', `anna karenina by ${cleanAuthor}`, ['anna karenina']], ['does not add empty candidate after removing author', cleanAuthor, []], ['adds candidate, removing subtitle', 'anna karenina: subtitle', ['anna karenina']], + ['adds candidate, not stripping subtitle for bare colon in title', '10:04', ['10:04']], + ['adds candidate, not stripping subtitle for colon between words without space', 'making the mission:impossible movies', ['making the mission:impossible movies']], ['adds candidate + variant, removing "by ..."', 'anna karenina by arnold schwarzenegger', ['anna karenina', 'anna karenina by arnold schwarzenegger']], + ['adds candidate + variant, removing "by ..." when title has bare colon', '10:04 by ben lerner', ['10:04', '10:04 by ben lerner']], ['adds candidate + variant, removing bitrate', 'anna karenina 64kbps', ['anna karenina', 'anna karenina 64kbps']], ['adds candidate + variant, removing edition 1', 'anna karenina 2nd edition', ['anna karenina', 'anna karenina 2nd edition']], ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], diff --git a/test/server/managers/ApiCacheManager.test.js b/test/server/managers/ApiCacheManager.test.js index 19bbeecf6..2e9ad5b77 100644 --- a/test/server/managers/ApiCacheManager.test.js +++ b/test/server/managers/ApiCacheManager.test.js @@ -1,6 +1,7 @@ // Import dependencies and modules for testing const { expect } = require('chai') const sinon = require('sinon') +const { LRUCache } = require('lru-cache') const ApiCacheManager = require('../../../server/managers/ApiCacheManager') describe('ApiCacheManager', () => { @@ -94,4 +95,17 @@ describe('ApiCacheManager', () => { expect(res.originalSend.calledWith(body)).to.be.true }) }) + + describe('clear on mediaProgress', () => { + it('should remove recent-episodes cache entries', () => { + const key = JSON.stringify({ user: 'u', url: '/libraries/abc-123/recent-episodes?limit=50&page=0' }) + const cache = new LRUCache({ max: 10 }) + cache.set(key, { body: '[]', headers: {}, statusCode: 200 }) + const manager = new ApiCacheManager(cache) + + manager.clear({ name: 'mediaProgress' }, 'afterUpdate') + + expect(cache.get(key)).to.be.undefined + }) + }) }) diff --git a/test/server/utils/parsers/parseNfoMetadata.test.js b/test/server/utils/parsers/parseNfoMetadata.test.js index 9ff51fbe5..ba11ddff2 100644 --- a/test/server/utils/parsers/parseNfoMetadata.test.js +++ b/test/server/utils/parsers/parseNfoMetadata.test.js @@ -21,6 +21,20 @@ describe('parseNfoMetadata', () => { expect(result.subtitle).to.equal('A Novel') }) + it('does not split title on bare colon without space', () => { + const nfoText = 'Title: 10:04' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('10:04') + expect(result.subtitle).to.be.undefined + }) + + it('does not split title on colon between words without space', () => { + const nfoText = 'Title: Making the Mission:Impossible Movies' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('Making the Mission:Impossible Movies') + expect(result.subtitle).to.be.undefined + }) + it('parses authors', () => { const nfoText = 'Author: F. Scott Fitzgerald' const result = parseNfoMetadata(nfoText)