diff --git a/booklore-ui/src/app/core/security/oidc-callback/oidc-callback.component.ts b/booklore-ui/src/app/core/security/oidc-callback/oidc-callback.component.ts index b4fdb5632..b37a36dad 100644 --- a/booklore-ui/src/app/core/security/oidc-callback/oidc-callback.component.ts +++ b/booklore-ui/src/app/core/security/oidc-callback/oidc-callback.component.ts @@ -1,8 +1,8 @@ import {Component, inject, OnInit} from '@angular/core'; import {Router} from '@angular/router'; import {OAuthService} from 'angular-oauth2-oidc'; -import {AuthService} from '../../../shared/service/auth.service'; import {MessageService} from 'primeng/api'; +import {TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-oidc-callback', @@ -13,6 +13,7 @@ export class OidcCallbackComponent implements OnInit { private router = inject(Router); private oauthService = inject(OAuthService); private messageService = inject(MessageService); + private readonly t = inject(TranslocoService); async ngOnInit(): Promise { try { @@ -26,8 +27,8 @@ export class OidcCallbackComponent implements OnInit { console.error('[OIDC Callback] Login failed', e); this.messageService.add({ severity: 'error', - summary: 'OIDC Login Failed', - detail: 'Redirecting to local login...', + summary: this.t.translate('auth.oidc.loginFailedSummary'), + detail: this.t.translate('auth.oidc.redirectingDetail'), life: 3000 }); setTimeout(() => { diff --git a/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.html b/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.html index 7f0fc2f50..f460f46d9 100644 --- a/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.html +++ b/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.html @@ -1,11 +1,12 @@ +
-

Add Physical Book

-

Catalog a physical book without a digital file

+

{{ t('title') }}

+

{{ t('description') }}

@@ -22,7 +23,7 @@
@@ -41,7 +42,7 @@
@@ -58,7 +59,7 @@
+ [placeholder]="t('isbnPlaceholder')"/>
@@ -77,7 +78,7 @@
@@ -102,14 +103,14 @@
@@ -119,7 +120,7 @@
+ [placeholder]="t('publisherPlaceholder')"/>
+ [placeholder]="t('publishedDatePlaceholder')"/>
@@ -149,7 +150,7 @@
+ [placeholder]="t('languagePlaceholder')"/>
@@ -180,7 +181,7 @@
@@ -206,28 +207,28 @@ @if (!selectedLibraryId) {
- Library is required + {{ t('validationLibraryRequired') }}
} @else if (!title.trim() && !isbn.trim()) {
- Title or ISBN is required + {{ t('validationTitleOrIsbn') }}
} @else {
- Ready to create + {{ t('validationReady') }}
}
+ diff --git a/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.ts b/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.ts index 59642a0f7..ca447babc 100644 --- a/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.ts +++ b/booklore-ui/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.ts @@ -14,6 +14,7 @@ import {Library} from '../../model/library.model'; import {CreatePhysicalBookRequest} from '../../model/book.model'; import {filter, take} from 'rxjs/operators'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {TranslocoDirective} from '@jsverse/transloco'; @Component({ selector: 'app-add-physical-book-dialog', @@ -27,7 +28,8 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; Select, Textarea, InputNumber, - AutoComplete + AutoComplete, + TranslocoDirective ], styleUrl: './add-physical-book-dialog.component.scss', }) diff --git a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.html b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.html index 26223eaa8..033a20ed9 100644 --- a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.html +++ b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.html @@ -1,11 +1,12 @@ +
-

Upload Additional File

-

{{ book.metadata?.title || 'Unknown Title' }}

+

{{ t('title') }}

+

{{ book.metadata?.title || t('unknownTitle') }}

- + @@ -98,7 +99,7 @@ } @@ -106,7 +107,7 @@ } @@ -114,7 +115,7 @@ } @@ -131,12 +132,11 @@
-

Drag and drop a file here to upload.

-

- Upload an additional file for {{ book.metadata?.title || 'this book' }}. -

+

{{ t('dragDropText') }}

+

+ diff --git a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts index bce6ce13d..0556cbf60 100644 --- a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts +++ b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectorRef, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { DynamicDialogRef, DynamicDialogConfig } from 'primeng/dynamicdialog'; @@ -13,6 +13,7 @@ import { AppSettingsService } from '../../../../shared/service/app-settings.serv import { Book, AdditionalFileType } from '../../model/book.model'; import { MessageService } from 'primeng/api'; import { filter, take } from 'rxjs/operators'; +import { TranslocoDirective, TranslocoService } from '@jsverse/transloco'; interface FileTypeOption { label: string; @@ -34,12 +35,15 @@ interface UploadingFile { Button, FileUpload, Badge, - Tooltip + Tooltip, + TranslocoDirective ], templateUrl: './additional-file-uploader.component.html', styleUrls: ['./additional-file-uploader.component.scss'] }) export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { + private readonly t = inject(TranslocoService); + book!: Book; files: UploadingFile[] = []; fileType: AdditionalFileType = AdditionalFileType.ALTERNATIVE_FORMAT; @@ -47,10 +51,7 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { isUploading = false; maxFileSizeBytes?: number; - fileTypeOptions: FileTypeOption[] = [ - { label: 'Alternative Format', value: AdditionalFileType.ALTERNATIVE_FORMAT }, - { label: 'Supplementary File', value: AdditionalFileType.SUPPLEMENTARY } - ]; + fileTypeOptions: FileTypeOption[] = []; private destroy$ = new Subject(); @@ -65,6 +66,10 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { ngOnInit(): void { this.book = this.config.data.book; + this.fileTypeOptions = [ + { label: this.t.translate('book.fileUploader.typeAlternativeFormat'), value: AdditionalFileType.ALTERNATIVE_FORMAT }, + { label: this.t.translate('book.fileUploader.typeSupplementary'), value: AdditionalFileType.SUPPLEMENTARY } + ]; this.appSettingsService.appSettings$ .pipe( filter(settings => settings != null), @@ -106,7 +111,8 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { const file = newFiles[0]; if (this.maxFileSizeBytes && file.size > this.maxFileSizeBytes) { - const errorMsg = `File exceeds maximum size of ${this.formatSize(this.maxFileSizeBytes)}`; + const maxSize = this.formatSize(this.maxFileSizeBytes); + const errorMsg = this.t.translate('book.fileUploader.toast.fileTooLargeError', { maxSize }); this.files = [{ file, status: 'Failed', @@ -114,8 +120,8 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { }]; this.messageService.add({ severity: 'error', - summary: 'File Too Large', - detail: `${file.name} exceeds the maximum file size of ${this.formatSize(this.maxFileSizeBytes)}`, + summary: this.t.translate('book.fileUploader.toast.fileTooLargeSummary'), + detail: this.t.translate('book.fileUploader.toast.fileTooLargeDetail', { fileName: file.name, maxSize }), life: 5000 }); } else { @@ -161,7 +167,7 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { }, error: (err) => { uploadFile.status = 'Failed'; - uploadFile.errorMessage = err?.error?.message || 'Upload failed due to unknown error.'; + uploadFile.errorMessage = err?.error?.message || this.t.translate('book.fileUploader.toast.uploadFailedUnknown'); console.error('Upload failed for', uploadFile.file.name, err); if (--pending === 0) { this.isUploading = false; @@ -203,18 +209,18 @@ export class AdditionalFileUploaderComponent implements OnInit, OnDestroy { } getFileStatusLabel(uploadFile: UploadingFile): string { - if (uploadFile.status === 'Failed' && uploadFile.errorMessage?.includes('exceeds maximum size')) { - return 'Too Large'; + if (uploadFile.status === 'Failed' && uploadFile.errorMessage?.includes('maximum size')) { + return this.t.translate('book.fileUploader.statusTooLarge'); } switch (uploadFile.status) { case 'Pending': - return 'Ready'; + return this.t.translate('book.fileUploader.statusReady'); case 'Uploading': - return 'Uploading'; + return this.t.translate('book.fileUploader.statusUploading'); case 'Uploaded': - return 'Uploaded'; + return this.t.translate('book.fileUploader.statusUploaded'); case 'Failed': - return 'Failed'; + return this.t.translate('book.fileUploader.statusFailed'); default: return uploadFile.status; } diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.html b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.html index 9bcd4d3f3..889cf997f 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.html @@ -1,3 +1,4 @@ +
@@ -12,17 +13,17 @@ } @else if (isFilterActive || hasSearchTerm) { {{ computedFilterLabel }} } @else { - All Books + {{ t('labels.allBooks') }} } } @else if (entityType === EntityType.UNSHELVED) { - {{ (isFilterActive || hasSearchTerm) ? 'Unshelved Books (Filtered)' : 'Unshelved Books' }} + {{ (isFilterActive || hasSearchTerm) ? t('labels.unshelvedBooksFiltered') : t('labels.unshelvedBooks') }} } @else { - {{ entityType }}: {{ (entity$ | async)?.name }}{{ (isFilterActive || hasSearchTerm) ? ' (Filtered)' : '' }} + {{ entityType }}: {{ (entity$ | async)?.name }}{{ (isFilterActive || hasSearchTerm) ? ' ' + t('labels.filteredSuffix') : '' }} }

@if (seriesCollapseFilter.isSeriesCollapsed && (bookState$ | async)?.books; as books) {

- Showing {{ books.length }} {{ books.length === 1 ? 'item' : 'items' }} (series collapsed) + {{ t('labels.seriesCollapsedInfo', { count: books.length, itemWord: books.length === 1 ? t('labels.item') : t('labels.items') }) }}

}
@@ -50,7 +51,7 @@ @@ -61,7 +62,7 @@ @@ -73,16 +74,16 @@ [(ngModel)]="visibleColumns" (ngModelChange)="onVisibleColumnsChange($event)" display="chip" - placeholder="Select Columns" + [placeholder]="t('placeholder.selectColumns')" [style]="{ width: '100%' }" [filter]="true" [showClear]="true" >
@if (isMobile) {
- +
@@ -167,7 +168,7 @@ @if (sortCriteriaCount > 1) { @@ -188,7 +189,7 @@ @@ -200,7 +201,7 @@ type="button" class="topbar-items topbar-item" (click)="searchDropdown.toggle($event)" - pTooltip="Search" + [pTooltip]="t('tooltip.search')" tooltipPosition="top" > @@ -211,7 +212,7 @@ @@ -271,14 +272,14 @@ @if (bookState?.error) {

- {{ entityType === EntityType.LIBRARY ? "Failed to load library's books!" : "Failed to load shelf's books!" }} + {{ entityType === EntityType.LIBRARY ? t('labels.failedLibrary') : t('labels.failedShelf') }}

} @if (!bookState?.error && bookState?.loaded && bookState?.books?.length === 0) {

- This collection has no books! + {{ t('labels.noBooks') }}

} @@ -326,7 +327,7 @@
{{ selectedCount }} - selected + {{ t('labels.selected') }}
+
diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts index a6c79fc29..29c8a10e5 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts @@ -57,6 +57,7 @@ import {BookBrowserScrollService} from './book-browser-scroll.service'; import {AppSettingsService} from '../../../../shared/service/app-settings.service'; import {MultiSortPopoverComponent} from './sorting/multi-sort-popover/multi-sort-popover.component'; import {SortService} from '../../service/sort.service'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; export enum EntityType { LIBRARY = 'Library', @@ -74,7 +75,7 @@ export enum EntityType { imports: [ Button, VirtualScrollerModule, BookCardComponent, AsyncPipe, ProgressSpinner, Menu, InputText, FormsModule, BookTableComponent, BookFilterComponent, Tooltip, NgClass, NgStyle, Popover, - Checkbox, Slider, Divider, MultiSelect, TieredMenu, BadgeModule, MultiSortPopoverComponent + Checkbox, Slider, Divider, MultiSelect, TieredMenu, BadgeModule, MultiSortPopoverComponent, TranslocoDirective ], providers: [SeriesCollapseFilter], animations: [ @@ -119,6 +120,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { private filterOrchestrationService = inject(BookFilterOrchestrationService); private localStorageService = inject(LocalStorageService); private scrollService = inject(BookBrowserScrollService); + private readonly t = inject(TranslocoService); bookState$: Observable | undefined; entity$: Observable | undefined; @@ -236,7 +238,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { const filters = this.selectedFilter.value; if (!filters || Object.keys(filters).length === 0) { - return 'All Books'; + return this.t.translate('book.browser.labels.allBooks'); } const filterEntries = Object.entries(filters); @@ -258,7 +260,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { .join(', '); return filterSummary.length > 50 - ? `${filterEntries.length} Active Filters` + ? this.t.translate('book.browser.labels.activeFilters', {count: filterEntries.length}) : filterSummary; } @@ -332,7 +334,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { this.entityType$ = of(entityType); this.entity$ = of(null); this.seriesCollapseFilter.setContext(null, null); - this.pageTitle.setPageTitle(currentPath === 'all-books' ? 'All Books' : 'Unshelved Books'); + this.pageTitle.setPageTitle(currentPath === 'all-books' ? this.t.translate('book.browser.labels.allBooks') : this.t.translate('book.browser.labels.unshelvedBooks')); } else { const routeEntityInfo$ = this.entityService.getEntityInfoFromRoute(this.activatedRoute); this.entityType$ = routeEntityInfo$.pipe(map(info => { @@ -415,7 +417,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { }); - this.currentFilterLabel = 'All Books'; + this.currentFilterLabel = this.t.translate('book.browser.labels.allBooks'); const filterParams = queryParamMap.get('filter'); if (filterParams) { @@ -494,7 +496,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { this.rawFilterParamFromUrl = null; const hasSidebarFilters = !!filters && Object.keys(filters).length > 0; - this.currentFilterLabel = hasSidebarFilters ? this.computedFilterLabel : 'All Books'; + this.currentFilterLabel = hasSidebarFilters ? this.computedFilterLabel : this.t.translate('book.browser.labels.allBooks'); this.queryParamsService.updateFilters(filters); } @@ -558,18 +560,18 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { confirmDeleteBooks(): void { this.confirmationService.confirm({ - message: `Are you sure you want to delete ${this.selectedBooks.size} book(s)?\n\nThis will permanently remove the book files from your filesystem.\n\nThis action cannot be undone.`, - header: 'Confirm Deletion', + message: this.t.translate('book.browser.confirm.deleteMessage', {count: this.selectedBooks.size}), + header: this.t.translate('book.browser.confirm.deleteHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', - acceptLabel: 'Delete', - rejectLabel: 'Cancel', + acceptLabel: this.t.translate('common.delete'), + rejectLabel: this.t.translate('common.cancel'), acceptButtonStyleClass: 'p-button-danger', rejectButtonStyleClass: 'p-button-outlined', accept: () => { const count = this.selectedBooks.size; - const loader = this.loadingService.show(`Deleting ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.browser.loading.deleting', {count})); this.bookService.deleteBooks(this.selectedBooks) .pipe(finalize(() => this.loadingService.hide(loader))) @@ -735,10 +737,10 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { this.userService.updateUserSetting(user.id, 'entityViewPreferences', prefs); this.messageService.add({ severity: 'success', - summary: 'Sort Saved', + summary: this.t.translate('book.browser.toast.sortSavedSummary'), detail: this.entityType === EntityType.ALL_BOOKS || this.entityType === EntityType.UNSHELVED - ? 'Default sort configuration saved.' - : `Sort configuration saved for this ${this.entityType.toLowerCase()}.` + ? this.t.translate('book.browser.toast.sortSavedGlobalDetail') + : this.t.translate('book.browser.toast.sortSavedEntityDetail', {entityType: this.entityType.toLowerCase()}) }); } @@ -775,17 +777,17 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { unshelfBooks(): void { if (!this.entity) return; const count = this.selectedBooks.size; - const loader = this.loadingService.show(`Unshelving ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.browser.loading.unshelving', {count})); this.bookService.updateBookShelves(this.selectedBooks, new Set(), new Set([this.entity.id!])) .pipe(finalize(() => this.loadingService.hide(loader))) .subscribe({ next: () => { - this.messageService.add({severity: 'info', summary: 'Success', detail: 'Books shelves updated'}); + this.messageService.add({severity: 'info', summary: this.t.translate('common.success'), detail: this.t.translate('book.browser.toast.unshelveSuccessDetail')}); this.bookSelectionService.deselectAll(); }, error: () => { - this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to update books shelves'}); + this.messageService.add({severity: 'error', summary: this.t.translate('common.error'), detail: this.t.translate('book.browser.toast.unshelveFailedDetail')}); } }); } @@ -829,17 +831,17 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { if (!this.selectedBooks || this.selectedBooks.size === 0) return; const count = this.selectedBooks.size; this.confirmationService.confirm({ - message: `Are you sure you want to regenerate covers for ${count} book(s)?`, - header: 'Confirm Cover Regeneration', + message: this.t.translate('book.browser.confirm.regenCoverMessage', {count}), + header: this.t.translate('book.browser.confirm.regenCoverHeader'), icon: 'pi pi-image', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'success' }, rejectButtonProps: { - label: 'No', + label: this.t.translate('common.no'), severity: 'secondary' }, accept: () => { @@ -847,16 +849,16 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { next: () => { this.messageService.add({ severity: 'success', - summary: 'Cover Regeneration Started', - detail: `Regenerating covers for ${count} book(s). Refresh the page when complete.`, + summary: this.t.translate('book.browser.toast.regenCoverStartedSummary'), + detail: this.t.translate('book.browser.toast.regenCoverStartedDetail', {count}), life: 3000 }); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Could not start cover regeneration.', + summary: this.t.translate('book.browser.toast.failedSummary'), + detail: this.t.translate('book.browser.toast.regenCoverFailedDetail'), life: 3000 }); } @@ -869,17 +871,17 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { if (!this.selectedBooks || this.selectedBooks.size === 0) return; const count = this.selectedBooks.size; this.confirmationService.confirm({ - message: `Are you sure you want to generate custom covers for ${count} book(s)?`, - header: 'Confirm Custom Cover Generation', + message: this.t.translate('book.browser.confirm.customCoverMessage', {count}), + header: this.t.translate('book.browser.confirm.customCoverHeader'), icon: 'pi pi-palette', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'success' }, rejectButtonProps: { - label: 'No', + label: this.t.translate('common.no'), severity: 'secondary' }, accept: () => { @@ -887,16 +889,16 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { next: () => { this.messageService.add({ severity: 'success', - summary: 'Custom Cover Generation Started', - detail: `Generating custom covers for ${count} book(s).`, + summary: this.t.translate('book.browser.toast.customCoverStartedSummary'), + detail: this.t.translate('book.browser.toast.customCoverStartedDetail', {count}), life: 3000 }); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Could not start custom cover generation.', + summary: this.t.translate('book.browser.toast.failedSummary'), + detail: this.t.translate('book.browser.toast.customCoverFailedDetail'), life: 3000 }); } @@ -920,8 +922,8 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { if (sourceBooks.length === 0) { this.messageService.add({ severity: 'warn', - summary: 'No Eligible Books', - detail: 'Selected books must be single-file books (no alternative formats).' + summary: this.t.translate('book.browser.toast.noEligibleBooksSummary'), + detail: this.t.translate('book.browser.toast.noEligibleBooksDetail') }); return; } @@ -931,8 +933,8 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy { if (libraryIds.size > 1) { this.messageService.add({ severity: 'warn', - summary: 'Multiple Libraries', - detail: 'All selected books must be from the same library.' + summary: this.t.translate('book.browser.toast.multipleLibrariesSummary'), + detail: this.t.translate('book.browser.toast.multipleLibrariesDetail') }); return; } diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html index 32ad94c73..0472077b8 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html @@ -8,7 +8,7 @@ class="book-cover" [class.loaded]="isImageLoaded" [class.square-cover]="_isAudiobook" - [alt]="'Cover of ' + displayTitle" + [alt]="('book.card.alt.cover' | transloco: { title: displayTitle })" loading="lazy" decoding="async" (load)="onImageLoad()"/> diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts index cc2d3c00a..e86233460 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts @@ -26,12 +26,13 @@ import {TaskHelperService} from '../../../../settings/task-management/task-helpe import {BookNavigationService} from '../../../service/book-navigation.service'; import {BookCardOverlayPreferenceService} from '../book-card-overlay-preference.service'; import {AppSettingsService} from '../../../../../shared/service/app-settings.service'; +import {TranslocoPipe, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-book-card', templateUrl: './book-card.component.html', styleUrls: ['./book-card.component.scss'], - imports: [Button, MenuModule, CheckboxModule, FormsModule, NgClass, TieredMenu, ProgressBar, TooltipModule, RouterLink], + imports: [Button, MenuModule, CheckboxModule, FormsModule, NgClass, TieredMenu, ProgressBar, TooltipModule, RouterLink, TranslocoPipe], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush }) @@ -70,6 +71,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { private bookNavigationService = inject(BookNavigationService); private cdr = inject(ChangeDetectorRef); private appSettingsService = inject(AppSettingsService); + private readonly t = inject(TranslocoService); protected _progressPercentage: number | null = null; protected _koProgressPercentage: number | null = null; @@ -140,7 +142,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { if (changes['seriesViewEnabled'] || changes['isSeriesCollapsed']) { this._isSeriesViewActive = this.seriesViewEnabled && !!this.book.seriesCount && this.book.seriesCount >= 1; this._displayTitle = (this.isSeriesCollapsed && this.book.metadata?.seriesName) ? this.book.metadata?.seriesName : this.book.metadata?.title; - this._titleTooltip = 'Title: ' + this._displayTitle; + this._titleTooltip = this.t.translate('book.card.alt.titleTooltip', { title: this._displayTitle }); } } @@ -169,8 +171,8 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { this._readStatusTooltip = this.readStatusHelper.getReadStatusTooltip(this.book.readStatus); this._shouldShowStatusIcon = this.readStatusHelper.shouldShowStatusIcon(this.book.readStatus); - this._seriesCountTooltip = 'Series collapsed: ' + this.book.seriesCount + ' books'; - this._titleTooltip = 'Title: ' + this._displayTitle; + this._seriesCountTooltip = this.t.translate('book.card.alt.seriesCollapsed', { count: this.book.seriesCount }); + this._titleTooltip = this.t.translate('book.card.alt.titleTooltip', { title: this._displayTitle }); } get hasProgress(): boolean { @@ -273,12 +275,12 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { private initMenu() { this.items = [ { - label: 'Assign Shelf', + label: this.t.translate('book.card.menu.assignShelf'), icon: 'pi pi-folder', command: () => this.openShelfDialog() }, { - label: 'View Details', + label: this.t.translate('book.card.menu.viewDetails'), icon: 'pi pi-info-circle', command: () => { setTimeout(() => { @@ -301,13 +303,13 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { if (hasAdditionalFiles) { const downloadItems = this.getDownloadMenuItems(); items.push({ - label: 'Download', + label: this.t.translate('book.card.menu.download'), icon: 'pi pi-download', items: downloadItems }); } else if (this.additionalFilesLoaded) { items.push({ - label: 'Download', + label: this.t.translate('book.card.menu.download'), icon: 'pi pi-download', command: () => { this.bookService.downloadFile(this.book); @@ -315,9 +317,9 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { }); } else { items.push({ - label: 'Download', + label: this.t.translate('book.card.menu.download'), icon: this.isSubMenuLoading ? 'pi pi-spin pi-spinner' : 'pi pi-download', - items: [{label: 'Loading...', disabled: true}] + items: [{label: this.t.translate('book.card.menu.loading'), disabled: true}] }); } } @@ -329,23 +331,23 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { if (hasAdditionalFiles) { const deleteItems = this.getDeleteMenuItems(); items.push({ - label: 'Delete', + label: this.t.translate('book.card.menu.delete'), icon: 'pi pi-trash', items: deleteItems }); } else if (this.additionalFilesLoaded) { items.push({ - label: 'Delete', + label: this.t.translate('book.card.menu.delete'), icon: 'pi pi-trash', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to delete "${this.book.metadata?.title}"?\n\nThis will permanently remove the book file from your filesystem.\n\nThis action cannot be undone.`, - header: 'Confirm Deletion', + message: this.t.translate('book.card.confirm.deleteBookMessage', {title: this.book.metadata?.title}), + header: this.t.translate('book.card.confirm.deleteBookHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', - acceptLabel: 'Delete', - rejectLabel: 'Cancel', + acceptLabel: this.t.translate('common.delete'), + rejectLabel: this.t.translate('common.cancel'), acceptButtonStyleClass: 'p-button-danger', rejectButtonStyleClass: 'p-button-outlined', accept: () => { @@ -356,9 +358,9 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { }); } else { items.push({ - label: 'Delete', + label: this.t.translate('book.card.menu.delete'), icon: this.isSubMenuLoading ? 'pi pi-spin pi-spinner' : 'pi pi-trash', - items: [{label: 'Loading...', disabled: true}] + items: [{label: this.t.translate('book.card.menu.loading'), disabled: true}] }); } } @@ -366,25 +368,25 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { if (this.user?.permissions.canEmailBook) { items.push( { - label: 'Email Book', + label: this.t.translate('book.card.menu.emailBook'), icon: 'pi pi-envelope', items: [{ - label: 'Quick Send', + label: this.t.translate('book.card.menu.quickSend'), icon: 'pi pi-envelope', command: () => { this.emailService.emailBookQuick(this.book.id).subscribe({ next: () => { this.messageService.add({ severity: 'info', - summary: 'Success', - detail: 'The book sending has been scheduled.', + summary: this.t.translate('common.success'), + detail: this.t.translate('book.card.toast.quickSendSuccessDetail'), }); }, error: (err) => { - const errorMessage = err?.error?.message || 'An error occurred while sending the book.'; + const errorMessage = err?.error?.message || this.t.translate('book.card.toast.quickSendErrorDetail'); this.messageService.add({ severity: 'error', - summary: 'Error', + summary: this.t.translate('common.error'), detail: errorMessage, }); }, @@ -392,7 +394,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { } }, { - label: 'Custom Send', + label: this.t.translate('book.card.menu.customSend'), icon: 'pi pi-envelope', command: () => { this.bookDialogHelperService.openCustomSendDialog(this.book); @@ -404,11 +406,11 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { if (this.user?.permissions.canEditMetadata) { items.push({ - label: 'Metadata', + label: this.t.translate('book.card.menu.metadata'), icon: 'pi pi-database', items: [ { - label: 'Search Metadata', + label: this.t.translate('book.card.menu.searchMetadata'), icon: 'pi pi-sparkles', command: () => { setTimeout(() => { @@ -419,7 +421,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { }, }, { - label: 'Auto Fetch', + label: this.t.translate('book.card.menu.autoFetch'), icon: 'pi pi-bolt', command: () => { this.taskHelperService.refreshMetadataTask({ @@ -429,44 +431,44 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { } }, { - label: 'Custom Fetch', + label: this.t.translate('book.card.menu.customFetch'), icon: 'pi pi-sync', command: () => { this.bookDialogHelperService.openMetadataRefreshDialog(new Set([this.book!.id])) }, }, { - label: 'Regenerate Cover (File)', + label: this.t.translate('book.card.menu.regenerateCover'), icon: 'pi pi-image', command: () => { this.bookService.regenerateCover(this.book.id).subscribe({ next: () => this.messageService.add({ severity: 'success', - summary: 'Success', - detail: 'Cover regeneration started' + summary: this.t.translate('common.success'), + detail: this.t.translate('book.card.toast.coverRegenSuccessDetail') }), error: (err) => this.messageService.add({ severity: 'error', - summary: 'Error', - detail: err?.error?.message || 'Failed to regenerate cover' + summary: this.t.translate('common.error'), + detail: err?.error?.message || this.t.translate('book.card.toast.coverRegenFailedDetail') }) }); } }, { - label: 'Generate Custom Cover', + label: this.t.translate('book.card.menu.generateCustomCover'), icon: 'pi pi-palette', command: () => { this.bookService.generateCustomCover(this.book.id).subscribe({ next: () => this.messageService.add({ severity: 'success', - summary: 'Success', - detail: 'Cover generated successfully' + summary: this.t.translate('common.success'), + detail: this.t.translate('book.card.toast.customCoverSuccessDetail') }), error: (err) => this.messageService.add({ severity: 'error', - summary: 'Error', - detail: err?.error?.message || 'Failed to generate cover' + summary: this.t.translate('common.error'), + detail: err?.error?.message || this.t.translate('book.card.toast.customCoverFailedDetail') }) }); } @@ -484,7 +486,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { if (this.user?.permissions.canMoveOrganizeFiles && this.diskType === 'LOCAL') { moreActions.push({ - label: 'Organize File', + label: this.t.translate('book.card.menu.organizeFile'), icon: 'pi pi-arrows-h', command: () => { this.bookDialogHelperService.openFileMoverDialog(new Set([this.book.id])); @@ -494,7 +496,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { moreActions.push( { - label: 'Read Status', + label: this.t.translate('book.card.menu.readStatus'), icon: 'pi pi-book', items: Object.entries(readStatusLabels).map(([status, label]) => ({ label, @@ -503,16 +505,16 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { next: () => { this.messageService.add({ severity: 'success', - summary: 'Read Status Updated', - detail: `Marked as "${label}"`, + summary: this.t.translate('book.card.toast.readStatusUpdatedSummary'), + detail: this.t.translate('book.card.toast.readStatusUpdatedDetail', {label}), life: 2000 }); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Update Failed', - detail: 'Could not update read status.', + summary: this.t.translate('book.card.toast.readStatusFailedSummary'), + detail: this.t.translate('book.card.toast.readStatusFailedDetail'), life: 3000 }); } @@ -521,23 +523,23 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { })) }, { - label: 'Reset Booklore Progress', + label: this.t.translate('book.card.menu.resetBookloreProgress'), icon: 'pi pi-undo', command: () => { this.bookService.resetProgress(this.book.id, ResetProgressTypes.BOOKLORE).subscribe({ next: () => { this.messageService.add({ severity: 'success', - summary: 'Progress Reset', - detail: 'Booklore reading progress has been reset.', + summary: this.t.translate('book.card.toast.progressResetSummary'), + detail: this.t.translate('book.card.toast.progressResetBookloreDetail'), life: 1500 }); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Could not reset Booklore progress.', + summary: this.t.translate('book.card.toast.progressResetFailedSummary'), + detail: this.t.translate('book.card.toast.progressResetBookloreFailedDetail'), life: 1500 }); } @@ -545,23 +547,23 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { }, }, { - label: 'Reset KOReader Progress', + label: this.t.translate('book.card.menu.resetKOReaderProgress'), icon: 'pi pi-undo', command: () => { this.bookService.resetProgress(this.book.id, ResetProgressTypes.KOREADER).subscribe({ next: () => { this.messageService.add({ severity: 'success', - summary: 'Progress Reset', - detail: 'KOReader reading progress has been reset.', + summary: this.t.translate('book.card.toast.progressResetSummary'), + detail: this.t.translate('book.card.toast.progressResetKOReaderDetail'), life: 1500 }); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Could not reset KOReader progress.', + summary: this.t.translate('book.card.toast.progressResetFailedSummary'), + detail: this.t.translate('book.card.toast.progressResetKOReaderFailedDetail'), life: 1500 }); } @@ -571,7 +573,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { ); items.push({ - label: 'More Actions', + label: this.t.translate('book.card.menu.moreActions'), icon: 'pi pi-ellipsis-h', items: moreActions }); @@ -657,17 +659,17 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { const items: MenuItem[] = []; items.push({ - label: 'Book', + label: this.t.translate('book.card.menu.book'), icon: 'pi pi-book', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to delete "${this.book.metadata?.title}"?\n\nThis will permanently remove the book file from your filesystem.\n\nThis action cannot be undone.`, - header: 'Confirm Deletion', + message: this.t.translate('book.card.confirm.deleteBookMessage', {title: this.book.metadata?.title}), + header: this.t.translate('book.card.confirm.deleteBookHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', - acceptLabel: 'Delete', - rejectLabel: 'Cancel', + acceptLabel: this.t.translate('common.delete'), + rejectLabel: this.t.translate('common.cancel'), acceptButtonStyleClass: 'p-button-danger', rejectButtonStyleClass: 'p-button-outlined', accept: () => { @@ -722,8 +724,8 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { private deleteAdditionalFile(bookId: number, fileId: number, fileName: string): void { this.confirmationService.confirm({ - message: `Are you sure you want to delete the additional file "${fileName}"?`, - header: 'Confirm File Deletion', + message: this.t.translate('book.card.confirm.deleteFileMessage', {fileName}), + header: this.t.translate('book.card.confirm.deleteFileHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', @@ -733,15 +735,15 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { next: () => { this.messageService.add({ severity: 'success', - summary: 'Success', - detail: `Additional file "${fileName}" deleted successfully` + summary: this.t.translate('common.success'), + detail: this.t.translate('book.card.toast.deleteFileSuccessDetail', {fileName}) }); }, error: (error) => { this.messageService.add({ severity: 'error', - summary: 'Error', - detail: `Failed to delete additional file: ${error.message || 'Unknown error'}` + summary: this.t.translate('common.error'), + detail: this.t.translate('book.card.toast.deleteFileErrorDetail', {error: error.message || 'Unknown error'}) }); } }); diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html index 2e2b4b9d6..aca44975d 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html @@ -1,7 +1,8 @@ +
- Filters + {{ t('title') }} @if (truncatedFilters[filterType]) {
- Showing first 100 items + {{ t('showingFirst100') }}
} } @@ -62,7 +63,8 @@
+
diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts index 477a94659..6b20b518c 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts @@ -14,6 +14,7 @@ import {MagicShelf} from '../../../../magic-shelf/service/magic-shelf.service'; import {Filter, FILTER_LABELS, FilterType} from './book-filter.config'; import {BookFilterService} from './book-filter.service'; import {filter} from 'rxjs/operators'; +import {TranslocoDirective} from '@jsverse/transloco'; type FilterModeOption = { label: string; value: BookFilterMode }; @@ -26,7 +27,8 @@ type FilterModeOption = { label: string; value: BookFilterMode }; imports: [ Accordion, AccordionPanel, AccordionHeader, AccordionContent, CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf, - NgClass, Badge, AsyncPipe, TitleCasePipe, FormsModule, SelectButton + NgClass, Badge, AsyncPipe, TitleCasePipe, FormsModule, SelectButton, + TranslocoDirective ] }) export class BookFilterComponent implements OnInit, OnDestroy { diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.html b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.html index 8ca07a1e4..aa7cf7b64 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.html @@ -1,3 +1,4 @@ + @@ -52,7 +53,7 @@ Book Cover Book Cover {{ metadata.title }} @@ -77,7 +78,7 @@ @if (shouldShowStatusIcon(book.readStatus)) {
@@ -116,3 +117,4 @@ }
+
diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts index de54c07af..f65eaf682 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts @@ -15,6 +15,7 @@ import {filter, Subject} from 'rxjs'; import {UserService} from '../../../../settings/user-management/user.service'; import {take, takeUntil} from 'rxjs/operators'; import {ReadStatusHelper} from '../../../helpers/read-status.helper'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-book-table', @@ -27,7 +28,8 @@ import {ReadStatusHelper} from '../../../helpers/read-status.helper'; Button, TooltipModule, NgClass, - RouterLink + RouterLink, + TranslocoDirective ], styleUrls: ['./book-table.component.scss'], providers: [DatePipe] @@ -48,6 +50,7 @@ export class BookTableComponent implements OnInit, OnDestroy, OnChanges { private userService = inject(UserService); private datePipe = inject(DatePipe); private readStatusHelper = inject(ReadStatusHelper); + private readonly t = inject(TranslocoService); private metadataCenterViewMode: 'route' | 'dialog' = 'route'; private destroy$ = new Subject(); @@ -327,15 +330,15 @@ export class BookTableComponent implements OnInit, OnDestroy, OnChanges { next: () => { this.messageService.add({ severity: 'success', - summary: `Metadata ${lockAction === 'LOCK' ? 'Locked' : 'Unlocked'}`, - detail: `Book metadata has been ${lockAction === 'LOCK' ? 'locked' : 'unlocked'} successfully.`, + summary: lockAction === 'LOCK' ? this.t.translate('book.table.toast.metadataLockedSummary') : this.t.translate('book.table.toast.metadataUnlockedSummary'), + detail: lockAction === 'LOCK' ? this.t.translate('book.table.toast.metadataLockedDetail') : this.t.translate('book.table.toast.metadataUnlockedDetail'), }); }, error: () => { this.messageService.add({ severity: 'error', - summary: `Failed to ${lockAction === 'LOCK' ? 'Lock' : 'Unlock'}`, - detail: `An error occurred while ${lockAction === 'LOCK' ? 'locking' : 'unlocking'} the metadata.`, + summary: lockAction === 'LOCK' ? this.t.translate('book.table.toast.lockFailedSummary') : this.t.translate('book.table.toast.unlockFailedSummary'), + detail: lockAction === 'LOCK' ? this.t.translate('book.table.toast.lockFailedDetail') : this.t.translate('book.table.toast.unlockFailedDetail'), }); } }); diff --git a/booklore-ui/src/app/features/book/components/book-browser/cover-scale-preference.service.ts b/booklore-ui/src/app/features/book/components/book-browser/cover-scale-preference.service.ts index 08ffb352b..58b1eaa67 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/cover-scale-preference.service.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/cover-scale-preference.service.ts @@ -2,6 +2,7 @@ import {inject, Injectable} from '@angular/core'; import {Subject} from 'rxjs'; import {debounceTime} from 'rxjs/operators'; import {MessageService} from 'primeng/api'; +import {TranslocoService} from '@jsverse/transloco'; import {LocalStorageService} from '../../../../shared/service/local-storage.service'; import {Book} from '../../model/book.model'; @@ -17,6 +18,7 @@ export class CoverScalePreferenceService { private readonly STORAGE_KEY = 'coverScalePreference'; private readonly messageService = inject(MessageService); + private readonly t = inject(TranslocoService); private readonly localStorageService = inject(LocalStorageService); private readonly scaleChangeSubject = new Subject(); @@ -64,15 +66,15 @@ export class CoverScalePreferenceService { this.localStorageService.set(this.STORAGE_KEY, scale); this.messageService.add({ severity: 'success', - summary: 'Cover Size Saved', - detail: `Cover size set to ${scale.toFixed(2)}x.`, + summary: this.t.translate('book.coverPref.toast.savedSummary'), + detail: this.t.translate('book.coverPref.toast.savedDetail', {scale: scale.toFixed(2)}), life: 1500 }); } catch (e) { this.messageService.add({ severity: 'error', - summary: 'Save Failed', - detail: 'Could not save cover size preference locally.', + summary: this.t.translate('book.coverPref.toast.saveFailedSummary'), + detail: this.t.translate('book.coverPref.toast.saveFailedDetail'), life: 3000 }); } diff --git a/booklore-ui/src/app/features/book/components/book-browser/filters/sidebar-filter-toggle-pref.service.ts b/booklore-ui/src/app/features/book/components/book-browser/filters/sidebar-filter-toggle-pref.service.ts index c546af1db..6826c254a 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/filters/sidebar-filter-toggle-pref.service.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/filters/sidebar-filter-toggle-pref.service.ts @@ -1,6 +1,7 @@ import {inject, Injectable} from '@angular/core'; import {BehaviorSubject} from 'rxjs'; import {MessageService} from 'primeng/api'; +import {TranslocoService} from '@jsverse/transloco'; import {LocalStorageService} from '../../../../../shared/service/local-storage.service'; @Injectable({ @@ -10,6 +11,7 @@ export class SidebarFilterTogglePrefService { private readonly STORAGE_KEY = 'showSidebarFilter'; private readonly messageService = inject(MessageService); + private readonly t = inject(TranslocoService); private readonly localStorageService = inject(LocalStorageService); private readonly showFilterSubject = new BehaviorSubject(true); @@ -43,8 +45,8 @@ export class SidebarFilterTogglePrefService { } catch (e) { this.messageService.add({ severity: 'error', - summary: 'Save Failed', - detail: 'Could not save sidebar filter preference locally.', + summary: this.t.translate('book.filterPref.toast.saveFailedSummary'), + detail: this.t.translate('book.filterPref.toast.saveFailedDetail'), life: 3000 }); } diff --git a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.html b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.html index 69d2579c0..f593b73d5 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.html @@ -1,11 +1,12 @@ +
-

Lock or Unlock Metadata

-

{{ bookIds.size }} book{{ bookIds.size > 1 ? 's' : '' }} selected

+

{{ t('title') }}

+

{{ t('selectedCount', { count: bookIds.size }) }}

+
diff --git a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts index 43221c024..eb6a3de71 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts @@ -8,6 +8,7 @@ import {BookService} from '../../../service/book.service'; import {Divider} from 'primeng/divider'; import {LoadingService} from '../../../../../core/services/loading.service'; import {finalize} from 'rxjs'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-lock-unlock-metadata-dialog', @@ -15,7 +16,8 @@ import {finalize} from 'rxjs'; imports: [ Button, FormsModule, - Divider + Divider, + TranslocoDirective ], templateUrl: './lock-unlock-metadata-dialog.component.html', styleUrl: './lock-unlock-metadata-dialog.component.scss' @@ -26,6 +28,7 @@ export class LockUnlockMetadataDialogComponent implements OnInit { dialogRef = inject(DynamicDialogRef); private messageService = inject(MessageService); private loadingService = inject(LoadingService); + private readonly t = inject(TranslocoService); fieldLocks: Record = {}; bookIds: Set = this.dynamicDialogConfig.data.bookIds; @@ -89,8 +92,8 @@ export class LockUnlockMetadataDialogComponent implements OnInit { getLockLabel(field: string): string { const state = this.fieldLocks[field]; - if (state === undefined) return 'Unselected'; - return state ? 'Locked' : 'Unlocked'; + if (state === undefined) return this.t.translate('book.lockUnlockDialog.unselected'); + return state ? this.t.translate('book.lockUnlockDialog.locked') : this.t.translate('book.lockUnlockDialog.unlocked'); } getLockIcon(field: string): string { @@ -124,7 +127,7 @@ export class LockUnlockMetadataDialogComponent implements OnInit { } this.isSaving = true; - const loader = this.loadingService.show('Updating field locks...'); + const loader = this.loadingService.show(this.t.translate('book.lockUnlockDialog.toast.updatingFieldLocks')); this.bookService.toggleFieldLocks(this.bookIds, fieldActions) .pipe(finalize(() => { @@ -135,16 +138,16 @@ export class LockUnlockMetadataDialogComponent implements OnInit { next: () => { this.messageService.add({ severity: 'success', - summary: 'Field Locks Updated', - detail: 'Selected metadata fields have been updated successfully.' + summary: this.t.translate('book.lockUnlockDialog.toast.updatedSummary'), + detail: this.t.translate('book.lockUnlockDialog.toast.updatedDetail') }); this.dialogRef.close('fields-updated'); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed to Update Field Locks', - detail: 'An error occurred while updating field lock statuses.' + summary: this.t.translate('book.lockUnlockDialog.toast.failedSummary'), + detail: this.t.translate('book.lockUnlockDialog.toast.failedDetail') }); } }); diff --git a/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.html b/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.html index 0d45c30d4..41155055e 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.html @@ -1,6 +1,7 @@ +
- Sort Order + {{ t('sortOrder') }}
@@ -45,7 +46,7 @@ [(ngModel)]="selectedField" optionLabel="label" optionValue="field" - placeholder="Add sort field..." + [placeholder]="t('addSortFieldPlaceholder')" [style]="{ width: '100%' }" size="small" appendTo="body" @@ -58,7 +59,7 @@
}
+ diff --git a/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.ts b/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.ts index c0ab9e917..3e96a0395 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/sorting/multi-sort-popover/multi-sort-popover.component.ts @@ -1,10 +1,11 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {Component, EventEmitter, inject, Input, Output} from '@angular/core'; import {SortDirection, SortOption} from '../../../../model/sort.model'; import {CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList, moveItemInArray} from '@angular/cdk/drag-drop'; import {Select} from 'primeng/select'; import {FormsModule} from '@angular/forms'; import {Tooltip} from 'primeng/tooltip'; import {Button} from 'primeng/button'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-multi-sort-popover', @@ -16,12 +17,14 @@ import {Button} from 'primeng/button'; Select, FormsModule, Tooltip, - Button + Button, + TranslocoDirective ], templateUrl: './multi-sort-popover.component.html', styleUrl: './multi-sort-popover.component.scss' }) export class MultiSortPopoverComponent { + private readonly t = inject(TranslocoService); @Input() sortCriteria: SortOption[] = []; @Input() availableSortOptions: SortOption[] = []; @Input() showSaveButton = false; @@ -79,7 +82,9 @@ export class MultiSortPopoverComponent { } getDirectionTooltip(direction: SortDirection): string { - return direction === SortDirection.ASCENDING ? 'Ascending - click to change' : 'Descending - click to change'; + return direction === SortDirection.ASCENDING + ? this.t.translate('book.sorting.ascendingTooltip') + : this.t.translate('book.sorting.descendingTooltip'); } protected readonly SortDirection = SortDirection; diff --git a/booklore-ui/src/app/features/book/components/book-browser/table-column-preference.service.ts b/booklore-ui/src/app/features/book/components/book-browser/table-column-preference.service.ts index 708fd75fe..a97675267 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/table-column-preference.service.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/table-column-preference.service.ts @@ -1,6 +1,7 @@ import {inject, Injectable} from '@angular/core'; import {BehaviorSubject} from 'rxjs'; import {MessageService} from 'primeng/api'; +import {TranslocoService} from '@jsverse/transloco'; import {TableColumnPreference, UserService} from '../../../settings/user-management/user.service'; @Injectable({ @@ -9,37 +10,21 @@ import {TableColumnPreference, UserService} from '../../../settings/user-managem export class TableColumnPreferenceService { private readonly userService = inject(UserService); private readonly messageService = inject(MessageService); + private readonly t = inject(TranslocoService); private readonly preferencesSubject = new BehaviorSubject([]); readonly preferences$ = this.preferencesSubject.asObservable(); - private readonly allAvailableColumns = [ - {field: 'readStatus', header: 'Read'}, - {field: 'title', header: 'Title'}, - {field: 'authors', header: 'Authors'}, - {field: 'publisher', header: 'Publisher'}, - {field: 'seriesName', header: 'Series'}, - {field: 'seriesNumber', header: 'Series #'}, - {field: 'categories', header: 'Genres'}, - {field: 'publishedDate', header: 'Published'}, - {field: 'lastReadTime', header: 'Last Read'}, - {field: 'addedOn', header: 'Added'}, - {field: 'fileName', header: 'File Name'}, - {field: 'fileSizeKb', header: 'File Size'}, - {field: 'language', header: 'Language'}, - {field: 'isbn', header: 'ISBN'}, - {field: 'pageCount', header: 'Pages'}, - {field: 'amazonRating', header: 'Amazon'}, - {field: 'amazonReviewCount', header: 'AZ #'}, - {field: 'goodreadsRating', header: 'Goodreads'}, - {field: 'goodreadsReviewCount', header: 'GR #'}, - {field: 'hardcoverRating', header: 'Hardcover'}, - {field: 'hardcoverReviewCount', header: 'HC #'}, - {field: 'ranobedbRating', header: 'Ranobedb'}, + private readonly allAvailableFields = [ + 'readStatus', 'title', 'authors', 'publisher', 'seriesName', 'seriesNumber', + 'categories', 'publishedDate', 'lastReadTime', 'addedOn', 'fileName', 'fileSizeKb', + 'language', 'isbn', 'pageCount', 'amazonRating', 'amazonReviewCount', + 'goodreadsRating', 'goodreadsReviewCount', 'hardcoverRating', 'hardcoverReviewCount', + 'ranobedbRating', ]; - private readonly fallbackPreferences: TableColumnPreference[] = this.allAvailableColumns.map((col, index) => ({ - field: col.field, + private readonly fallbackPreferences: TableColumnPreference[] = this.allAvailableFields.map((field, index) => ({ + field, visible: true, order: index })); @@ -50,7 +35,10 @@ export class TableColumnPreferenceService { } get allColumns(): { field: string; header: string }[] { - return this.allAvailableColumns; + return this.allAvailableFields.map(field => ({ + field, + header: this.t.translate(`book.columnPref.columns.${field}`) + })); } get visibleColumns(): { field: string; header: string }[] { @@ -59,7 +47,7 @@ export class TableColumnPreferenceService { .sort((a, b) => a.order - b.order) .map(pref => ({ field: pref.field, - header: this.getColumnHeader(pref.field) + header: this.t.translate(`book.columnPref.columns.${pref.field}`) })); } @@ -70,11 +58,11 @@ export class TableColumnPreferenceService { saveVisibleColumns(selectedColumns: { field: string }[]): void { const selectedFieldSet = new Set(selectedColumns.map(c => c.field)); - const updatedPreferences: TableColumnPreference[] = this.allAvailableColumns.map((col, index) => { - const selectionIndex = selectedColumns.findIndex(c => c.field === col.field); + const updatedPreferences: TableColumnPreference[] = this.allAvailableFields.map((field, index) => { + const selectionIndex = selectedColumns.findIndex(c => c.field === field); return { - field: col.field, - visible: selectedFieldSet.has(col.field), + field, + visible: selectedFieldSet.has(field), order: selectionIndex >= 0 ? selectionIndex : index }; }); @@ -88,23 +76,19 @@ export class TableColumnPreferenceService { this.messageService.add({ severity: 'success', - summary: 'Preferences Saved', - detail: 'Your column layout has been saved.', + summary: this.t.translate('book.columnPref.toast.savedSummary'), + detail: this.t.translate('book.columnPref.toast.savedDetail'), life: 1500 }); } - private getColumnHeader(field: string): string { - return this.allAvailableColumns.find(col => col.field === field)?.header ?? field; - } - private mergeWithAllColumns(savedPrefs: TableColumnPreference[]): TableColumnPreference[] { const savedPrefMap = new Map(savedPrefs.map(p => [p.field, p])); - return this.allAvailableColumns.map((col, index) => { - const saved = savedPrefMap.get(col.field); + return this.allAvailableFields.map((field, index) => { + const saved = savedPrefMap.get(field); return { - field: col.field, + field, visible: saved?.visible ?? true, order: saved?.order ?? index }; diff --git a/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.html b/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.html index fac50d31a..54840983f 100644 --- a/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.html +++ b/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.html @@ -1,11 +1,12 @@ +
-

Attach {{ isBulkMode ? 'Files' : 'File' }} to Another Book

-

Move {{ isBulkMode ? 'these books\' files' : 'this book\'s file' }} to another book as alternative format{{ isBulkMode ? 's' : '' }}

+

{{ isBulkMode ? t('titleBulk') : t('title') }}

+

{{ isBulkMode ? t('descriptionBulk') : t('description') }}

-

Source Book{{ isBulkMode ? 's' : '' }} ({{ sourceBooks.length }})

+

{{ isBulkMode ? t('sourceBooksLabel', { count: sourceBooks.length }) : t('sourceBookLabel', { count: sourceBooks.length }) }}

@for (book of sourceBooks; track book.id) {
-
{{ book.metadata?.title || 'Unknown Title' }}
+
{{ book.metadata?.title || t('unknownTitle') }}
@if (book.metadata?.authors?.length) {
{{ book.metadata?.authors?.join(', ') }}
} @@ -39,7 +40,7 @@
-

Select Target Book

+

{{ t('selectTargetBook') }}

-
{{ book.metadata?.title || 'Unknown Title' }}
+
{{ book.metadata?.title || t('unknownTitle') }}
@if (book.metadata?.authors?.length) {
{{ book.metadata.authors.join(', ') }}
} @@ -78,7 +79,7 @@ [disabled]="isAttaching">
@@ -86,11 +87,11 @@
-

This will move {{ isBulkMode ? 'the files from the selected books' : 'the file from the source book' }} to the target book as alternative format{{ isBulkMode ? 's' : '' }}.

+

{{ isBulkMode ? t('warningMoveBulk') : t('warningMove') }}

@if (deleteSourceBooks) { -

The source book{{ isBulkMode ? ' records' : ' record' }} will be deleted (file{{ isBulkMode ? 's' : '' }} will be preserved in target book).

+

{{ isBulkMode ? t('warningDeleteBulk') : t('warningDelete') }}

} @else { -

The source book{{ isBulkMode ? ' records' : ' record' }} will remain but will have no readable files.

+

{{ isBulkMode ? t('warningKeepBulk') : t('warningKeep') }}

}
@@ -98,14 +99,14 @@
+ diff --git a/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts b/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts index cda270f25..9fb77b6a0 100644 --- a/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts +++ b/booklore-ui/src/app/features/book/components/book-file-attacher/book-file-attacher.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, inject, OnInit, OnDestroy } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { DynamicDialogRef, DynamicDialogConfig } from 'primeng/dynamicdialog'; import { AutoComplete, AutoCompleteSelectEvent } from 'primeng/autocomplete'; @@ -9,6 +9,7 @@ import { filter, take } from 'rxjs/operators'; import { BookService } from '../../service/book.service'; import { Book } from '../../model/book.model'; import { MessageService } from 'primeng/api'; +import { TranslocoDirective, TranslocoPipe, TranslocoService } from '@jsverse/transloco'; @Component({ selector: 'app-book-file-attacher', @@ -17,7 +18,9 @@ import { MessageService } from 'primeng/api'; FormsModule, AutoComplete, Button, - Checkbox + Checkbox, + TranslocoDirective, + TranslocoPipe, ], templateUrl: './book-file-attacher.component.html', styleUrls: ['./book-file-attacher.component.scss'] @@ -33,6 +36,8 @@ export class BookFileAttacherComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); private allBooks: Book[] = []; + private readonly t = inject(TranslocoService); + constructor( private dialogRef: DynamicDialogRef, private config: DynamicDialogConfig, @@ -110,9 +115,9 @@ export class BookFileAttacherComponent implements OnInit, OnDestroy { getSourceFileInfo(book: Book): string { const file = book.primaryFile; - if (!file) return 'Unknown file'; - const format = file.extension?.toUpperCase() || file.bookType || 'Unknown'; - return `${format} - ${file.fileName || 'Unknown filename'}`; + if (!file) return this.t.translate('book.fileAttacher.unknownFile'); + const format = file.extension?.toUpperCase() || file.bookType || this.t.translate('book.fileAttacher.unknownFormat'); + return `${format} - ${file.fileName || this.t.translate('book.fileAttacher.unknownFilename')}`; } canAttach(): boolean { diff --git a/booklore-ui/src/app/features/book/components/book-notes/book-notes-component.ts b/booklore-ui/src/app/features/book/components/book-notes/book-notes-component.ts index cb3fe20b2..1f652641e 100644 --- a/booklore-ui/src/app/features/book/components/book-notes/book-notes-component.ts +++ b/booklore-ui/src/app/features/book/components/book-notes/book-notes-component.ts @@ -10,6 +10,7 @@ import {ConfirmDialog} from 'primeng/confirmdialog'; import {ProgressSpinner} from 'primeng/progressspinner'; import {Tooltip} from 'primeng/tooltip'; import {ConfirmationService, MessageService} from 'primeng/api'; +import {TranslocoService} from '@jsverse/transloco'; import {BookNote, BookNoteService, CreateBookNoteRequest} from '../../../../shared/service/book-note.service'; @Component({ @@ -36,6 +37,7 @@ export class BookNotesComponent implements OnInit, OnChanges { private confirmationService = inject(ConfirmationService); private messageService = inject(MessageService); private destroyRef = inject(DestroyRef); + private readonly t = inject(TranslocoService); notes: BookNote[] = []; loading = false; @@ -85,8 +87,8 @@ export class BookNotesComponent implements OnInit, OnChanges { this.loading = false; this.messageService.add({ severity: 'error', - summary: 'Error', - detail: 'Failed to load notes for this book.' + summary: this.t.translate('common.error'), + detail: this.t.translate('book.notes.toast.loadFailedDetail') }); } }); @@ -116,8 +118,8 @@ export class BookNotesComponent implements OnInit, OnChanges { if (!this.newNote.title.trim() || !this.newNote.content.trim()) { this.messageService.add({ severity: 'warn', - summary: 'Validation Error', - detail: 'Both title and content are required.' + summary: this.t.translate('book.notes.toast.validationSummary'), + detail: this.t.translate('book.notes.toast.validationDetail') }); return; } @@ -130,16 +132,16 @@ export class BookNotesComponent implements OnInit, OnChanges { this.showCreateDialog = false; this.messageService.add({ severity: 'success', - summary: 'Success', - detail: 'Note created successfully.' + summary: this.t.translate('common.success'), + detail: this.t.translate('book.notes.toast.createSuccessDetail') }); }, error: (error) => { console.error('Failed to create note:', error); this.messageService.add({ severity: 'error', - summary: 'Error', - detail: 'Failed to create note.' + summary: this.t.translate('common.error'), + detail: this.t.translate('book.notes.toast.createFailedDetail') }); } }); @@ -149,8 +151,8 @@ export class BookNotesComponent implements OnInit, OnChanges { if (!this.editNote.title?.trim() || !this.editNote.content?.trim()) { this.messageService.add({ severity: 'warn', - summary: 'Validation Error', - detail: 'Both title and content are required.' + summary: this.t.translate('book.notes.toast.validationSummary'), + detail: this.t.translate('book.notes.toast.validationDetail') }); return; } @@ -170,16 +172,16 @@ export class BookNotesComponent implements OnInit, OnChanges { this.selectedNote = null; this.messageService.add({ severity: 'success', - summary: 'Success', - detail: 'Note updated successfully.' + summary: this.t.translate('common.success'), + detail: this.t.translate('book.notes.toast.updateSuccessDetail') }); }, error: (error) => { console.error('Failed to update note:', error); this.messageService.add({ severity: 'error', - summary: 'Error', - detail: 'Failed to update note.' + summary: this.t.translate('common.error'), + detail: this.t.translate('book.notes.toast.updateFailedDetail') }); } }); @@ -188,8 +190,8 @@ export class BookNotesComponent implements OnInit, OnChanges { deleteNote(note: BookNote): void { this.confirmationService.confirm({ key: 'deleteNote', - message: `Are you sure you want to delete the note "${note.title}"?`, - header: 'Confirm Deletion', + message: this.t.translate('book.notes.confirm.deleteMessage', {title: note.title}), + header: this.t.translate('book.notes.confirm.deleteHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', @@ -207,16 +209,16 @@ export class BookNotesComponent implements OnInit, OnChanges { this.notes = this.notes.filter(n => n.id !== noteId); this.messageService.add({ severity: 'success', - summary: 'Success', - detail: 'Note deleted successfully.' + summary: this.t.translate('common.success'), + detail: this.t.translate('book.notes.toast.deleteSuccessDetail') }); }, error: (error) => { console.error('Failed to delete note:', error); this.messageService.add({ severity: 'error', - summary: 'Error', - detail: 'Failed to delete note.' + summary: this.t.translate('common.error'), + detail: this.t.translate('book.notes.toast.deleteFailedDetail') }); } }); diff --git a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.html b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.html index ce582e937..a9249caa9 100644 --- a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.html +++ b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.html @@ -1,9 +1,10 @@ +
@if (loading) {
- Getting latest reviews... + {{ t('labels.loadingReviews') }}
} @else if (reviews?.length === 0) {
@@ -15,20 +16,20 @@ icon="pi pi-download" severity="primary" (click)="fetchNewReviews()" - pTooltip="Fetch Reviews" + [pTooltip]="t('tooltip.fetchReviews')" tooltipPosition="top" class="action-btn floating-btn"> }
-

No reviews available for this book

+

{{ t('empty.noReviews') }}

@if (!reviewDownloadEnabled) {

- Book review downloads are currently disabled. Enable this in Metadata Settings to fetch reviews. + {{ t('empty.downloadsDisabled') }}

} @else if (hasPermission && !reviewsLocked) {

- Click "Fetch Reviews" to download reviews from configured providers + {{ t('empty.fetchPrompt') }}

}
@@ -40,7 +41,7 @@
@if (review.spoiler && !isSpoilerRevealed(review.id!)) {
- {{ review.reviewerName || 'Anonymous' }} + {{ review.reviewerName || t('labels.anonymous') }} @if (review.metadataProvider) { } @if (review.spoiler) { - + }
@@ -89,7 +90,7 @@ @if (reviewsLocked) {
} @else { @@ -99,7 +100,7 @@ severity="danger" text (onClick)="deleteReview(review)" - pTooltip="Delete Review" + [pTooltip]="t('tooltip.deleteReview')" tooltipPosition="top" class="action-btn-hover"/> } @@ -126,14 +127,14 @@ @if (review.body) {
{{ review.body }}
} @else { -
No review content available
+
{{ t('empty.noContent') }}
}
} @else { @if (review.body) {
{{ review.body }}
} @else { -
No review content available
+
{{ t('empty.noContent') }}
} }
@@ -157,7 +158,7 @@ [severity]="reviewsLocked ? 'danger' : 'success'" [disabled]="!reviewDownloadEnabled || loading" (click)="toggleReviewsLock()" - [pTooltip]="loading ? 'Please wait while reviews are being fetched' : (!reviewDownloadEnabled ? 'Enable review downloads in settings to use this feature' : (reviewsLocked ? 'Unlock Reviews' : 'Lock Reviews'))" + [pTooltip]="loading ? t('tooltip.pleaseWait') : (!reviewDownloadEnabled ? t('tooltip.enableDownloads') : (reviewsLocked ? t('tooltip.unlockReviews') : t('tooltip.lockReviews')))" tooltipPosition="left" class="action-btn"/> @@ -169,7 +170,7 @@ [loading]="loading" [disabled]="reviewsLocked || !reviewDownloadEnabled || loading" (click)="fetchNewReviews()" - [pTooltip]="loading ? 'Please wait while reviews are being fetched' : (!reviewDownloadEnabled ? 'Enable review downloads in settings to use this feature' : (reviewsLocked ? 'Reviews are locked' : 'Fetch New Reviews'))" + [pTooltip]="loading ? t('tooltip.pleaseWait') : (!reviewDownloadEnabled ? t('tooltip.enableDownloads') : (reviewsLocked ? t('tooltip.reviewsLocked') : t('tooltip.fetchNewReviews')))" tooltipPosition="left" class="action-btn"/> } @@ -182,7 +183,7 @@ severity="danger" [disabled]="reviewsLocked || loading" (click)="deleteAllReviews()" - [pTooltip]="loading ? 'Please wait while reviews are being fetched' : (reviewsLocked ? 'Reviews are locked' : 'Delete All Reviews')" + [pTooltip]="loading ? t('tooltip.pleaseWait') : (reviewsLocked ? t('tooltip.reviewsLocked') : t('tooltip.deleteAllReviews'))" tooltipPosition="left" class="action-btn"/> } @@ -195,7 +196,7 @@ [severity]="allSpoilersRevealed ? 'warn' : 'info'" [disabled]="loading" (click)="toggleSpoilerVisibility()" - [pTooltip]="loading ? 'Please wait while reviews are being fetched' : (allSpoilersRevealed ? 'Hide All Spoilers' : 'Reveal All Spoilers')" + [pTooltip]="loading ? t('tooltip.pleaseWait') : (allSpoilersRevealed ? t('tooltip.hideAllSpoilers') : t('tooltip.revealAllSpoilers'))" tooltipPosition="left" class="action-btn"/> @@ -206,10 +207,11 @@ severity="contrast" [disabled]="loading" (click)="toggleSortOrder()" - [pTooltip]="loading ? 'Please wait while reviews are being fetched' : (sortAscending ? 'Sort by Newest First' : 'Sort by Oldest First')" + [pTooltip]="loading ? t('tooltip.pleaseWait') : (sortAscending ? t('tooltip.sortNewest') : t('tooltip.sortOldest'))" tooltipPosition="left" class="action-btn"/> }
+ diff --git a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts index dbb5e0aea..c51ce80ce 100644 --- a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts +++ b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts @@ -7,6 +7,7 @@ import {Rating} from 'primeng/rating'; import {Tag} from 'primeng/tag'; import {Button} from 'primeng/button'; import {ConfirmationService, MessageService} from 'primeng/api'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; import {UserService} from '../../../settings/user-management/user.service'; import {FormsModule} from '@angular/forms'; import {Tooltip} from 'primeng/tooltip'; @@ -16,7 +17,7 @@ import {AppSettingsService} from '../../../../shared/service/app-settings.servic @Component({ selector: 'app-book-reviews', standalone: true, - imports: [ProgressSpinner, Rating, Tag, Button, FormsModule, Tooltip], + imports: [ProgressSpinner, Rating, Tag, Button, FormsModule, Tooltip, TranslocoDirective], templateUrl: './book-reviews.component.html', styleUrl: './book-reviews.component.scss' }) @@ -32,6 +33,7 @@ export class BookReviewsComponent implements OnInit, OnChanges { private userService = inject(UserService); private appSettingsService = inject(AppSettingsService); private destroyRef = inject(DestroyRef); + private readonly t = inject(TranslocoService); loading = false; hasPermission = false; @@ -84,8 +86,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { this.messageService.add({ severity: 'error', - summary: 'Failed to Load Reviews', - detail: 'Could not load reviews for this book.', + summary: this.t.translate('book.reviews.toast.loadFailedSummary'), + detail: this.t.translate('book.reviews.toast.loadFailedDetail'), life: 3000 }); } @@ -107,8 +109,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { this.updateSpoilerState(); this.messageService.add({ severity: 'success', - summary: 'Reviews Updated', - detail: 'Latest reviews have been fetched successfully.', + summary: this.t.translate('book.reviews.toast.reviewsUpdatedSummary'), + detail: this.t.translate('book.reviews.toast.reviewsUpdatedDetail'), life: 3000 }); }, @@ -117,8 +119,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { this.loading = false; this.messageService.add({ severity: 'error', - summary: 'Fetch Failed', - detail: 'Could not fetch new reviews for this book.', + summary: this.t.translate('book.reviews.toast.fetchFailedSummary'), + detail: this.t.translate('book.reviews.toast.fetchFailedDetail'), life: 3000 }); } @@ -129,8 +131,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { if (!this.reviews || this.reviews.length === 0 || this.reviewsLocked) return; this.confirmationService.confirm({ - message: `Are you sure you want to delete all ${this.reviews.length} reviews for this book? This action cannot be undone.`, - header: 'Confirm Delete All', + message: this.t.translate('book.reviews.confirm.deleteAllMessage', {count: this.reviews.length}), + header: this.t.translate('book.reviews.confirm.deleteAllHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', @@ -145,8 +147,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { this.allSpoilersRevealed = false; this.messageService.add({ severity: 'success', - summary: 'All Reviews Deleted', - detail: 'All reviews have been successfully deleted.', + summary: this.t.translate('book.reviews.toast.allDeletedSummary'), + detail: this.t.translate('book.reviews.toast.allDeletedDetail'), life: 3000 }); }, @@ -154,8 +156,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { console.error('Failed to delete all reviews:', error); this.messageService.add({ severity: 'error', - summary: 'Delete Failed', - detail: 'Could not delete all reviews.', + summary: this.t.translate('book.reviews.toast.deleteAllFailedSummary'), + detail: this.t.translate('book.reviews.toast.deleteAllFailedDetail'), life: 3000 }); } @@ -200,13 +202,10 @@ export class BookReviewsComponent implements OnInit, OnChanges { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { - const action = newLockState ? 'locked' : 'unlocked'; this.messageService.add({ severity: 'info', - summary: `Reviews ${action.charAt(0).toUpperCase() + action.slice(1)}`, - detail: newLockState - ? 'Reviews are now protected from modifications and refreshes.' - : 'Reviews can now be modified and refreshed.', + summary: this.t.translate(newLockState ? 'book.reviews.toast.lockedSummary' : 'book.reviews.toast.unlockedSummary'), + detail: this.t.translate(newLockState ? 'book.reviews.toast.lockedDetail' : 'book.reviews.toast.unlockedDetail'), life: 3000 }); }, @@ -214,8 +213,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { console.error('Failed to toggle lock status:', error); this.messageService.add({ severity: 'error', - summary: 'Lock Toggle Failed', - detail: 'Could not change the lock status for reviews.', + summary: this.t.translate('book.reviews.toast.lockFailedSummary'), + detail: this.t.translate('book.reviews.toast.lockFailedDetail'), life: 3000 }); } @@ -255,8 +254,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { if (!review.id || this.reviewsLocked) return; this.confirmationService.confirm({ - message: `Are you sure you want to delete this review by ${review.reviewerName || 'Anonymous'}?`, - header: 'Confirm Deletion', + message: this.t.translate('book.reviews.confirm.deleteMessage', {reviewer: review.reviewerName || 'Anonymous'}), + header: this.t.translate('book.reviews.confirm.deleteHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', @@ -268,8 +267,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { this.updateSpoilerState(); this.messageService.add({ severity: 'success', - summary: 'Review Deleted', - detail: 'The review has been successfully deleted.', + summary: this.t.translate('book.reviews.toast.deleteSuccessSummary'), + detail: this.t.translate('book.reviews.toast.deleteSuccessDetail'), life: 2000 }); }, @@ -277,8 +276,8 @@ export class BookReviewsComponent implements OnInit, OnChanges { console.error('Failed to delete review:', error); this.messageService.add({ severity: 'error', - summary: 'Delete Failed', - detail: 'Could not delete the review.', + summary: this.t.translate('book.reviews.toast.deleteFailedSummary'), + detail: this.t.translate('book.reviews.toast.deleteFailedDetail'), life: 3000 }); } diff --git a/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.html b/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.html index b53dc5045..469b2adde 100644 --- a/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.html +++ b/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.html @@ -1,3 +1,4 @@ +
@@ -8,7 +9,7 @@ (input)="onSearchInputChange()" (focus)="isSearchFocused = true" (blur)="onSearchBlur()" - placeholder="Title, Author, Series, Genre, or ISBN..." + [placeholder]="t('placeholder')" class="search-input" /> @if (searchQuery) { @@ -18,7 +19,7 @@ [text]="true" [rounded]="true" class="clear-search-btn" - aria-label="Clear Search"> + [attr.aria-label]="t('clearSearch')"> } @@ -34,7 +35,7 @@
Book Cover
@@ -45,7 +46,7 @@
@if (book.metadata?.authors?.length ?? 0 > 0) { -

by {{ getAuthorNames(book.metadata?.authors) }}

+

{{ t('byPrefix') }} {{ getAuthorNames(book.metadata?.authors) }}

} @if (getPublishedYear(book.metadata?.publishedDate)) { @@ -64,9 +65,10 @@ } } @else {
- No results found + {{ t('noResults') }}
}
}
+ diff --git a/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.ts b/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.ts index 0cb7d30a9..bbc2e029e 100644 --- a/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.ts +++ b/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.ts @@ -13,6 +13,7 @@ import {Router} from '@angular/router'; import {IconField} from 'primeng/iconfield'; import {InputIcon} from 'primeng/inputicon'; import {HeaderFilter} from '../book-browser/filters/HeaderFilter'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-book-searcher', @@ -24,7 +25,8 @@ import {HeaderFilter} from '../book-browser/filters/HeaderFilter'; SlicePipe, Divider, IconField, - InputIcon + InputIcon, + TranslocoDirective, ], styleUrls: ['./book-searcher.component.scss'], standalone: true @@ -39,6 +41,7 @@ export class BookSearcherComponent implements OnInit, OnDestroy { private bookService = inject(BookService); private router = inject(Router); protected urlHelper = inject(UrlHelperService); + private readonly t = inject(TranslocoService); private headerFilter = new HeaderFilter(this.#searchSubject.asObservable()); ngOnInit(): void { @@ -56,7 +59,7 @@ export class BookSearcherComponent implements OnInit, OnDestroy { } getAuthorNames(authors: string[] | undefined): string { - return authors?.join(', ') || 'Unknown Author'; + return authors?.join(', ') || this.t.translate('book.searcher.unknownAuthor'); } getPublishedYear(publishedDate: string | undefined): string | null { diff --git a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html index ef56de05f..c924ba91d 100644 --- a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html +++ b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.html @@ -1,11 +1,12 @@ +
-

Send Book

-

Email this book to a recipient

+

{{ t('title') }}

+

{{ t('description') }}

- + @@ -32,11 +33,11 @@
- + 0) {
- +
@for (file of emailableFiles; track file.id) {
- {{ file.bookType || 'Unknown' }} - @if (file.isPrimary) { Primary } + {{ file.bookType || t('unknownFormat') }} + @if (file.isPrimary) { {{ t('primaryBadge') }} } {{ formatFileSize(file.fileSizeKb) }}
@@ -65,7 +66,7 @@ @if (showLargeFileWarning) {
- This file exceeds 25MB. Some email providers may reject large attachments. + {{ t('largeFileWarning') }}
}
@@ -74,9 +75,10 @@
+ diff --git a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts index 91df8c701..350b8d39e 100644 --- a/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts +++ b/booklore-ui/src/app/features/book/components/book-sender/book-sender.component.ts @@ -11,6 +11,7 @@ import {EmailV2ProviderService} from '../../../settings/email-v2/email-v2-provid import {EmailV2RecipientService} from '../../../settings/email-v2/email-v2-recipient/email-v2-recipient.service'; import {Book, BookFile} from '../../model/book.model'; import {RadioButton} from 'primeng/radiobutton'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; interface EmailableFile { id: number; @@ -27,7 +28,8 @@ const LARGE_FILE_THRESHOLD_KB = 25 * 1024; // 25MB Button, Select, FormsModule, - RadioButton + RadioButton, + TranslocoDirective, ], templateUrl: './book-sender.component.html', styleUrls: ['./book-sender.component.scss'] @@ -38,6 +40,7 @@ export class BookSenderComponent implements OnInit { private emailRecipientService = inject(EmailV2RecipientService); private emailService = inject(EmailService); private messageService = inject(MessageService); + private readonly t = inject(TranslocoService); dynamicDialogRef = inject(DynamicDialogRef); private dynamicDialogConfig = inject(DynamicDialogConfig); @@ -143,16 +146,16 @@ export class BookSenderComponent implements OnInit { next: () => { this.messageService.add({ severity: 'success', - summary: 'Email Scheduled', - detail: 'The book has been successfully scheduled for sending.' + summary: this.t.translate('book.sender.toast.emailScheduledSummary'), + detail: this.t.translate('book.sender.toast.emailScheduledDetail') }); this.dynamicDialogRef.close(true); }, error: (error) => { this.messageService.add({ severity: 'error', - summary: 'Sending Failed', - detail: 'There was an issue while scheduling the book for sending. Please try again later.' + summary: this.t.translate('book.sender.toast.sendingFailedSummary'), + detail: this.t.translate('book.sender.toast.sendingFailedDetail') }); console.error('Error sending book:', error); } @@ -161,22 +164,22 @@ export class BookSenderComponent implements OnInit { if (!this.selectedProvider) { this.messageService.add({ severity: 'error', - summary: 'Email Provider Missing', - detail: 'Please select an email provider to proceed.' + summary: this.t.translate('book.sender.toast.providerMissingSummary'), + detail: this.t.translate('book.sender.toast.providerMissingDetail') }); } if (!this.selectedRecipient) { this.messageService.add({ severity: 'error', - summary: 'Recipient Missing', - detail: 'Please select a recipient to send the book.' + summary: this.t.translate('book.sender.toast.recipientMissingSummary'), + detail: this.t.translate('book.sender.toast.recipientMissingDetail') }); } if (!this.book?.id) { this.messageService.add({ severity: 'error', - summary: 'Book Not Selected', - detail: 'Please select a book to send.' + summary: this.t.translate('book.sender.toast.bookNotSelectedSummary'), + detail: this.t.translate('book.sender.toast.bookNotSelectedDetail') }); } } diff --git a/booklore-ui/src/app/features/book/components/series-page/series-page.component.html b/booklore-ui/src/app/features/book/components/series-page/series-page.component.html index d3ed85f85..893fb5796 100644 --- a/booklore-ui/src/app/features/book/components/series-page/series-page.component.html +++ b/booklore-ui/src/app/features/book/components/series-page/series-page.component.html @@ -1,10 +1,11 @@ + @if (filteredBooks$ | async; as books) {
- Series Details + {{ t('seriesDetailsTab') }} @@ -49,7 +50,7 @@ } @if (books.length === 0) { -
No books found for this series.
+
{{ t('noBooksFound') }}
}
@@ -129,7 +130,7 @@
{{ selectedBooks.size }} - selected + {{ t('selected') }}
} + diff --git a/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts b/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts index cd02bf4a2..cf8670ca2 100644 --- a/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts +++ b/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts @@ -22,6 +22,7 @@ import {TaskHelperService} from "../../../settings/task-management/task-helper.s import {MetadataRefreshType} from "../../../metadata/model/request/metadata-refresh-type.enum"; import {TieredMenu} from "primeng/tieredmenu"; import {AppSettingsService} from "../../../../shared/service/app-settings.service"; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; import {Tooltip} from "primeng/tooltip"; import {Divider} from "primeng/divider"; import {animate, style, transition, trigger} from "@angular/animations"; @@ -50,7 +51,8 @@ import {BookCardOverlayPreferenceService} from '../book-browser/book-card-overla VirtualScrollerModule, TieredMenu, Tooltip, - Divider + Divider, + TranslocoDirective ], animations: [ trigger('slideInOut', [ @@ -82,6 +84,7 @@ export class SeriesPageComponent implements OnDestroy { private messageService = inject(MessageService); protected bookCardOverlayPreferenceService = inject(BookCardOverlayPreferenceService); protected appSettingsService = inject(AppSettingsService); + private readonly t = inject(TranslocoService); tab: string = "view"; isExpanded = false; @@ -271,23 +274,23 @@ export class SeriesPageComponent implements OnDestroy { const v = (value ?? '').toString().toUpperCase(); switch (v) { case ReadStatus.UNREAD: - return 'UNREAD'; + return this.t.translate('book.seriesPage.status.unread'); case ReadStatus.READING: - return 'READING'; + return this.t.translate('book.seriesPage.status.reading'); case ReadStatus.RE_READING: - return 'RE-READING'; + return this.t.translate('book.seriesPage.status.reReading'); case ReadStatus.READ: - return 'READ'; + return this.t.translate('book.seriesPage.status.read'); case ReadStatus.PARTIALLY_READ: - return 'PARTIALLY READ'; + return this.t.translate('book.seriesPage.status.partiallyRead'); case ReadStatus.PAUSED: - return 'PAUSED'; + return this.t.translate('book.seriesPage.status.paused'); case ReadStatus.ABANDONED: - return 'ABANDONED'; + return this.t.translate('book.seriesPage.status.abandoned'); case ReadStatus.WONT_READ: - return "WON'T READ"; + return this.t.translate('book.seriesPage.status.wontRead'); default: - return 'UNSET'; + return this.t.translate('book.seriesPage.status.unset'); } } @@ -374,18 +377,18 @@ export class SeriesPageComponent implements OnDestroy { confirmDeleteBooks(): void { this.confirmationService.confirm({ - message: `Are you sure you want to delete ${this.selectedBooks.size} book(s)?\n\nThis will permanently remove the book files from your filesystem.\n\nThis action cannot be undone.`, - header: 'Confirm Deletion', + message: this.t.translate('book.browser.confirm.deleteMessage', {count: this.selectedBooks.size}), + header: this.t.translate('book.browser.confirm.deleteHeader'), icon: 'pi pi-exclamation-triangle', acceptIcon: 'pi pi-trash', rejectIcon: 'pi pi-times', - acceptLabel: 'Delete', - rejectLabel: 'Cancel', + acceptLabel: this.t.translate('common.delete'), + rejectLabel: this.t.translate('common.cancel'), acceptButtonStyleClass: 'p-button-danger', rejectButtonStyleClass: 'p-button-outlined', accept: () => { const count = this.selectedBooks.size; - const loader = this.loadingService.show(`Deleting ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.browser.loading.deleting', {count})); this.bookService.deleteBooks(this.selectedBooks) .pipe(finalize(() => this.loadingService.hide(loader))) @@ -437,17 +440,17 @@ export class SeriesPageComponent implements OnDestroy { if (!this.selectedBooks || this.selectedBooks.size === 0) return; const count = this.selectedBooks.size; this.confirmationService.confirm({ - message: `Are you sure you want to regenerate covers for ${count} book(s)?`, - header: 'Confirm Cover Regeneration', + message: this.t.translate('book.browser.confirm.regenCoverMessage', {count}), + header: this.t.translate('book.browser.confirm.regenCoverHeader'), icon: 'pi pi-image', acceptLabel: 'Yes', rejectLabel: 'No', acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'success' }, rejectButtonProps: { - label: 'No', + label: this.t.translate('common.no'), severity: 'secondary' }, accept: () => { @@ -455,16 +458,16 @@ export class SeriesPageComponent implements OnDestroy { next: () => { this.messageService.add({ severity: 'success', - summary: 'Cover Regeneration Started', - detail: `Regenerating covers for ${count} book(s). Refresh the page when complete.`, + summary: this.t.translate('book.browser.toast.regenCoverStartedSummary'), + detail: this.t.translate('book.browser.toast.regenCoverStartedDetail', {count}), life: 3000 }); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Could not start cover regeneration.', + summary: this.t.translate('book.browser.toast.failedSummary'), + detail: this.t.translate('book.browser.toast.regenCoverFailedDetail'), life: 3000 }); } @@ -477,17 +480,17 @@ export class SeriesPageComponent implements OnDestroy { if (!this.selectedBooks || this.selectedBooks.size === 0) return; const count = this.selectedBooks.size; this.confirmationService.confirm({ - message: `Are you sure you want to generate custom covers for ${count} book(s)?`, - header: 'Confirm Custom Cover Generation', + message: this.t.translate('book.browser.confirm.customCoverMessage', {count}), + header: this.t.translate('book.browser.confirm.customCoverHeader'), icon: 'pi pi-palette', acceptLabel: 'Yes', rejectLabel: 'No', acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'success' }, rejectButtonProps: { - label: 'No', + label: this.t.translate('common.no'), severity: 'secondary' }, accept: () => { @@ -495,16 +498,16 @@ export class SeriesPageComponent implements OnDestroy { next: () => { this.messageService.add({ severity: 'success', - summary: 'Custom Cover Generation Started', - detail: `Generating custom covers for ${count} book(s).`, + summary: this.t.translate('book.browser.toast.customCoverStartedSummary'), + detail: this.t.translate('book.browser.toast.customCoverStartedDetail', {count}), life: 3000 }); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Could not start custom cover generation.', + summary: this.t.translate('book.browser.toast.failedSummary'), + detail: this.t.translate('book.browser.toast.customCoverFailedDetail'), life: 3000 }); } diff --git a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html index da805c3b2..8ae9beef0 100644 --- a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html +++ b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html @@ -1,15 +1,16 @@ +
-

Assign Books to Shelves

+

{{ t('title') }}

@if (isMultiBooks) { - Select shelves for {{ bookIds.size }} {{ bookIds.size === 1 ? 'book' : 'books' }} + {{ t('descriptionMulti', { count: bookIds.size }) }} } @else { - Organize "{{ book.metadata?.title }}" into your shelves + {{ t('descriptionSingle', { title: book.metadata?.title }) }} }

@@ -29,7 +30,7 @@
- {{ (shelfState$ | async)!.shelves!.length }} {{ (shelfState$ | async)!.shelves!.length === 1 ? 'shelf' : 'shelves' }} available + {{ t('shelvesAvailable', { count: (shelfState$ | async)!.shelves!.length }) }}
@@ -68,18 +69,15 @@
-

No Shelves Available

-

- Create your first shelf to start organizing your books.
- Click the button below to get started. -

+

{{ t('emptyTitle') }}

+

}
+
diff --git a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts index 3c3049b4e..b211f14a7 100644 --- a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts +++ b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts @@ -17,6 +17,7 @@ import {LoadingService} from '../../../../core/services/loading.service'; import {UserService} from '../../../settings/user-management/user.service'; import {IconDisplayComponent} from '../../../../shared/components/icon-display/icon-display.component'; import {IconSelection} from '../../../../shared/service/icon-picker.service'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-shelf-assigner', @@ -28,7 +29,8 @@ import {IconSelection} from '../../../../shared/service/icon-picker.service'; Checkbox, AsyncPipe, FormsModule, - IconDisplayComponent + IconDisplayComponent, + TranslocoDirective ] }) export class ShelfAssignerComponent implements OnInit { @@ -41,6 +43,7 @@ export class ShelfAssignerComponent implements OnInit { private bookDialogHelper = inject(BookDialogHelperService); private loadingService = inject(LoadingService); private userService = inject(UserService); + private readonly t = inject(TranslocoService); shelfState$: Observable = combineLatest([ this.shelfService.shelfState$, @@ -78,17 +81,17 @@ export class ShelfAssignerComponent implements OnInit { } private updateBookShelves(bookIds: Set, idsToAssign: Set, idsToUnassign: Set): void { - const loader = this.loadingService.show(`Updating shelves for ${bookIds.size} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.shelfAssigner.loading.updatingShelves', { count: bookIds.size })); this.bookService.updateBookShelves(bookIds, idsToAssign, idsToUnassign) .pipe(finalize(() => this.loadingService.hide(loader))) .subscribe({ next: () => { - this.messageService.add({severity: 'info', summary: 'Success', detail: 'Book shelves updated'}); + this.messageService.add({severity: 'info', summary: this.t.translate('common.success'), detail: this.t.translate('book.shelfAssigner.toast.updateSuccessDetail')}); this.dynamicDialogRef.close({assigned: true}); }, error: () => { - this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to update book shelves'}); + this.messageService.add({severity: 'error', summary: this.t.translate('common.error'), detail: this.t.translate('book.shelfAssigner.toast.updateFailedDetail')}); this.dynamicDialogRef.close({assigned: false}); } }); diff --git a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html index 84a49d080..341e3af2b 100644 --- a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html +++ b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html @@ -1,11 +1,12 @@ +
-

Create New Shelf

-

Add a custom shelf to organize your books

+

{{ t('title') }}

+

{{ t('description') }}

@@ -22,7 +23,7 @@
@@ -32,9 +33,9 @@ pInputText [(ngModel)]="shelfName" class="input-full" - placeholder="e.g., Favorites, To Read, Currently Reading" + [placeholder]="t('shelfNamePlaceholder')" [class.filled]="shelfName.trim()" - + /> @if (shelfName.trim()) { @@ -47,14 +48,14 @@
@if (!selectedIcon) { } @else { @@ -66,7 +67,7 @@ />
- Selected Icon + {{ t('selectedIcon') }} @if (selectedIcon.type === 'PRIME_NG') { {{ selectedIcon.value }} @@ -82,7 +83,7 @@ [rounded]="true" size="small" (onClick)="clearSelectedIcon()" - pTooltip="Remove icon" + [pTooltip]="t('removeIconTooltip')" tooltipPosition="left" />
@@ -95,11 +96,11 @@
- +
} @@ -110,24 +111,24 @@ @if (!shelfName.trim()) {
- Shelf name is required + {{ t('validationRequired') }}
} @else {
- Ready to create + {{ t('validationReady') }}
}
+ diff --git a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.ts b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.ts index 48539b748..c0c46ee25 100644 --- a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.ts +++ b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.ts @@ -11,6 +11,7 @@ import {Tooltip} from 'primeng/tooltip'; import {IconDisplayComponent} from '../../../../shared/components/icon-display/icon-display.component'; import {UserService} from '../../../settings/user-management/user.service'; import {CheckboxModule} from 'primeng/checkbox'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-shelf-creator', @@ -22,7 +23,8 @@ import {CheckboxModule} from 'primeng/checkbox'; InputText, Tooltip, IconDisplayComponent, - CheckboxModule + CheckboxModule, + TranslocoDirective ], styleUrl: './shelf-creator.component.scss', }) @@ -32,6 +34,7 @@ export class ShelfCreatorComponent { private messageService = inject(MessageService); private iconPickerService = inject(IconPickerService); private userService = inject(UserService); + private readonly t = inject(TranslocoService); shelfName: string = ''; selectedIcon: IconSelection | null = null; @@ -67,11 +70,11 @@ export class ShelfCreatorComponent { this.shelfService.createShelf(newShelf as Shelf).subscribe({ next: () => { - this.messageService.add({severity: 'info', summary: 'Success', detail: `Shelf created: ${this.shelfName}`}); + this.messageService.add({severity: 'info', summary: this.t.translate('common.success'), detail: this.t.translate('book.shelfCreator.toast.createSuccessDetail', { name: this.shelfName })}); this.dynamicDialogRef.close(true); }, error: (e) => { - this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to create shelf'}); + this.messageService.add({severity: 'error', summary: this.t.translate('common.error'), detail: this.t.translate('book.shelfCreator.toast.createFailedDetail')}); console.error('Error creating shelf:', e); } }); diff --git a/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.html b/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.html index 9f0aa3126..cb5b555e9 100644 --- a/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.html +++ b/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.html @@ -1,12 +1,13 @@ +
-

Edit Shelf

+

{{ t('title') }}

- Customize your shelf name and icon + {{ t('description') }}

- - + +
- +
@if (!selectedIcon) { - + } @if (selectedIcon) { @@ -50,10 +51,10 @@ @if (isAdmin) {
- +
- +
} @@ -64,13 +65,13 @@
+ diff --git a/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.ts b/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.ts index 21368ec27..0bfe4371f 100644 --- a/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.ts +++ b/booklore-ui/src/app/features/book/components/shelf-edit-dialog/shelf-edit-dialog.component.ts @@ -11,6 +11,7 @@ import {IconPickerService, IconSelection} from '../../../../shared/service/icon- import {IconDisplayComponent} from '../../../../shared/components/icon-display/icon-display.component'; import {CheckboxModule} from 'primeng/checkbox'; import {UserService} from '../../../settings/user-management/user.service'; +import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; @Component({ selector: 'app-shelf-edit-dialog', @@ -20,7 +21,8 @@ import {UserService} from '../../../settings/user-management/user.service'; ReactiveFormsModule, FormsModule, IconDisplayComponent, - CheckboxModule + CheckboxModule, + TranslocoDirective ], templateUrl: './shelf-edit-dialog.component.html', standalone: true, @@ -34,6 +36,7 @@ export class ShelfEditDialogComponent implements OnInit { private messageService = inject(MessageService); private iconPickerService = inject(IconPickerService); private userService = inject(UserService); + private readonly t = inject(TranslocoService); shelfName: string = ''; selectedIcon: IconSelection | null = null; @@ -82,11 +85,11 @@ export class ShelfEditDialogComponent implements OnInit { this.shelfService.updateShelf(shelf, this.shelf?.id).subscribe({ next: () => { - this.messageService.add({severity: 'success', summary: 'Shelf Updated', detail: 'The shelf was updated successfully.'}); + this.messageService.add({severity: 'success', summary: this.t.translate('book.shelfEditDialog.toast.updateSuccessSummary'), detail: this.t.translate('book.shelfEditDialog.toast.updateSuccessDetail')}); this.dynamicDialogRef.close(); }, error: (e) => { - this.messageService.add({severity: 'error', summary: 'Update Failed', detail: 'An error occurred while updating the shelf. Please try again.'}); + this.messageService.add({severity: 'error', summary: this.t.translate('book.shelfEditDialog.toast.updateFailedSummary'), detail: this.t.translate('book.shelfEditDialog.toast.updateFailedDetail')}); console.error(e); } }); diff --git a/booklore-ui/src/app/features/book/service/book-menu.service.ts b/booklore-ui/src/app/features/book/service/book-menu.service.ts index b8318b5e4..7d75ea059 100644 --- a/booklore-ui/src/app/features/book/service/book-menu.service.ts +++ b/booklore-ui/src/app/features/book/service/book-menu.service.ts @@ -9,6 +9,7 @@ import {LoadingService} from '../../../core/services/loading.service'; import {User} from '../../settings/user-management/user.service'; import {APIException} from '../../../shared/models/api-exception.model'; import {HttpErrorResponse} from '@angular/common/http'; +import {TranslocoService} from '@jsverse/transloco'; @Injectable({ providedIn: 'root' @@ -19,6 +20,7 @@ export class BookMenuService { messageService = inject(MessageService); bookService = inject(BookService); loadingService = inject(LoadingService); + private readonly t = inject(TranslocoService); getMetadataMenuItems( autoFetchMetadata: () => void, @@ -34,7 +36,7 @@ export class BookMenuService { if (permissions?.canBulkAutoFetchMetadata) { items.push({ - label: 'Auto Fetch Metadata', + label: this.t.translate('book.menuService.menu.autoFetchMetadata'), icon: 'pi pi-bolt', command: autoFetchMetadata }); @@ -42,7 +44,7 @@ export class BookMenuService { if (permissions?.canBulkCustomFetchMetadata) { items.push({ - label: 'Custom Fetch Metadata', + label: this.t.translate('book.menuService.menu.customFetchMetadata'), icon: 'pi pi-sync', command: fetchMetadata }); @@ -50,12 +52,12 @@ export class BookMenuService { if (permissions?.canBulkEditMetadata) { items.push({ - label: 'Bulk Metadata Editor', + label: this.t.translate('book.menuService.menu.bulkMetadataEditor'), icon: 'pi pi-table', command: bulkEditMetadata }); items.push({ - label: 'Multi-Book Metadata Editor', + label: this.t.translate('book.menuService.menu.multiBookMetadataEditor'), icon: 'pi pi-clone', command: multiBookEditMetadata }); @@ -63,12 +65,12 @@ export class BookMenuService { if (permissions?.canBulkRegenerateCover) { items.push({ - label: 'Regenerate Covers', + label: this.t.translate('book.menuService.menu.regenerateCovers'), icon: 'pi pi-image', command: regenerateCovers }); items.push({ - label: 'Generate Custom Covers', + label: this.t.translate('book.menuService.menu.generateCustomCovers'), icon: 'pi pi-palette', command: generateCustomCovers }); @@ -84,27 +86,27 @@ export class BookMenuService { if (permissions?.canBulkResetBookReadStatus) { items.push({ - label: 'Update Read Status', + label: this.t.translate('book.menuService.menu.updateReadStatus'), icon: 'pi pi-book', items: Object.entries(readStatusLabels).map(([status, label]) => ({ label, command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to mark ${count} book(s) as "${label}"?`, - header: 'Confirm Read Status Update', + message: this.t.translate('book.menuService.confirm.readStatusMessage', {count, label}), + header: this.t.translate('book.menuService.confirm.readStatusHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'success' }, rejectButtonProps: { - label: 'No', + label: this.t.translate('common.no'), severity: 'secondary' }, accept: () => { - const loader = this.loadingService.show(`Updating read status for ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.updatingReadStatus', {count})); this.bookService.updateBookReadStatus(Array.from(selectedBooks), status as ReadStatus) .pipe(finalize(() => this.loadingService.hide(loader))) @@ -112,8 +114,8 @@ export class BookMenuService { next: () => { this.messageService.add({ severity: 'success', - summary: 'Read Status Updated', - detail: `Marked as "${label}"`, + summary: this.t.translate('book.menuService.toast.readStatusUpdatedSummary'), + detail: this.t.translate('book.menuService.toast.readStatusUpdatedDetail', {label}), life: 2000 }); }, @@ -121,8 +123,8 @@ export class BookMenuService { const apiError = err.error as APIException; this.messageService.add({ severity: 'error', - summary: 'Update Failed', - detail: apiError?.message || 'Could not update read status.', + summary: this.t.translate('book.menuService.toast.updateFailedSummary'), + detail: apiError?.message || this.t.translate('book.menuService.toast.readStatusFailedDetail'), life: 3000 }); } @@ -136,20 +138,20 @@ export class BookMenuService { if (permissions?.canBulkEditMetadata) { items.push({ - label: 'Set Age Rating', + label: this.t.translate('book.menuService.menu.setAgeRating'), icon: 'pi pi-user', items: [ ...AGE_RATING_OPTIONS.map(option => ({ label: option.label, command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to set the age rating to "${option.label}" for ${count} book(s)?`, - header: 'Confirm Age Rating Update', + message: this.t.translate('book.menuService.confirm.ageRatingMessage', {label: option.label, count}), + header: this.t.translate('book.menuService.confirm.ageRatingHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), accept: () => { - const loader = this.loadingService.show(`Setting age rating for ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.settingAgeRating', {count})); this.bookService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), ageRating: option.id @@ -158,8 +160,8 @@ export class BookMenuService { next: () => { this.messageService.add({ severity: 'success', - summary: 'Age Rating Updated', - detail: `Set to "${option.label}"`, + summary: this.t.translate('book.menuService.toast.ageRatingUpdatedSummary'), + detail: this.t.translate('book.menuService.toast.ageRatingUpdatedDetail', {label: option.label}), life: 2000 }); }, @@ -167,8 +169,8 @@ export class BookMenuService { const apiError = err.error as APIException; this.messageService.add({ severity: 'error', - summary: 'Update Failed', - detail: apiError?.message || 'Could not update age rating.', + summary: this.t.translate('book.menuService.toast.updateFailedSummary'), + detail: apiError?.message || this.t.translate('book.menuService.toast.ageRatingFailedDetail'), life: 3000 }); } @@ -181,17 +183,17 @@ export class BookMenuService { separator: true }, { - label: 'Clear Age Rating', + label: this.t.translate('book.menuService.menu.clearAgeRating'), icon: 'pi pi-times', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to clear the age rating for ${count} book(s)?`, - header: 'Confirm Clear Age Rating', + message: this.t.translate('book.menuService.confirm.clearAgeRatingMessage', {count}), + header: this.t.translate('book.menuService.confirm.clearAgeRatingHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), accept: () => { - const loader = this.loadingService.show(`Clearing age rating for ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.clearingAgeRating', {count})); this.bookService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), clearAgeRating: true @@ -200,8 +202,8 @@ export class BookMenuService { next: () => { this.messageService.add({ severity: 'success', - summary: 'Age Rating Cleared', - detail: 'Age rating has been cleared.', + summary: this.t.translate('book.menuService.toast.ageRatingClearedSummary'), + detail: this.t.translate('book.menuService.toast.ageRatingClearedDetail'), life: 2000 }); }, @@ -209,8 +211,8 @@ export class BookMenuService { const apiError = err.error as APIException; this.messageService.add({ severity: 'error', - summary: 'Update Failed', - detail: apiError?.message || 'Could not clear age rating.', + summary: this.t.translate('book.menuService.toast.updateFailedSummary'), + detail: apiError?.message || this.t.translate('book.menuService.toast.clearAgeRatingFailedDetail'), life: 3000 }); } @@ -223,20 +225,20 @@ export class BookMenuService { }); items.push({ - label: 'Set Content Rating', + label: this.t.translate('book.menuService.menu.setContentRating'), icon: 'pi pi-shield', items: [ ...Object.entries(CONTENT_RATING_LABELS).map(([value, label]) => ({ label: label, command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to set the content rating to "${label}" for ${count} book(s)?`, - header: 'Confirm Content Rating Update', + message: this.t.translate('book.menuService.confirm.contentRatingMessage', {label, count}), + header: this.t.translate('book.menuService.confirm.contentRatingHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), accept: () => { - const loader = this.loadingService.show(`Setting content rating for ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.settingContentRating', {count})); this.bookService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), contentRating: value @@ -245,8 +247,8 @@ export class BookMenuService { next: () => { this.messageService.add({ severity: 'success', - summary: 'Content Rating Updated', - detail: `Set to "${label}"`, + summary: this.t.translate('book.menuService.toast.contentRatingUpdatedSummary'), + detail: this.t.translate('book.menuService.toast.contentRatingUpdatedDetail', {label}), life: 2000 }); }, @@ -254,8 +256,8 @@ export class BookMenuService { const apiError = err.error as APIException; this.messageService.add({ severity: 'error', - summary: 'Update Failed', - detail: apiError?.message || 'Could not update content rating.', + summary: this.t.translate('book.menuService.toast.updateFailedSummary'), + detail: apiError?.message || this.t.translate('book.menuService.toast.contentRatingFailedDetail'), life: 3000 }); } @@ -268,17 +270,17 @@ export class BookMenuService { separator: true }, { - label: 'Clear Content Rating', + label: this.t.translate('book.menuService.menu.clearContentRating'), icon: 'pi pi-times', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to clear the content rating for ${count} book(s)?`, - header: 'Confirm Clear Content Rating', + message: this.t.translate('book.menuService.confirm.clearContentRatingMessage', {count}), + header: this.t.translate('book.menuService.confirm.clearContentRatingHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), accept: () => { - const loader = this.loadingService.show(`Clearing content rating for ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.clearingContentRating', {count})); this.bookService.updateBooksMetadata({ bookIds: Array.from(selectedBooks), clearContentRating: true @@ -287,8 +289,8 @@ export class BookMenuService { next: () => { this.messageService.add({ severity: 'success', - summary: 'Content Rating Cleared', - detail: 'Content rating has been cleared.', + summary: this.t.translate('book.menuService.toast.contentRatingClearedSummary'), + detail: this.t.translate('book.menuService.toast.contentRatingClearedDetail'), life: 2000 }); }, @@ -296,8 +298,8 @@ export class BookMenuService { const apiError = err.error as APIException; this.messageService.add({ severity: 'error', - summary: 'Update Failed', - detail: apiError?.message || 'Could not clear content rating.', + summary: this.t.translate('book.menuService.toast.updateFailedSummary'), + detail: apiError?.message || this.t.translate('book.menuService.toast.clearContentRatingFailedDetail'), life: 3000 }); } @@ -313,17 +315,17 @@ export class BookMenuService { // Shelf Actions if (permissions?.canManageLibrary || permissions?.admin) { // Assuming these permissions cover shelf management for books items.push({ - label: 'Remove from all shelves', + label: this.t.translate('book.menuService.menu.removeFromAllShelves'), icon: 'pi pi-bookmark-fill', // Or bookmark-slash command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to remove ${count} book(s) from ALL their shelves?`, - header: 'Confirm Unshelve', + message: this.t.translate('book.menuService.confirm.unshelveMessage', {count}), + header: this.t.translate('book.menuService.confirm.unshelveHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), accept: () => { - const loader = this.loadingService.show(`Removing ${count} book(s) from shelves...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.removingFromShelves', {count})); const books = this.bookService.getBooksByIdsFromState(Array.from(selectedBooks)); const allShelfIds = new Set(); books.forEach(b => b.shelves?.forEach(s => { @@ -332,7 +334,7 @@ export class BookMenuService { if (allShelfIds.size === 0) { this.loadingService.hide(loader); - this.messageService.add({ severity: 'info', summary: 'Info', detail: 'Selected books are not on any shelves.' }); + this.messageService.add({ severity: 'info', summary: 'Info', detail: this.t.translate('book.menuService.toast.noBooksOnShelvesDetail') }); return; } @@ -340,10 +342,10 @@ export class BookMenuService { .pipe(finalize(() => this.loadingService.hide(loader))) .subscribe({ next: () => { - this.messageService.add({severity: 'success', summary: 'Success', detail: 'Books removed from all shelves'}); + this.messageService.add({severity: 'success', summary: this.t.translate('common.success'), detail: this.t.translate('book.menuService.toast.unshelveSuccessDetail')}); }, error: () => { - this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to update books shelves'}); + this.messageService.add({severity: 'error', summary: this.t.translate('common.error'), detail: this.t.translate('book.menuService.toast.unshelveFailedDetail')}); } }); } @@ -354,17 +356,17 @@ export class BookMenuService { if (permissions?.canBulkResetBookloreReadProgress) { items.push({ - label: 'Reset Booklore Progress', + label: this.t.translate('book.menuService.menu.resetBookloreProgress'), icon: 'pi pi-undo', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to reset Booklore reading progress for ${count} book(s)?`, - header: 'Confirm Reset', + message: this.t.translate('book.menuService.confirm.resetBookloreMessage', {count}), + header: this.t.translate('book.menuService.confirm.resetHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), accept: () => { - const loader = this.loadingService.show(`Resetting Booklore progress for ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.resettingBookloreProgress', {count})); this.bookService.resetProgress(Array.from(selectedBooks), ResetProgressTypes.BOOKLORE) .pipe(finalize(() => this.loadingService.hide(loader))) @@ -372,8 +374,8 @@ export class BookMenuService { next: () => { this.messageService.add({ severity: 'success', - summary: 'Progress Reset', - detail: 'Booklore reading progress has been reset.', + summary: this.t.translate('book.menuService.toast.progressResetSummary'), + detail: this.t.translate('book.menuService.toast.bookloreProgressResetDetail'), life: 1500 }); }, @@ -381,8 +383,8 @@ export class BookMenuService { const apiError = err.error as APIException; this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: apiError?.message || 'Could not reset progress.', + summary: this.t.translate('book.menuService.toast.failedSummary'), + detail: apiError?.message || this.t.translate('book.menuService.toast.progressResetFailedDetail'), life: 3000 }); } @@ -395,17 +397,17 @@ export class BookMenuService { if (permissions?.canBulkResetKoReaderReadProgress) { items.push({ - label: 'Reset KOReader Progress', + label: this.t.translate('book.menuService.menu.resetKOReaderProgress'), icon: 'pi pi-undo', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to reset KOReader reading progress for ${count} book(s)?`, - header: 'Confirm Reset', + message: this.t.translate('book.menuService.confirm.resetKOReaderMessage', {count}), + header: this.t.translate('book.menuService.confirm.resetHeader'), icon: 'pi pi-exclamation-triangle', - acceptLabel: 'Yes', - rejectLabel: 'No', + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.no'), accept: () => { - const loader = this.loadingService.show(`Resetting KOReader progress for ${count} book(s)...`); + const loader = this.loadingService.show(this.t.translate('book.menuService.loading.resettingKOReaderProgress', {count})); this.bookService.resetProgress(Array.from(selectedBooks), ResetProgressTypes.KOREADER) .pipe(finalize(() => this.loadingService.hide(loader))) @@ -413,8 +415,8 @@ export class BookMenuService { next: () => { this.messageService.add({ severity: 'success', - summary: 'Progress Reset', - detail: 'KOReader reading progress has been reset.', + summary: this.t.translate('book.menuService.toast.progressResetSummary'), + detail: this.t.translate('book.menuService.toast.koreaderProgressResetDetail'), life: 1500 }); }, @@ -422,8 +424,8 @@ export class BookMenuService { const apiError = err.error as APIException; this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: apiError?.message || 'Could not reset progress.', + summary: this.t.translate('book.menuService.toast.failedSummary'), + detail: apiError?.message || this.t.translate('book.menuService.toast.progressResetFailedDetail'), life: 3000 }); } diff --git a/booklore-ui/src/app/features/book/service/book.service.ts b/booklore-ui/src/app/features/book/service/book.service.ts index 1f524d67b..8b7ddd400 100644 --- a/booklore-ui/src/app/features/book/service/book.service.ts +++ b/booklore-ui/src/app/features/book/service/book.service.ts @@ -13,6 +13,7 @@ import {Router} from '@angular/router'; import {BookStateService} from './book-state.service'; import {BookSocketService} from './book-socket.service'; import {BookPatchService} from './book-patch.service'; +import {TranslocoService} from '@jsverse/transloco'; export interface BookStatusUpdateResponse { bookId: number; @@ -41,6 +42,7 @@ export class BookService { private bookStateService = inject(BookStateService); private bookSocketService = inject(BookSocketService); private bookPatchService = inject(BookPatchService); + private readonly t = inject(TranslocoService); private loading$: Observable | null = null; @@ -210,22 +212,22 @@ export class BookService { if (response.failedFileDeletions?.length > 0) { this.messageService.add({ severity: 'warn', - summary: 'Some files could not be deleted', - detail: `Books: ${response.failedFileDeletions.join(', ')}`, + summary: this.t.translate('book.bookService.toast.someFilesNotDeletedSummary'), + detail: this.t.translate('book.bookService.toast.someFilesNotDeletedDetail', {fileNames: response.failedFileDeletions.join(', ')}), }); } else { this.messageService.add({ severity: 'success', - summary: 'Books Deleted', - detail: `${idList.length} book(s) deleted successfully.`, + summary: this.t.translate('book.bookService.toast.booksDeletedSummary'), + detail: this.t.translate('book.bookService.toast.booksDeletedDetail', {count: idList.length}), }); } }), catchError(error => { this.messageService.add({ severity: 'error', - summary: 'Delete Failed', - detail: error?.error?.message || error?.message || 'An error occurred while deleting books.', + summary: this.t.translate('book.bookService.toast.deleteFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.deleteFailedDetail'), }); return throwError(() => error); }) @@ -253,15 +255,15 @@ export class BookService { }); this.messageService.add({ severity: 'success', - summary: 'Physical Book Created', - detail: `"${newBook.metadata?.title || 'Book'}" has been added to your library.` + summary: this.t.translate('book.bookService.toast.physicalBookCreatedSummary'), + detail: this.t.translate('book.bookService.toast.physicalBookCreatedDetail', {title: newBook.metadata?.title || 'Book'}) }); }), catchError(error => { this.messageService.add({ severity: 'error', - summary: 'Creation Failed', - detail: error?.error?.message || error?.message || 'An error occurred while creating the physical book.' + summary: this.t.translate('book.bookService.toast.creationFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.creationFailedDetail') }); return throwError(() => error); }) @@ -384,15 +386,15 @@ export class BookService { this.messageService.add({ severity: 'success', - summary: 'File Deleted', - detail: 'Additional file deleted successfully.' + summary: this.t.translate('book.bookService.toast.fileDeletedSummary'), + detail: this.t.translate('book.bookService.toast.additionalFileDeletedDetail') }); }), catchError(error => { this.messageService.add({ severity: 'error', - summary: 'Delete Failed', - detail: error?.error?.message || error?.message || 'An error occurred while deleting the file.' + summary: this.t.translate('book.bookService.toast.fileDeleteFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.fileDeleteFailedDetail') }); return throwError(() => error); }) @@ -441,15 +443,15 @@ export class BookService { this.messageService.add({ severity: 'success', - summary: 'File Deleted', - detail: 'Book file deleted successfully.' + summary: this.t.translate('book.bookService.toast.fileDeletedSummary'), + detail: this.t.translate('book.bookService.toast.bookFileDeletedDetail') }); }), catchError(error => { this.messageService.add({ severity: 'error', - summary: 'Delete Failed', - detail: error?.error?.message || error?.message || 'An error occurred while deleting the file.' + summary: this.t.translate('book.bookService.toast.fileDeleteFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.fileDeleteFailedDetail') }); return throwError(() => error); }) @@ -507,15 +509,15 @@ export class BookService { this.messageService.add({ severity: 'success', - summary: 'File Uploaded', - detail: 'Additional file uploaded successfully.' + summary: this.t.translate('book.bookService.toast.fileUploadedSummary'), + detail: this.t.translate('book.bookService.toast.fileUploadedDetail') }); }), catchError(error => { this.messageService.add({ severity: 'error', - summary: 'Upload Failed', - detail: error?.error?.message || error?.message || 'An error occurred while uploading the file.' + summary: this.t.translate('book.bookService.toast.uploadFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.uploadFailedDetail') }); return throwError(() => error); }) @@ -644,8 +646,8 @@ export class BookService { catchError(error => { this.messageService.add({ severity: 'error', - summary: 'Field Lock Update Failed', - detail: 'Failed to update metadata field locks. Please try again.', + summary: this.t.translate('book.bookService.toast.fieldLockFailedSummary'), + detail: this.t.translate('book.bookService.toast.fieldLockFailedDetail'), }); throw error; }) @@ -789,15 +791,15 @@ export class BookService { const fileCount = sourceBookIds.length; this.messageService.add({ severity: 'success', - summary: 'Files Attached', - detail: `${fileCount} book file${fileCount > 1 ? 's have' : ' has'} been attached successfully.` + summary: this.t.translate('book.bookService.toast.filesAttachedSummary'), + detail: this.t.translate('book.bookService.toast.filesAttachedDetail', {count: fileCount}) }); }), catchError(error => { this.messageService.add({ severity: 'error', - summary: 'Attachment Failed', - detail: error?.error?.message || error?.message || 'An error occurred while attaching the files.' + summary: this.t.translate('book.bookService.toast.attachmentFailedSummary'), + detail: error?.error?.message || error?.message || this.t.translate('book.bookService.toast.attachmentFailedDetail') }); return throwError(() => error); }) diff --git a/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts b/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts index 264711975..c6f0fc9d3 100644 --- a/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts +++ b/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts @@ -13,6 +13,7 @@ import {LoadingService} from '../../../core/services/loading.service'; import {finalize} from 'rxjs'; import {DialogLauncherService} from '../../../shared/services/dialog-launcher.service'; import {BookDialogHelperService} from '../components/book-browser/book-dialog-helper.service'; +import {TranslocoService} from '@jsverse/transloco'; @Injectable({ providedIn: 'root', @@ -30,14 +31,15 @@ export class LibraryShelfMenuService { private userService = inject(UserService); private loadingService = inject(LoadingService); private bookDialogHelperService = inject(BookDialogHelperService); + private readonly t = inject(TranslocoService); initializeLibraryMenuItems(entity: Library | Shelf | MagicShelf | null): MenuItem[] { return [ { - label: 'Options', + label: this.t.translate('book.shelfMenuService.library.optionsLabel'), items: [ { - label: 'Add Physical Book', + label: this.t.translate('book.shelfMenuService.library.addPhysicalBook'), icon: 'pi pi-book', command: () => { this.bookDialogHelperService.openAddPhysicalBookDialog(entity?.id as number); @@ -47,44 +49,44 @@ export class LibraryShelfMenuService { separator: true }, { - label: 'Edit Library', + label: this.t.translate('book.shelfMenuService.library.editLibrary'), icon: 'pi pi-pen-to-square', command: () => { this.dialogLauncherService.openLibraryEditDialog((entity?.id as number)); } }, { - label: 'Re-scan Library', + label: this.t.translate('book.shelfMenuService.library.rescanLibrary'), icon: 'pi pi-refresh', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to refresh library: ${entity?.name}?`, - header: 'Confirmation', + message: this.t.translate('book.shelfMenuService.confirm.rescanLibraryMessage', {name: entity?.name}), + header: this.t.translate('book.shelfMenuService.confirm.header'), icon: undefined, - acceptLabel: 'Rescan', - rejectLabel: 'Cancel', + acceptLabel: this.t.translate('book.shelfMenuService.confirm.rescanLabel'), + rejectLabel: this.t.translate('common.cancel'), acceptIcon: undefined, rejectIcon: undefined, acceptButtonStyleClass: undefined, rejectButtonStyleClass: undefined, rejectButtonProps: { - label: 'Cancel', + label: this.t.translate('common.cancel'), severity: 'secondary', }, acceptButtonProps: { - label: 'Rescan', + label: this.t.translate('book.shelfMenuService.confirm.rescanLabel'), severity: 'success', }, accept: () => { this.libraryService.refreshLibrary(entity?.id!).subscribe({ complete: () => { - this.messageService.add({severity: 'info', summary: 'Success', detail: 'Library refresh scheduled'}); + this.messageService.add({severity: 'info', summary: this.t.translate('common.success'), detail: this.t.translate('book.shelfMenuService.toast.libraryRefreshSuccessDetail')}); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Failed to refresh library', + summary: this.t.translate('book.shelfMenuService.toast.failedSummary'), + detail: this.t.translate('book.shelfMenuService.toast.libraryRefreshFailedDetail'), }); } }); @@ -93,14 +95,14 @@ export class LibraryShelfMenuService { } }, { - label: 'Custom Fetch Metadata', + label: this.t.translate('book.shelfMenuService.library.customFetchMetadata'), icon: 'pi pi-sync', command: () => { this.dialogLauncherService.openLibraryMetadataFetchDialog((entity?.id as number)); } }, { - label: 'Auto Fetch Metadata', + label: this.t.translate('book.shelfMenuService.library.autoFetchMetadata'), icon: 'pi pi-bolt', command: () => { this.taskHelperService.refreshMetadataTask({ @@ -113,37 +115,37 @@ export class LibraryShelfMenuService { separator: true }, { - label: 'Delete Library', + label: this.t.translate('book.shelfMenuService.library.deleteLibrary'), icon: 'pi pi-trash', command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to delete library: ${entity?.name}?`, - header: 'Confirmation', - acceptLabel: 'Yes', - rejectLabel: 'Cancel', + message: this.t.translate('book.shelfMenuService.confirm.deleteLibraryMessage', {name: entity?.name}), + header: this.t.translate('book.shelfMenuService.confirm.header'), + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.cancel'), rejectButtonProps: { - label: 'Cancel', + label: this.t.translate('common.cancel'), severity: 'secondary', }, acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'danger', }, accept: () => { - const loader = this.loadingService.show(`Deleting library '${entity?.name}'...`); + const loader = this.loadingService.show(this.t.translate('book.shelfMenuService.loading.deletingLibrary', {name: entity?.name})); this.libraryService.deleteLibrary(entity?.id!) .pipe(finalize(() => this.loadingService.hide(loader))) .subscribe({ complete: () => { this.router.navigate(['/']); - this.messageService.add({severity: 'info', summary: 'Success', detail: 'Library was deleted'}); + this.messageService.add({severity: 'info', summary: this.t.translate('common.success'), detail: this.t.translate('book.shelfMenuService.toast.libraryDeletedDetail')}); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Failed to delete library', + summary: this.t.translate('book.shelfMenuService.toast.failedSummary'), + detail: this.t.translate('book.shelfMenuService.toast.libraryDeleteFailedDetail'), }); } }); @@ -164,10 +166,10 @@ export class LibraryShelfMenuService { return [ { - label: (isPublicShelf ? 'Public Shelf - ' : '') + (disableOptions ? 'Read only' : 'Options'), + label: (isPublicShelf ? this.t.translate('book.shelfMenuService.shelf.publicShelfPrefix') : '') + (disableOptions ? this.t.translate('book.shelfMenuService.shelf.readOnly') : this.t.translate('book.shelfMenuService.shelf.optionsLabel')), items: [ { - label: 'Edit Shelf', + label: this.t.translate('book.shelfMenuService.shelf.editShelf'), icon: 'pi pi-pen-to-square', disabled: disableOptions, command: () => { @@ -178,34 +180,34 @@ export class LibraryShelfMenuService { separator: true }, { - label: 'Delete Shelf', + label: this.t.translate('book.shelfMenuService.shelf.deleteShelf'), icon: 'pi pi-trash', disabled: disableOptions, command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to delete shelf: ${entity?.name}?`, - header: 'Confirmation', - acceptLabel: 'Yes', - rejectLabel: 'Cancel', + message: this.t.translate('book.shelfMenuService.confirm.deleteShelfMessage', {name: entity?.name}), + header: this.t.translate('book.shelfMenuService.confirm.header'), + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.cancel'), acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'danger' }, rejectButtonProps: { - label: 'Cancel', + label: this.t.translate('common.cancel'), severity: 'secondary' }, accept: () => { this.shelfService.deleteShelf(entity?.id!).subscribe({ complete: () => { this.router.navigate(['/']); - this.messageService.add({severity: 'info', summary: 'Success', detail: 'Shelf was deleted'}); + this.messageService.add({severity: 'info', summary: this.t.translate('common.success'), detail: this.t.translate('book.shelfMenuService.toast.shelfDeletedDetail')}); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Failed to delete shelf', + summary: this.t.translate('book.shelfMenuService.toast.failedSummary'), + detail: this.t.translate('book.shelfMenuService.toast.shelfDeleteFailedDetail'), }); } }); @@ -225,10 +227,10 @@ export class LibraryShelfMenuService { return [ { - label: 'Options', + label: this.t.translate('book.shelfMenuService.magicShelf.optionsLabel'), items: [ { - label: 'Edit Magic Shelf', + label: this.t.translate('book.shelfMenuService.magicShelf.editMagicShelf'), icon: 'pi pi-pen-to-square', disabled: disableOptions, command: () => { @@ -239,34 +241,34 @@ export class LibraryShelfMenuService { separator: true }, { - label: 'Delete Magic Shelf', + label: this.t.translate('book.shelfMenuService.magicShelf.deleteMagicShelf'), icon: 'pi pi-trash', disabled: disableOptions, command: () => { this.confirmationService.confirm({ - message: `Are you sure you want to delete magic shelf: ${entity?.name}?`, - header: 'Confirmation', - acceptLabel: 'Yes', - rejectLabel: 'Cancel', + message: this.t.translate('book.shelfMenuService.confirm.deleteMagicShelfMessage', {name: entity?.name}), + header: this.t.translate('book.shelfMenuService.confirm.header'), + acceptLabel: this.t.translate('common.yes'), + rejectLabel: this.t.translate('common.cancel'), acceptButtonProps: { - label: 'Yes', + label: this.t.translate('common.yes'), severity: 'danger' }, rejectButtonProps: { - label: 'Cancel', + label: this.t.translate('common.cancel'), severity: 'secondary' }, accept: () => { this.magicShelfService.deleteShelf(entity?.id!).subscribe({ complete: () => { this.router.navigate(['/']); - this.messageService.add({severity: 'info', summary: 'Success', detail: 'Magic shelf was deleted'}); + this.messageService.add({severity: 'info', summary: this.t.translate('common.success'), detail: this.t.translate('book.shelfMenuService.toast.magicShelfDeletedDetail')}); }, error: () => { this.messageService.add({ severity: 'error', - summary: 'Failed', - detail: 'Failed to delete shelf', + summary: this.t.translate('book.shelfMenuService.toast.failedSummary'), + detail: this.t.translate('book.shelfMenuService.toast.magicShelfDeleteFailedDetail'), }); } }); diff --git a/booklore-ui/src/app/features/readers/audiobook-player/audiobook-player.component.html b/booklore-ui/src/app/features/readers/audiobook-player/audiobook-player.component.html index 3722b9a56..749e6f911 100644 --- a/booklore-ui/src/app/features/readers/audiobook-player/audiobook-player.component.html +++ b/booklore-ui/src/app/features/readers/audiobook-player/audiobook-player.component.html @@ -1,7 +1,8 @@ + @if (isLoading) {
- Loading audiobook... + {{ t('loading') }}
} @else {
@@ -23,12 +24,12 @@ [text]="true" severity="secondary" (onClick)="closeReader()" - pTooltip="Back" + [pTooltip]="t('header.backTooltip')" tooltipPosition="bottom" />
-

{{ audiobookInfo.title || 'Untitled' }}

- {{ audiobookInfo.author || 'Unknown Author' }} +

{{ audiobookInfo.title || t('untitled') }}

+ {{ audiobookInfo.author || t('unknownAuthor') }}
@@ -61,7 +62,7 @@ Cover } @else { @@ -81,11 +82,11 @@
@if (audiobookInfo.folderBased && currentTrack) { {{ currentTrack.title }} - Track {{ currentTrackIndex + 1 }} of {{ audiobookInfo.tracks?.length }} + {{ t('trackInfo.trackOf', { current: currentTrackIndex + 1, total: audiobookInfo.tracks?.length }) }} } @else if (getCurrentChapter()) { {{ getCurrentChapter()?.title }} @if (hasMultipleChapters()) { - Chapter {{ getCurrentChapterIndex() + 1 }} of {{ audiobookInfo.chapters?.length }} + {{ t('trackInfo.chapterOf', { current: getCurrentChapterIndex() + 1, total: audiobookInfo.chapters?.length }) }} } }
@@ -119,7 +120,7 @@ severity="secondary" (onClick)="previousTrack()" [disabled]="currentTrackIndex === 0" - pTooltip="Previous Track" + [pTooltip]="t('controls.previousTrackTooltip')" tooltipPosition="top" size="large" /> @@ -131,7 +132,7 @@ severity="secondary" (onClick)="previousChapter()" [disabled]="!canGoPreviousChapter()" - pTooltip="Previous Chapter" + [pTooltip]="t('controls.previousChapterTooltip')" tooltipPosition="top" size="large" /> @@ -143,7 +144,7 @@ [text]="true" severity="secondary" (onClick)="seekRelative(-30)" - pTooltip="-30s" + [pTooltip]="t('controls.rewindTooltip')" tooltipPosition="top" size="large" /> @@ -164,7 +165,7 @@ [text]="true" severity="secondary" (onClick)="seekRelative(30)" - pTooltip="+30s" + [pTooltip]="t('controls.forwardTooltip')" tooltipPosition="top" size="large" /> @@ -177,7 +178,7 @@ severity="secondary" (onClick)="nextTrack()" [disabled]="!audiobookInfo.tracks || currentTrackIndex >= audiobookInfo.tracks.length - 1" - pTooltip="Next Track" + [pTooltip]="t('controls.nextTrackTooltip')" tooltipPosition="top" size="large" /> @@ -189,7 +190,7 @@ severity="secondary" (onClick)="nextChapter()" [disabled]="!canGoNextChapter()" - pTooltip="Next Chapter" + [pTooltip]="t('controls.nextChapterTooltip')" tooltipPosition="top" size="large" /> @@ -236,7 +237,7 @@ - Narrated by {{ audiobookInfo.narrator }} + {{ t('details.narratedBy', { narrator: audiobookInfo.narrator }) }} } @if (audiobookInfo.bitrate) { - {{ audiobookInfo.bitrate }} kbps + {{ t('details.kbps', { bitrate: audiobookInfo.bitrate }) }} } @if (audiobookInfo.durationMs) { - {{ formatDuration(audiobookInfo.durationMs) }} total + {{ t('details.totalDuration', { duration: formatDuration(audiobookInfo.durationMs) }) }} }
@@ -291,7 +292,7 @@ @if (showTrackList) {
} + diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts index 56dc7a052..df89f209b 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/panel/panel.component.ts @@ -3,6 +3,7 @@ import {CommonModule} from '@angular/common'; import {FormsModule} from '@angular/forms'; import {Subject} from 'rxjs'; import {takeUntil, debounceTime, distinctUntilChanged, filter} from 'rxjs/operators'; +import {TranslocoDirective} from '@jsverse/transloco'; import {ReaderLeftSidebarService, LeftSidebarTab} from './panel.service'; import {BookNoteV2} from '../../../../../shared/service/book-note-v2.service'; import {SearchState, SearchResult} from '../sidebar/sidebar.service'; @@ -13,7 +14,7 @@ import {ReaderIconComponent} from '../../shared/icon.component'; standalone: true, templateUrl: './panel.component.html', styleUrls: ['./panel.component.scss'], - imports: [CommonModule, FormsModule, ReaderIconComponent] + imports: [CommonModule, FormsModule, TranslocoDirective, ReaderIconComponent] }) export class ReaderLeftSidebarComponent implements OnInit, OnDestroy { private leftSidebarService = inject(ReaderLeftSidebarService); diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.html b/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.html index d161267bb..0a05d78c9 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.html +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.html @@ -1,3 +1,4 @@ + @if (isOpen) { - @@ -110,8 +111,8 @@ } @else {
-

No bookmarks yet

- Tap the bookmark icon to save your place +

{{ t('noBookmarksYet') }}

+ {{ t('noBookmarksHint') }}
} } @@ -129,7 +130,7 @@ } {{ annotation.createdAt | date: 'MMM d, y' }}
- @@ -138,8 +139,8 @@ } @else {
-

No highlights yet

- Select text to create a highlight +

{{ t('noHighlightsYet') }}

+ {{ t('noHighlightsHint') }}
} } @@ -148,3 +149,4 @@
} + diff --git a/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.ts index e992c82da..aa4c85f9d 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/layout/sidebar/sidebar.component.ts @@ -3,6 +3,7 @@ import {CommonModule} from '@angular/common'; import {FormsModule} from '@angular/forms'; import {Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; +import {TranslocoDirective} from '@jsverse/transloco'; import {ReaderSidebarService, SidebarBookInfo, SidebarTab} from './sidebar.service'; import {TocItem} from 'epubjs'; import {BookMark} from '../../../../../shared/service/book-mark.service'; @@ -14,7 +15,7 @@ import {ReaderIconComponent} from '../../shared/icon.component'; standalone: true, templateUrl: './sidebar.component.html', styleUrls: ['./sidebar.component.scss'], - imports: [CommonModule, FormsModule, ReaderIconComponent] + imports: [CommonModule, FormsModule, TranslocoDirective, ReaderIconComponent] }) export class ReaderSidebarComponent implements OnInit, OnDestroy { private sidebarService = inject(ReaderSidebarService); diff --git a/booklore-ui/src/app/features/readers/ebook-reader/shared/header-footer.util.ts b/booklore-ui/src/app/features/readers/ebook-reader/shared/header-footer.util.ts index 179762baa..bfca3bf73 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/shared/header-footer.util.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/shared/header-footer.util.ts @@ -11,7 +11,7 @@ export interface ThemeInfo { export class PageDecorator { private static readonly DEFAULT_FONT_SIZE = '0.875rem'; - static updateHeadersAndFooters(renderer: any, chapterName: string, pageInfo?: PageInfo, theme?: ThemeInfo): void { + static updateHeadersAndFooters(renderer: any, chapterName: string, pageInfo?: PageInfo, theme?: ThemeInfo, timeRemainingLabel?: string): void { if (!renderer) { return; @@ -21,7 +21,7 @@ export class PageDecorator { const isSingleColumn = columnCount === 1; this.updateHeaders(renderer, chapterName, isSingleColumn, theme); - this.updateFooters(renderer, pageInfo, isSingleColumn, theme); + this.updateFooters(renderer, pageInfo, isSingleColumn, theme, timeRemainingLabel); } private static updateHeaders(renderer: any, chapterName: string, isSingleColumn: boolean, theme?: ThemeInfo): void { @@ -40,7 +40,7 @@ export class PageDecorator { }); } - private static updateFooters(renderer: any, pageInfo: PageInfo | undefined, isSingleColumn: boolean, theme?: ThemeInfo): void { + private static updateFooters(renderer: any, pageInfo: PageInfo | undefined, isSingleColumn: boolean, theme?: ThemeInfo, timeRemainingLabel?: string): void { if (!renderer.feet || !Array.isArray(renderer.feet) || renderer.feet.length === 0 || !pageInfo) { return; } @@ -49,7 +49,7 @@ export class PageDecorator { renderer.feet.forEach((footElement: HTMLElement, index: number) => { if (footElement) { - const footerContent = this.createFooterContent(pageInfo, isSingleColumn, index, renderer.feet.length, footerStyle); + const footerContent = this.createFooterContent(pageInfo, isSingleColumn, index, renderer.feet.length, footerStyle, timeRemainingLabel); footElement.replaceChildren(footerContent); } }); @@ -88,11 +88,11 @@ export class PageDecorator { return headerContent; } - private static createFooterContent(pageInfo: PageInfo, isSingleColumn: boolean, index: number, totalColumns: number, style: string): HTMLElement { + private static createFooterContent(pageInfo: PageInfo, isSingleColumn: boolean, index: number, totalColumns: number, style: string, timeRemainingLabel?: string): HTMLElement { const footerContent = document.createElement('div'); footerContent.style.cssText = style; - const text = 'Time remaining in section: ' + (pageInfo.sectionTimeText ?? '0s'); + const text = timeRemainingLabel ?? ('Time remaining in section: ' + (pageInfo.sectionTimeText ?? '0s')); if (isSingleColumn) { const timeSpan = document.createElement('span'); diff --git a/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.html b/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.html index 292e716e8..4b640c818 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.html +++ b/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.html @@ -1,3 +1,4 @@ + @if (visible) {
-
-
- @@ -57,15 +58,16 @@
- @if (overlappingAnnotationId) {
- }
} + diff --git a/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.ts b/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.ts index 3b802eabb..9410e5c3f 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/shared/selection-popup.component.ts @@ -1,5 +1,6 @@ import {Component, EventEmitter, Input, Output} from '@angular/core'; import {CommonModule} from '@angular/common'; +import {TranslocoDirective} from '@jsverse/transloco'; import {ReaderIconComponent} from './icon.component'; export type AnnotationStyle = 'highlight' | 'underline' | 'strikethrough' | 'squiggly'; @@ -15,7 +16,7 @@ export interface TextSelectionAction { @Component({ selector: 'app-text-selection-popup', standalone: true, - imports: [CommonModule, ReaderIconComponent], + imports: [CommonModule, TranslocoDirective, ReaderIconComponent], templateUrl: './selection-popup.component.html', styleUrls: ['./selection-popup.component.scss'] }) diff --git a/booklore-ui/src/app/features/readers/ebook-reader/state/progress.service.ts b/booklore-ui/src/app/features/readers/ebook-reader/state/progress.service.ts index b17553747..9d7158acc 100644 --- a/booklore-ui/src/app/features/readers/ebook-reader/state/progress.service.ts +++ b/booklore-ui/src/app/features/readers/ebook-reader/state/progress.service.ts @@ -1,5 +1,6 @@ import {inject, Injectable} from '@angular/core'; import {Subject} from 'rxjs'; +import {TranslocoService} from '@jsverse/transloco'; import {BookPatchService} from '../../../book/service/book-patch.service'; import {ReadingSessionService} from '../../../../shared/service/reading-session.service'; import {PageInfo, ThemeInfo} from '../core/view-manager.service'; @@ -23,6 +24,7 @@ export interface ProgressState { export class ReaderProgressService { private bookPatchService = inject(BookPatchService); private readingSessionService = inject(ReadingSessionService); + private readonly t = inject(TranslocoService); private viewManager = inject(ReaderViewManagerService); private stateService = inject(ReaderStateService); private annotationService = inject(ReaderAnnotationHttpService); @@ -144,10 +146,15 @@ export class ReaderProgressService { bg: this.stateService.currentState.theme.bg || this.stateService.currentState.theme.light.bg }; + const timeLabel = this.t.translate('readerEbook.headerFooterUtil.timeRemainingInSection', { + time: this._currentPageInfo?.sectionTimeText ?? '0s' + }); + this.viewManager.updateHeadersAndFooters( this._currentChapterName || '', this._currentPageInfo, - theme + theme, + timeLabel ); } diff --git a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.ts b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.ts index 70eb95857..f401d3f74 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.ts +++ b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.ts @@ -3,11 +3,13 @@ import {User, UserService, UserSettings} from '../user-management/user.service'; import {MessageService} from 'primeng/api'; import {filter, takeUntil} from 'rxjs/operators'; import {Subject} from 'rxjs'; +import {TranslocoService} from '@jsverse/transloco'; @Injectable({providedIn: 'root'}) export class ReaderPreferencesService implements OnDestroy { private readonly userService = inject(UserService); private readonly messageService = inject(MessageService); + private readonly t = inject(TranslocoService); private currentUser: User | null = null; private readonly destroy$ = new Subject(); @@ -38,8 +40,8 @@ export class ReaderPreferencesService implements OnDestroy { this.userService.updateUserSetting(this.currentUser.id, rootKey, updatedValue); this.messageService.add({ severity: 'success', - summary: 'Preferences Updated', - detail: 'Your preferences have been saved successfully.', + summary: this.t.translate('settingsReader.toast.preferencesUpdated'), + detail: this.t.translate('settingsReader.toast.preferencesUpdatedDetail'), life: 2000 }); } diff --git a/booklore-ui/src/app/features/settings/task-management/task-helper.service.ts b/booklore-ui/src/app/features/settings/task-management/task-helper.service.ts index 0ac67cbee..d4076a4f1 100644 --- a/booklore-ui/src/app/features/settings/task-management/task-helper.service.ts +++ b/booklore-ui/src/app/features/settings/task-management/task-helper.service.ts @@ -4,6 +4,7 @@ import {MetadataRefreshRequest} from '../../metadata/model/request/metadata-refr import {catchError, map} from 'rxjs/operators'; import {of} from 'rxjs'; import {TaskCreateRequest, TaskService, TaskType} from './task.service'; +import {TranslocoService} from '@jsverse/transloco'; @Injectable({ providedIn: 'root' @@ -11,6 +12,7 @@ import {TaskCreateRequest, TaskService, TaskType} from './task.service'; export class TaskHelperService { private taskService = inject(TaskService); private messageService = inject(MessageService); + private readonly t = inject(TranslocoService); refreshMetadataTask(options: MetadataRefreshRequest) { const request: TaskCreateRequest = { @@ -22,8 +24,8 @@ export class TaskHelperService { map(() => { this.messageService.add({ severity: 'success', - summary: 'Metadata Update Scheduled', - detail: 'The metadata update for the selected books has been successfully scheduled.' + summary: this.t.translate('settingsTasks.toast.metadataScheduled'), + detail: this.t.translate('settingsTasks.toast.metadataScheduledDetail') }); return {success: true}; }), @@ -31,16 +33,16 @@ export class TaskHelperService { if (e.status === 409) { this.messageService.add({ severity: 'error', - summary: 'Task Already Running', + summary: this.t.translate('settingsTasks.toast.alreadyRunning'), life: 5000, - detail: 'A metadata refresh task is already in progress. Please wait for it to complete before starting another one.' + detail: this.t.translate('settingsTasks.toast.metadataAlreadyRunningDetail') }); } else { this.messageService.add({ severity: 'error', - summary: 'Metadata Update Failed', + summary: this.t.translate('settingsTasks.toast.metadataFailed'), life: 5000, - detail: 'An unexpected error occurred while scheduling the metadata update. Please try again later or contact support if the issue persists.' + detail: this.t.translate('settingsTasks.toast.metadataFailedDetail') }); } return of({success: false}); diff --git a/booklore-ui/src/app/shared/components/live-notification-box/live-notification-box.component.ts b/booklore-ui/src/app/shared/components/live-notification-box/live-notification-box.component.ts index 4c9d8f754..65f233eb2 100644 --- a/booklore-ui/src/app/shared/components/live-notification-box/live-notification-box.component.ts +++ b/booklore-ui/src/app/shared/components/live-notification-box/live-notification-box.component.ts @@ -2,6 +2,7 @@ import {Component, inject} from '@angular/core'; import {NotificationEventService} from '../../websocket/notification-event.service'; import {LogNotification} from '../../websocket/model/log-notification.model'; import {Tag} from 'primeng/tag'; +import {TranslocoService} from '@jsverse/transloco'; import {TagComponent} from '../tag/tag.component'; @@ -18,7 +19,8 @@ import {TagComponent} from '../tag/tag.component'; ] }) export class LiveNotificationBoxComponent { - latestNotification: LogNotification = {message: 'No recent notifications...'}; + private readonly t = inject(TranslocoService); + latestNotification: LogNotification = {message: this.t.translate('shared.liveNotification.defaultMessage')}; private notificationService = inject(NotificationEventService); diff --git a/booklore-ui/src/app/shared/components/metadata-progress-widget/metadata-progress-widget-component.ts b/booklore-ui/src/app/shared/components/metadata-progress-widget/metadata-progress-widget-component.ts index e08de2b0a..ab26026c4 100644 --- a/booklore-ui/src/app/shared/components/metadata-progress-widget/metadata-progress-widget-component.ts +++ b/booklore-ui/src/app/shared/components/metadata-progress-widget/metadata-progress-widget-component.ts @@ -7,6 +7,7 @@ import {ButtonModule} from 'primeng/button'; import {Divider} from 'primeng/divider'; import {Tooltip} from 'primeng/tooltip'; import {MessageService} from 'primeng/api'; +import {TranslocoService} from '@jsverse/transloco'; import {MetadataBatchProgressNotification, MetadataBatchStatus, MetadataBatchStatusLabels} from '../../model/metadata-batch-progress.model'; import {MetadataProgressService} from '../../service/metadata-progress.service'; @@ -31,6 +32,7 @@ export class MetadataProgressWidgetComponent implements OnInit, OnDestroy { private metadataTaskService = inject(MetadataTaskService); private taskService = inject(TaskService); private messageService = inject(MessageService); + private readonly t = inject(TranslocoService); private lastUpdateMap = new Map(); private timeoutHandles = new Map(); @@ -79,7 +81,7 @@ export class MetadataProgressWidgetComponent implements OnInit, OnDestroy { this.activeTasks[taskId] = { ...task, status: MetadataBatchStatus.ERROR, - message: 'Task stalled or backend unavailable' + message: this.t.translate('shared.metadataProgress.taskStalled') }; this.activeTasks = {...this.activeTasks}; } @@ -112,23 +114,23 @@ export class MetadataProgressWidgetComponent implements OnInit, OnDestroy { this.activeTasks[taskId] = { ...task, status: MetadataBatchStatus.CANCELLED, - message: 'Task cancelled by user' + message: this.t.translate('shared.metadataProgress.taskCancelled') }; this.activeTasks = {...this.activeTasks}; } this.messageService.add({ severity: 'success', - summary: 'Cancellation Scheduled', - detail: 'Task cancellation has been successfully scheduled' + summary: this.t.translate('shared.metadataProgress.cancellationScheduledSummary'), + detail: this.t.translate('shared.metadataProgress.cancellationScheduledDetail') }); }, error: (error) => { console.error('Failed to cancel task:', error); this.messageService.add({ severity: 'error', - summary: 'Cancel Failed', - detail: 'Failed to cancel the task. Please try again.' + summary: this.t.translate('shared.metadataProgress.cancelFailedSummary'), + detail: this.t.translate('shared.metadataProgress.cancelFailedDetail') }); } }); diff --git a/booklore-ui/src/app/shared/service/settings-helper.service.ts b/booklore-ui/src/app/shared/service/settings-helper.service.ts index 6ba17f08d..480f17b92 100644 --- a/booklore-ui/src/app/shared/service/settings-helper.service.ts +++ b/booklore-ui/src/app/shared/service/settings-helper.service.ts @@ -1,6 +1,7 @@ import {Injectable, inject} from '@angular/core'; import {AppSettingsService} from './app-settings.service'; import {MessageService} from 'primeng/api'; +import {TranslocoService} from '@jsverse/transloco'; import {Observable} from 'rxjs'; @Injectable({ @@ -10,6 +11,7 @@ export class SettingsHelperService { private readonly appSettingsService = inject(AppSettingsService); private readonly messageService = inject(MessageService); + private readonly t = inject(TranslocoService); saveSetting(key: string, value: unknown): Observable { const observable = this.appSettingsService.saveSettings([{key, newValue: value}]); @@ -28,16 +30,16 @@ export class SettingsHelperService { private showSuccessMessage(): void { this.messageService.add({ severity: 'success', - summary: 'Settings Saved', - detail: 'The settings were successfully saved!' + summary: this.t.translate('shared.settingsHelper.settingsSavedSummary'), + detail: this.t.translate('shared.settingsHelper.settingsSavedDetail') }); } private showErrorMessage(): void { this.messageService.add({ severity: 'error', - summary: 'Error', - detail: 'There was an error saving the settings.' + summary: this.t.translate('common.error'), + detail: this.t.translate('shared.settingsHelper.saveErrorDetail') }); } diff --git a/booklore-ui/src/i18n/en/auth.json b/booklore-ui/src/i18n/en/auth.json index 2cc069d88..5668b70e9 100644 --- a/booklore-ui/src/i18n/en/auth.json +++ b/booklore-ui/src/i18n/en/auth.json @@ -1,4 +1,8 @@ { + "oidc": { + "loginFailedSummary": "OIDC Login Failed", + "redirectingDetail": "Redirecting to local login..." + }, "login": { "title": "Welcome Back", "subtitle": "Sign in to continue your journey", diff --git a/booklore-ui/src/i18n/en/book.json b/booklore-ui/src/i18n/en/book.json new file mode 100644 index 000000000..8c21db144 --- /dev/null +++ b/booklore-ui/src/i18n/en/book.json @@ -0,0 +1,649 @@ +{ + "browser": { + "confirm": { + "deleteMessage": "Are you sure you want to delete {{ count }} book(s)?\n\nThis will permanently remove the book files from your filesystem.\n\nThis action cannot be undone.", + "deleteHeader": "Confirm Deletion", + "regenCoverMessage": "Are you sure you want to regenerate covers for {{ count }} book(s)?", + "regenCoverHeader": "Confirm Cover Regeneration", + "customCoverMessage": "Are you sure you want to generate custom covers for {{ count }} book(s)?", + "customCoverHeader": "Confirm Custom Cover Generation" + }, + "toast": { + "sortSavedSummary": "Sort Saved", + "sortSavedGlobalDetail": "Default sort configuration saved.", + "sortSavedEntityDetail": "Sort configuration saved for this {{ entityType }}.", + "regenCoverStartedSummary": "Cover Regeneration Started", + "regenCoverStartedDetail": "Regenerating covers for {{ count }} book(s). Refresh the page when complete.", + "customCoverStartedSummary": "Custom Cover Generation Started", + "customCoverStartedDetail": "Generating custom covers for {{ count }} book(s).", + "failedSummary": "Failed", + "regenCoverFailedDetail": "Could not start cover regeneration.", + "customCoverFailedDetail": "Could not start custom cover generation.", + "unshelveSuccessDetail": "Books shelves updated", + "unshelveFailedDetail": "Failed to update books shelves", + "noEligibleBooksSummary": "No Eligible Books", + "noEligibleBooksDetail": "Selected books must be single-file books (no alternative formats).", + "multipleLibrariesSummary": "Multiple Libraries", + "multipleLibrariesDetail": "All selected books must be from the same library." + }, + "loading": { + "deleting": "Deleting {{ count }} book(s)...", + "unshelving": "Unshelving {{ count }} book(s)..." + }, + "labels": { + "allBooks": "All Books", + "unshelvedBooks": "Unshelved Books", + "unshelvedBooksFiltered": "Unshelved Books (Filtered)", + "filteredSuffix": "(Filtered)", + "seriesCollapsedInfo": "Showing {{ count }} {{ itemWord }} (series collapsed)", + "item": "item", + "items": "items", + "selected": "selected", + "setAsDefault": "Set as default?", + "save": "Save", + "collapseSeries": "Collapse series", + "gridColumns": "Grid columns", + "gridItemSize": "Grid item size", + "bookTypeOverlay": "Book type overlay", + "noBooks": "This collection has no books!", + "failedLibrary": "Failed to load library's books!", + "failedShelf": "Failed to load shelf's books!", + "activeFilters": "{{ count }} Active Filters" + }, + "tooltip": { + "clearFilters": "Clear applied filters", + "visibleColumns": "Visible columns", + "displaySettings": "Display settings", + "selectSorting": "Select sorting", + "toggleView": "Toggle between Grid and Table view", + "search": "Search", + "toggleSidebar": "Toggle sidebar filters", + "metadataActions": "Metadata actions", + "assignToShelf": "Assign to shelf", + "removeFromShelf": "Remove from this shelf", + "lockUnlockMetadata": "Lock/Unlock metadata", + "organizeFiles": "Organize Files", + "attachToBook": "Attach to Another Book", + "moreActions": "More actions", + "selectAll": "Select all books", + "deselectAll": "Deselect all books", + "deleteSelected": "Delete selected books" + }, + "placeholder": { + "selectColumns": "Select Columns", + "search": "Title, Author, Series, Genre, or ISBN..." + } + }, + "card": { + "menu": { + "assignShelf": "Assign Shelf", + "viewDetails": "View Details", + "download": "Download", + "delete": "Delete", + "emailBook": "Email Book", + "quickSend": "Quick Send", + "customSend": "Custom Send", + "metadata": "Metadata", + "searchMetadata": "Search Metadata", + "autoFetch": "Auto Fetch", + "customFetch": "Custom Fetch", + "regenerateCover": "Regenerate Cover (File)", + "generateCustomCover": "Generate Custom Cover", + "moreActions": "More Actions", + "organizeFile": "Organize File", + "readStatus": "Read Status", + "resetBookloreProgress": "Reset Booklore Progress", + "resetKOReaderProgress": "Reset KOReader Progress", + "book": "Book", + "loading": "Loading..." + }, + "confirm": { + "deleteBookMessage": "Are you sure you want to delete \"{{ title }}\"?\n\nThis will permanently remove the book file from your filesystem.\n\nThis action cannot be undone.", + "deleteBookHeader": "Confirm Deletion", + "deleteFileMessage": "Are you sure you want to delete the additional file \"{{ fileName }}\"?", + "deleteFileHeader": "Confirm File Deletion" + }, + "toast": { + "quickSendSuccessDetail": "The book sending has been scheduled.", + "quickSendErrorDetail": "An error occurred while sending the book.", + "deleteFileSuccessDetail": "Additional file \"{{ fileName }}\" deleted successfully", + "deleteFileErrorDetail": "Failed to delete additional file: {{ error }}", + "readStatusUpdatedSummary": "Read Status Updated", + "readStatusUpdatedDetail": "Marked as \"{{ label }}\"", + "readStatusFailedSummary": "Update Failed", + "readStatusFailedDetail": "Could not update read status.", + "progressResetSummary": "Progress Reset", + "progressResetBookloreDetail": "Booklore reading progress has been reset.", + "progressResetKOReaderDetail": "KOReader reading progress has been reset.", + "progressResetFailedSummary": "Failed", + "progressResetBookloreFailedDetail": "Could not reset Booklore progress.", + "progressResetKOReaderFailedDetail": "Could not reset KOReader progress.", + "coverRegenSuccessSummary": "Success", + "coverRegenSuccessDetail": "Cover regeneration started", + "coverRegenFailedDetail": "Failed to regenerate cover", + "customCoverSuccessSummary": "Success", + "customCoverSuccessDetail": "Cover generated successfully", + "customCoverFailedDetail": "Failed to generate cover" + }, + "alt": { + "cover": "Cover of {{ title }}", + "titleTooltip": "Title: {{ title }}", + "seriesCollapsed": "Series collapsed: {{ count }} books" + } + }, + "notes": { + "confirm": { + "deleteMessage": "Are you sure you want to delete the note \"{{ title }}\"?", + "deleteHeader": "Confirm Deletion" + }, + "toast": { + "loadFailedDetail": "Failed to load notes for this book.", + "validationSummary": "Validation Error", + "validationDetail": "Both title and content are required.", + "createSuccessDetail": "Note created successfully.", + "createFailedDetail": "Failed to create note.", + "updateSuccessDetail": "Note updated successfully.", + "updateFailedDetail": "Failed to update note.", + "deleteSuccessDetail": "Note deleted successfully.", + "deleteFailedDetail": "Failed to delete note." + } + }, + "bookService": { + "toast": { + "someFilesNotDeletedSummary": "Some files could not be deleted", + "someFilesNotDeletedDetail": "Books: {{ fileNames }}", + "booksDeletedSummary": "Books Deleted", + "booksDeletedDetail": "{{ count }} book(s) deleted successfully.", + "deleteFailedSummary": "Delete Failed", + "deleteFailedDetail": "An error occurred while deleting books.", + "physicalBookCreatedSummary": "Physical Book Created", + "physicalBookCreatedDetail": "\"{{ title }}\" has been added to your library.", + "creationFailedSummary": "Creation Failed", + "creationFailedDetail": "An error occurred while creating the physical book.", + "fileDeletedSummary": "File Deleted", + "additionalFileDeletedDetail": "Additional file deleted successfully.", + "bookFileDeletedDetail": "Book file deleted successfully.", + "fileDeleteFailedSummary": "Delete Failed", + "fileDeleteFailedDetail": "An error occurred while deleting the file.", + "fileUploadedSummary": "File Uploaded", + "fileUploadedDetail": "Additional file uploaded successfully.", + "uploadFailedSummary": "Upload Failed", + "uploadFailedDetail": "An error occurred while uploading the file.", + "fieldLockFailedSummary": "Field Lock Update Failed", + "fieldLockFailedDetail": "Failed to update metadata field locks. Please try again.", + "filesAttachedSummary": "Files Attached", + "filesAttachedDetail": "{{ count }} book file(s) have been attached successfully.", + "attachmentFailedSummary": "Attachment Failed", + "attachmentFailedDetail": "An error occurred while attaching the files." + } + }, + "menuService": { + "menu": { + "autoFetchMetadata": "Auto Fetch Metadata", + "customFetchMetadata": "Custom Fetch Metadata", + "bulkMetadataEditor": "Bulk Metadata Editor", + "multiBookMetadataEditor": "Multi-Book Metadata Editor", + "regenerateCovers": "Regenerate Covers", + "generateCustomCovers": "Generate Custom Covers", + "updateReadStatus": "Update Read Status", + "setAgeRating": "Set Age Rating", + "clearAgeRating": "Clear Age Rating", + "setContentRating": "Set Content Rating", + "clearContentRating": "Clear Content Rating", + "removeFromAllShelves": "Remove from all shelves", + "resetBookloreProgress": "Reset Booklore Progress", + "resetKOReaderProgress": "Reset KOReader Progress" + }, + "confirm": { + "readStatusMessage": "Are you sure you want to mark {{ count }} book(s) as \"{{ label }}\"?", + "readStatusHeader": "Confirm Read Status Update", + "ageRatingMessage": "Are you sure you want to set the age rating to \"{{ label }}\" for {{ count }} book(s)?", + "ageRatingHeader": "Confirm Age Rating Update", + "clearAgeRatingMessage": "Are you sure you want to clear the age rating for {{ count }} book(s)?", + "clearAgeRatingHeader": "Confirm Clear Age Rating", + "contentRatingMessage": "Are you sure you want to set the content rating to \"{{ label }}\" for {{ count }} book(s)?", + "contentRatingHeader": "Confirm Content Rating Update", + "clearContentRatingMessage": "Are you sure you want to clear the content rating for {{ count }} book(s)?", + "clearContentRatingHeader": "Confirm Clear Content Rating", + "unshelveMessage": "Are you sure you want to remove {{ count }} book(s) from ALL their shelves?", + "unshelveHeader": "Confirm Unshelve", + "resetBookloreMessage": "Are you sure you want to reset Booklore reading progress for {{ count }} book(s)?", + "resetKOReaderMessage": "Are you sure you want to reset KOReader reading progress for {{ count }} book(s)?", + "resetHeader": "Confirm Reset" + }, + "toast": { + "readStatusUpdatedSummary": "Read Status Updated", + "readStatusUpdatedDetail": "Marked as \"{{ label }}\"", + "updateFailedSummary": "Update Failed", + "readStatusFailedDetail": "Could not update read status.", + "ageRatingUpdatedSummary": "Age Rating Updated", + "ageRatingUpdatedDetail": "Set to \"{{ label }}\"", + "ageRatingFailedDetail": "Could not update age rating.", + "ageRatingClearedSummary": "Age Rating Cleared", + "ageRatingClearedDetail": "Age rating has been cleared.", + "clearAgeRatingFailedDetail": "Could not clear age rating.", + "contentRatingUpdatedSummary": "Content Rating Updated", + "contentRatingUpdatedDetail": "Set to \"{{ label }}\"", + "contentRatingFailedDetail": "Could not update content rating.", + "contentRatingClearedSummary": "Content Rating Cleared", + "contentRatingClearedDetail": "Content rating has been cleared.", + "clearContentRatingFailedDetail": "Could not clear content rating.", + "noBooksOnShelvesDetail": "Selected books are not on any shelves.", + "unshelveSuccessDetail": "Books removed from all shelves", + "unshelveFailedDetail": "Failed to update books shelves", + "progressResetSummary": "Progress Reset", + "bookloreProgressResetDetail": "Booklore reading progress has been reset.", + "koreaderProgressResetDetail": "KOReader reading progress has been reset.", + "failedSummary": "Failed", + "progressResetFailedDetail": "Could not reset progress." + }, + "loading": { + "updatingReadStatus": "Updating read status for {{ count }} book(s)...", + "settingAgeRating": "Setting age rating for {{ count }} book(s)...", + "clearingAgeRating": "Clearing age rating for {{ count }} book(s)...", + "settingContentRating": "Setting content rating for {{ count }} book(s)...", + "clearingContentRating": "Clearing content rating for {{ count }} book(s)...", + "removingFromShelves": "Removing {{ count }} book(s) from shelves...", + "resettingBookloreProgress": "Resetting Booklore progress for {{ count }} book(s)...", + "resettingKOReaderProgress": "Resetting KOReader progress for {{ count }} book(s)..." + } + }, + "shelfMenuService": { + "library": { + "optionsLabel": "Options", + "addPhysicalBook": "Add Physical Book", + "editLibrary": "Edit Library", + "rescanLibrary": "Re-scan Library", + "customFetchMetadata": "Custom Fetch Metadata", + "autoFetchMetadata": "Auto Fetch Metadata", + "deleteLibrary": "Delete Library" + }, + "shelf": { + "publicShelfPrefix": "Public Shelf - ", + "readOnly": "Read only", + "optionsLabel": "Options", + "editShelf": "Edit Shelf", + "deleteShelf": "Delete Shelf" + }, + "magicShelf": { + "optionsLabel": "Options", + "editMagicShelf": "Edit Magic Shelf", + "deleteMagicShelf": "Delete Magic Shelf" + }, + "confirm": { + "rescanLibraryMessage": "Are you sure you want to refresh library: {{ name }}?", + "deleteLibraryMessage": "Are you sure you want to delete library: {{ name }}?", + "deleteShelfMessage": "Are you sure you want to delete shelf: {{ name }}?", + "deleteMagicShelfMessage": "Are you sure you want to delete magic shelf: {{ name }}?", + "header": "Confirmation", + "rescanLabel": "Rescan" + }, + "toast": { + "libraryRefreshSuccessDetail": "Library refresh scheduled", + "libraryRefreshFailedDetail": "Failed to refresh library", + "libraryDeletedDetail": "Library was deleted", + "libraryDeleteFailedDetail": "Failed to delete library", + "shelfDeletedDetail": "Shelf was deleted", + "shelfDeleteFailedDetail": "Failed to delete shelf", + "magicShelfDeletedDetail": "Magic shelf was deleted", + "magicShelfDeleteFailedDetail": "Failed to delete shelf", + "failedSummary": "Failed" + }, + "loading": { + "deletingLibrary": "Deleting library '{{ name }}'..." + } + }, + "columnPref": { + "columns": { + "readStatus": "Read", + "title": "Title", + "authors": "Authors", + "publisher": "Publisher", + "seriesName": "Series", + "seriesNumber": "Series #", + "categories": "Genres", + "publishedDate": "Published", + "lastReadTime": "Last Read", + "addedOn": "Added", + "fileName": "File Name", + "fileSizeKb": "File Size", + "language": "Language", + "isbn": "ISBN", + "pageCount": "Pages", + "amazonRating": "Amazon", + "amazonReviewCount": "AZ #", + "goodreadsRating": "Goodreads", + "goodreadsReviewCount": "GR #", + "hardcoverRating": "Hardcover", + "hardcoverReviewCount": "HC #", + "ranobedbRating": "Ranobedb" + }, + "toast": { + "savedSummary": "Preferences Saved", + "savedDetail": "Your column layout has been saved." + } + }, + "coverPref": { + "toast": { + "savedSummary": "Cover Size Saved", + "savedDetail": "Cover size set to {{ scale }}x.", + "saveFailedSummary": "Save Failed", + "saveFailedDetail": "Could not save cover size preference locally." + } + }, + "filterPref": { + "toast": { + "saveFailedSummary": "Save Failed", + "saveFailedDetail": "Could not save sidebar filter preference locally." + } + }, + "reviews": { + "confirm": { + "deleteAllMessage": "Are you sure you want to delete all {{ count }} reviews for this book? This action cannot be undone.", + "deleteAllHeader": "Confirm Delete All", + "deleteMessage": "Are you sure you want to delete this review by {{ reviewer }}?", + "deleteHeader": "Confirm Deletion" + }, + "toast": { + "loadFailedSummary": "Failed to Load Reviews", + "loadFailedDetail": "Could not load reviews for this book.", + "reviewsUpdatedSummary": "Reviews Updated", + "reviewsUpdatedDetail": "Latest reviews have been fetched successfully.", + "fetchFailedSummary": "Fetch Failed", + "fetchFailedDetail": "Could not fetch new reviews for this book.", + "allDeletedSummary": "All Reviews Deleted", + "allDeletedDetail": "All reviews have been successfully deleted.", + "deleteAllFailedSummary": "Delete Failed", + "deleteAllFailedDetail": "Could not delete all reviews.", + "deleteSuccessSummary": "Review Deleted", + "deleteSuccessDetail": "The review has been successfully deleted.", + "deleteFailedSummary": "Delete Failed", + "deleteFailedDetail": "Could not delete the review.", + "lockedSummary": "Reviews Locked", + "lockedDetail": "Reviews are now protected from modifications and refreshes.", + "unlockedSummary": "Reviews Unlocked", + "unlockedDetail": "Reviews can now be modified and refreshed.", + "lockFailedSummary": "Lock Toggle Failed", + "lockFailedDetail": "Could not change the lock status for reviews." + }, + "labels": { + "loadingReviews": "Getting latest reviews...", + "showSpoiler": "Show Spoiler", + "anonymous": "Anonymous", + "spoiler": "Spoiler" + }, + "empty": { + "noReviews": "No reviews available for this book", + "downloadsDisabled": "Book review downloads are currently disabled. Enable this in Metadata Settings to fetch reviews.", + "fetchPrompt": "Click \"Fetch Reviews\" to download reviews from configured providers", + "noContent": "No review content available" + }, + "tooltip": { + "fetchReviews": "Fetch Reviews", + "reviewsLocked": "Reviews are locked", + "deleteReview": "Delete Review", + "pleaseWait": "Please wait while reviews are being fetched", + "enableDownloads": "Enable review downloads in settings to use this feature", + "unlockReviews": "Unlock Reviews", + "lockReviews": "Lock Reviews", + "fetchNewReviews": "Fetch New Reviews", + "deleteAllReviews": "Delete All Reviews", + "hideAllSpoilers": "Hide All Spoilers", + "revealAllSpoilers": "Reveal All Spoilers", + "sortNewest": "Sort by Newest First", + "sortOldest": "Sort by Oldest First" + } + }, + "addPhysicalBook": { + "title": "Add Physical Book", + "description": "Catalog a physical book without a digital file", + "closeTooltip": "Close", + "libraryLabel": "Library", + "libraryPlaceholder": "Select a library", + "titleLabel": "Title", + "titlePlaceholder": "e.g., The Great Gatsby", + "isbnLabel": "ISBN", + "isbnPlaceholder": "e.g., 9780134685991", + "authorsLabel": "Authors", + "authorsPlaceholder": "Type author name and press Enter", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Brief description of the book...", + "publisherLabel": "Publisher", + "publisherPlaceholder": "Publisher name", + "publishedDateLabel": "Published Date", + "publishedDatePlaceholder": "e.g., 2020 or 2020-05-15", + "languageLabel": "Language", + "languagePlaceholder": "e.g., English", + "pageCountLabel": "Page Count", + "pageCountPlaceholder": "Number of pages", + "categoriesLabel": "Categories/Genres", + "categoriesPlaceholder": "Type category and press Enter", + "validationLibraryRequired": "Library is required", + "validationTitleOrIsbn": "Title or ISBN is required", + "validationReady": "Ready to create", + "cancelButton": "Cancel", + "addButton": "Add Physical Book" + }, + "fileUploader": { + "title": "Upload Additional File", + "unknownTitle": "Unknown Title", + "selectFileType": "Select file type", + "typeAlternativeFormat": "Alternative Format", + "typeSupplementary": "Supplementary File", + "descriptionLabel": "Description (Optional)", + "descriptionPlaceholder": "Add a description for this file...", + "statusUploading": "Uploading", + "statusUploaded": "Uploaded", + "statusUploadFailed": "Upload failed", + "statusTooLarge": "Too Large", + "statusReady": "Ready", + "statusFailed": "Failed", + "dragDropText": "Drag and drop a file here to upload.", + "uploadForBook": "Upload an additional file for {{ title }}.", + "thisBook": "this book", + "toast": { + "fileTooLargeSummary": "File Too Large", + "fileTooLargeDetail": "{{ fileName }} exceeds the maximum file size of {{ maxSize }}", + "fileTooLargeError": "File exceeds maximum size of {{ maxSize }}", + "uploadFailedUnknown": "Upload failed due to unknown error." + } + }, + "filter": { + "title": "Filters", + "showingFirst100": "Showing first 100 items", + "footerNote": "Note: Top 100 items are displayed per filter category" + }, + "shelfAssigner": { + "title": "Assign Books to Shelves", + "descriptionMulti": "Select shelves for {{ count }} book(s)", + "descriptionSingle": "Organize \"{{ title }}\" into your shelves", + "shelvesAvailable": "{{ count }} shelf(ves) available", + "emptyTitle": "No Shelves Available", + "emptyDescription": "Create your first shelf to start organizing your books.
Click the button below to get started.", + "createShelf": "Create Shelf", + "cancelButton": "Cancel", + "saveChanges": "Save Changes", + "loading": { + "updatingShelves": "Updating shelves for {{ count }} book(s)..." + }, + "toast": { + "updateSuccessDetail": "Book shelves updated", + "updateFailedDetail": "Failed to update book shelves" + } + }, + "shelfCreator": { + "title": "Create New Shelf", + "description": "Add a custom shelf to organize your books", + "closeTooltip": "Close", + "shelfNameLabel": "Shelf Name", + "shelfNamePlaceholder": "e.g., Favorites, To Read, Currently Reading", + "shelfIconLabel": "Shelf Icon (Optional)", + "chooseIcon": "Choose an Icon", + "chooseIconSubtitle": "Select from available icons", + "selectedIcon": "Selected Icon", + "removeIconTooltip": "Remove icon", + "visibilityLabel": "Visibility", + "makePublicLabel": "Make this shelf public (read-only for others)", + "validationRequired": "Shelf name is required", + "validationReady": "Ready to create", + "cancelButton": "Cancel", + "createButton": "Create Shelf", + "toast": { + "createSuccessDetail": "Shelf created: {{ name }}", + "createFailedDetail": "Failed to create shelf" + } + }, + "shelfEditDialog": { + "title": "Edit Shelf", + "description": "Customize your shelf name and icon", + "shelfNameLabel": "Shelf Name:", + "shelfNamePlaceholder": "Enter shelf name...", + "shelfIconLabel": "Shelf Icon:", + "selectIcon": "Select Icon", + "visibilityLabel": "Visibility:", + "publicShelfLabel": "Public Shelf", + "cancelButton": "Cancel", + "saveChanges": "Save Changes", + "toast": { + "updateSuccessSummary": "Shelf Updated", + "updateSuccessDetail": "The shelf was updated successfully.", + "updateFailedSummary": "Update Failed", + "updateFailedDetail": "An error occurred while updating the shelf. Please try again." + } + }, + "table": { + "bookCoverAlt": "Book Cover", + "statusPrefix": "Status: ", + "locked": "Locked", + "unlocked": "Unlocked", + "toast": { + "metadataLockedSummary": "Metadata Locked", + "metadataLockedDetail": "Book metadata has been locked successfully.", + "metadataUnlockedSummary": "Metadata Unlocked", + "metadataUnlockedDetail": "Book metadata has been unlocked successfully.", + "lockFailedSummary": "Failed to Lock", + "lockFailedDetail": "An error occurred while locking the metadata.", + "unlockFailedSummary": "Failed to Unlock", + "unlockFailedDetail": "An error occurred while unlocking the metadata." + } + }, + "lockUnlockDialog": { + "title": "Lock or Unlock Metadata", + "selectedCount": "{{ count }} book(s) selected", + "reset": "Reset", + "lockAll": "Lock All", + "unlockAll": "Unlock All", + "save": "Save", + "saving": "Saving...", + "locked": "Locked", + "unlocked": "Unlocked", + "unselected": "Unselected", + "toast": { + "updatingFieldLocks": "Updating field locks...", + "updatedSummary": "Field Locks Updated", + "updatedDetail": "Selected metadata fields have been updated successfully.", + "failedSummary": "Failed to Update Field Locks", + "failedDetail": "An error occurred while updating field lock statuses." + } + }, + "sorting": { + "sortOrder": "Sort Order", + "removeTooltip": "Remove", + "addSortFieldPlaceholder": "Add sort field...", + "saveAsDefault": "Save as Default", + "ascendingTooltip": "Ascending - click to change", + "descendingTooltip": "Descending - click to change" + }, + "fileAttacher": { + "title": "Attach File to Another Book", + "titleBulk": "Attach Files to Another Book", + "description": "Move this book's file to another book as alternative format", + "descriptionBulk": "Move these books' files to another book as alternative formats", + "sourceBookLabel": "Source Book ({{ count }})", + "sourceBooksLabel": "Source Books ({{ count }})", + "unknownTitle": "Unknown Title", + "selectTargetBook": "Select Target Book", + "searchPlaceholder": "Search for a book in the same library...", + "deleteSource": "Delete source book after attachment", + "deleteSourceBulk": "Delete source books after attachment", + "warningMove": "This will move the file from the source book to the target book as alternative format.", + "warningMoveBulk": "This will move the files from the selected books to the target book as alternative formats.", + "warningDelete": "The source book record will be deleted (file will be preserved in target book).", + "warningDeleteBulk": "The source book records will be deleted (files will be preserved in target book).", + "warningKeep": "The source book record will remain but will have no readable files.", + "warningKeepBulk": "The source book records will remain but will have no readable files.", + "attachFile": "Attach File", + "attachFilesBulk": "Attach Files", + "unknownFile": "Unknown file", + "unknownFormat": "Unknown", + "unknownFilename": "Unknown filename" + }, + "searcher": { + "placeholder": "Title, Author, Series, Genre, or ISBN...", + "clearSearch": "Clear Search", + "bookCoverAlt": "Book Cover", + "byPrefix": "by", + "noResults": "No results found", + "unknownAuthor": "Unknown Author" + }, + "sender": { + "title": "Send Book", + "description": "Email this book to a recipient", + "emailProvider": "Email Provider", + "selectProvider": "Select Email Provider", + "recipient": "Recipient", + "selectRecipient": "Select Book Recipient", + "fileFormat": "File Format", + "unknownFormat": "Unknown", + "primaryBadge": "Primary", + "largeFileWarning": "This file exceeds 25MB. Some email providers may reject large attachments.", + "sendBook": "Send Book", + "toast": { + "emailScheduledSummary": "Email Scheduled", + "emailScheduledDetail": "The book has been successfully scheduled for sending.", + "sendingFailedSummary": "Sending Failed", + "sendingFailedDetail": "There was an issue while scheduling the book for sending. Please try again later.", + "providerMissingSummary": "Email Provider Missing", + "providerMissingDetail": "Please select an email provider to proceed.", + "recipientMissingSummary": "Recipient Missing", + "recipientMissingDetail": "Please select a recipient to send the book.", + "bookNotSelectedSummary": "Book Not Selected", + "bookNotSelectedDetail": "Please select a book to send." + } + }, + "seriesPage": { + "seriesDetailsTab": "Series Details", + "publisher": "Publisher:", + "years": "Years:", + "numberOfBooks": "Number of books:", + "language": "Language:", + "readStatus": "Read Status:", + "noDescription": "No description available.", + "showLess": "Show less", + "showMore": "Show more", + "noBooksFound": "No books found for this series.", + "selected": "selected", + "loadingSeriesDetails": "Loading series details...", + "status": { + "unread": "UNREAD", + "reading": "READING", + "reReading": "RE-READING", + "read": "READ", + "partiallyRead": "PARTIALLY READ", + "paused": "PAUSED", + "abandoned": "ABANDONED", + "wontRead": "WON'T READ", + "unset": "UNSET" + }, + "tooltip": { + "metadataActions": "Metadata actions", + "assignToShelf": "Assign to shelf", + "lockUnlockMetadata": "Lock/Unlock metadata", + "organizeFiles": "Organize Files", + "moreActions": "More actions", + "selectAll": "Select all books", + "deselectAll": "Deselect all books", + "deleteSelected": "Delete selected books" + } + } +} diff --git a/booklore-ui/src/i18n/en/index.ts b/booklore-ui/src/i18n/en/index.ts index 34200c0c6..57ef8c05a 100644 --- a/booklore-ui/src/i18n/en/index.ts +++ b/booklore-ui/src/i18n/en/index.ts @@ -24,8 +24,12 @@ import libraryCreator from './library-creator.json'; import bookdrop from './bookdrop.json'; import metadata from './metadata.json'; import notebook from './notebook.json'; +import book from './book.json'; +import readerAudiobook from './reader-audiobook.json'; +import readerCbx from './reader-cbx.json'; +import readerEbook from './reader-ebook.json'; // To add a new domain: create the JSON file and add it here. // Settings tabs each get their own file: settings-email, settings-reader, settings-view, etc. -const translations: Translation = {common, auth, nav, dashboard, settings, settingsEmail, settingsReader, settingsView, settingsMeta, settingsLibMeta, settingsApp, settingsUsers, settingsNaming, settingsOpds, settingsTasks, settingsAuth, settingsDevice, settingsProfile, app, shared, layout, libraryCreator, bookdrop, metadata, notebook}; +const translations: Translation = {common, auth, nav, dashboard, settings, settingsEmail, settingsReader, settingsView, settingsMeta, settingsLibMeta, settingsApp, settingsUsers, settingsNaming, settingsOpds, settingsTasks, settingsAuth, settingsDevice, settingsProfile, app, shared, layout, libraryCreator, bookdrop, metadata, notebook, book, readerAudiobook, readerCbx, readerEbook}; export default translations; diff --git a/booklore-ui/src/i18n/en/reader-audiobook.json b/booklore-ui/src/i18n/en/reader-audiobook.json new file mode 100644 index 000000000..18fb0b838 --- /dev/null +++ b/booklore-ui/src/i18n/en/reader-audiobook.json @@ -0,0 +1,64 @@ +{ + "loading": "Loading audiobook...", + "untitled": "Untitled", + "unknownAuthor": "Unknown Author", + "coverAlt": "Cover", + "header": { + "backTooltip": "Back", + "bookmarksTooltip": "Bookmarks", + "chaptersTracksTooltip": "Chapters/Tracks" + }, + "trackInfo": { + "trackOf": "Track {{ current }} of {{ total }}", + "chapterOf": "Chapter {{ current }} of {{ total }}" + }, + "controls": { + "previousTrackTooltip": "Previous Track", + "previousChapterTooltip": "Previous Chapter", + "rewindTooltip": "-30s", + "forwardTooltip": "+30s", + "nextTrackTooltip": "Next Track", + "nextChapterTooltip": "Next Chapter" + }, + "extra": { + "addBookmark": "Add Bookmark", + "sleepTimer": "Sleep Timer", + "endOfChapter": "End of chapter" + }, + "details": { + "narratedBy": "Narrated by {{ narrator }}", + "kbps": "{{ bitrate }} kbps", + "totalDuration": "{{ duration }} total" + }, + "sidebar": { + "tracks": "Tracks", + "chapters": "Chapters" + }, + "bookmarks": { + "title": "Bookmarks", + "empty": "No bookmarks yet", + "emptyHint": "Add a bookmark to save your place", + "deleteTooltip": "Delete", + "bookmarkAt": "Bookmark at {{ time }}" + }, + "sleepTimerMenu": { + "minutes15": "15 minutes", + "minutes30": "30 minutes", + "minutes45": "45 minutes", + "minutes60": "60 minutes", + "endOfChapter": "End of chapter", + "cancelTimer": "Cancel timer" + }, + "toast": { + "loadFailed": "Failed to load audiobook", + "audioLoadFailed": "Failed to load audio", + "sleepTimerSet": "Playback will stop in {{ minutes }} minutes", + "sleepTimerEndOfChapter": "Playback will stop at end of chapter", + "sleepTimerStopped": "Playback stopped by sleep timer", + "bookmarkAdded": "Bookmark Added", + "bookmarkExists": "Bookmark Exists", + "bookmarkExistsDetail": "A bookmark already exists at this position", + "bookmarkFailed": "Failed to add bookmark", + "bookmarkDeleted": "Bookmark Deleted" + } +} diff --git a/booklore-ui/src/i18n/en/reader-cbx.json b/booklore-ui/src/i18n/en/reader-cbx.json new file mode 100644 index 000000000..905a57983 --- /dev/null +++ b/booklore-ui/src/i18n/en/reader-cbx.json @@ -0,0 +1,127 @@ +{ + "reader": { + "continueToNextBook": "Continue to Next Book", + "noPagesAvailable": "No pages available.", + "loadingBook": "Loading book...", + "slideshow": "Slideshow" + }, + "noteDialog": { + "editNote": "Edit Note", + "addNote": "Add Note", + "pageLabel": "Page", + "pageInfo": "Page {{ pageNumber }}", + "yourNote": "Your Note", + "placeholder": "Write your note here...", + "noteColor": "Note Color", + "updateNote": "Update Note", + "saveNote": "Save Note", + "colorAmber": "Amber", + "colorGreen": "Green", + "colorBlue": "Blue", + "colorPink": "Pink", + "colorPurple": "Purple", + "colorDeepOrange": "Deep Orange" + }, + "shortcutsHelp": { + "title": "Keyboard Shortcuts", + "gotIt": "Got it", + "groupNavigation": "Navigation", + "groupDisplay": "Display", + "groupPlayback": "Playback", + "groupOther": "Other", + "previousNextPage": "Previous / Next page", + "swipeLeftRight": "Swipe left/right", + "nextPage": "Next page", + "previousPage": "Previous page", + "firstPage": "First page", + "lastPage": "Last page", + "toggleFullscreen": "Toggle fullscreen", + "toggleReadingDirection": "Toggle reading direction (LTR/RTL)", + "exitFullscreenCloseDialogs": "Exit fullscreen / Close dialogs", + "toggleZoom": "Toggle zoom (fit page / actual size)", + "doubleTap": "Double-tap", + "toggleSlideshow": "Toggle slideshow / auto-play", + "showHelpDialog": "Show this help dialog" + }, + "footer": { + "prevBook": "Prev Book", + "nextBook": "Next Book", + "firstPage": "First Page", + "previousPage": "Previous Page", + "nextPage": "Next Page", + "lastPage": "Last Page", + "of": "of", + "pageSlider": "Page Slider", + "pagePlaceholder": "Page", + "go": "Go", + "noPreviousBook": "No Previous Book", + "noNextBook": "No Next Book", + "previousBookTooltip": "Previous: {{ title }}", + "nextBookTooltip": "Next: {{ title }}" + }, + "header": { + "contents": "Contents", + "addBookmark": "Add Bookmark", + "removeBookmark": "Remove Bookmark", + "addNote": "Add Note", + "pageHasNotesAddAnother": "Page has notes - Add another", + "stopSlideshow": "Stop Slideshow (P)", + "startSlideshow": "Start Slideshow (P)", + "exitFullscreen": "Exit Fullscreen (F)", + "fullscreen": "Fullscreen (F)", + "keyboardShortcuts": "Keyboard Shortcuts (?)", + "more": "More", + "stopSlideshowLabel": "Stop Slideshow", + "startSlideshowLabel": "Start Slideshow", + "exitFullscreenLabel": "Exit Fullscreen", + "fullscreenLabel": "Fullscreen", + "keyboardShortcutsLabel": "Keyboard Shortcuts", + "settings": "Settings", + "closeReader": "Close Reader" + }, + "quickSettings": { + "fitMode": "Fit Mode", + "fitPage": "Fit Page", + "fitWidth": "Fit Width", + "fitHeight": "Fit Height", + "actualSize": "Actual Size", + "automatic": "Automatic", + "scrollMode": "Scroll Mode", + "paginated": "Paginated", + "infinite": "Infinite", + "longStrip": "Long Strip", + "pageView": "Page View", + "twoPage": "Two-Page", + "single": "Single", + "pageSpread": "Page Spread", + "oddFirst": "Odd First", + "evenFirst": "Even First", + "readingDirection": "Reading Direction", + "leftToRight": "Left to Right", + "rightToLeft": "Right to Left", + "slideshowInterval": "Slideshow Interval", + "background": "Background", + "black": "Black", + "gray": "Gray", + "white": "White" + }, + "sidebar": { + "contentTab": "Content", + "bookmarksTab": "Bookmarks", + "notesTab": "Notes", + "noPagesFound": "No pages found", + "noBookmarksYet": "No bookmarks yet", + "bookmarkHint": "Tap the bookmark icon to save your place", + "searchNotesPlaceholder": "Search notes...", + "noMatchingNotes": "No matching notes", + "tryDifferentSearch": "Try different search terms", + "noNotesYet": "No notes yet", + "notesHint": "Tap the notes icon to add a note for the current page", + "deleteBookmark": "Delete bookmark", + "editNote": "Edit note", + "deleteNote": "Delete note", + "clearSearch": "Clear search", + "page": "Page", + "untitled": "Untitled" + } +} diff --git a/booklore-ui/src/i18n/en/reader-ebook.json b/booklore-ui/src/i18n/en/reader-ebook.json new file mode 100644 index 000000000..4ce02fa4f --- /dev/null +++ b/booklore-ui/src/i18n/en/reader-ebook.json @@ -0,0 +1,194 @@ +{ + "reader": { + "loadingBook": "Loading book..." + }, + "metadataDialog": { + "title": "Book Information", + "basicInformation": "Basic Information", + "titleLabel": "Title", + "subtitle": "Subtitle", + "authors": "Author(s)", + "publisher": "Publisher", + "published": "Published", + "language": "Language", + "pages": "Pages", + "unknown": "Unknown", + "na": "N/A", + "series": "Series", + "seriesName": "Series Name", + "bookNumber": "Book Number", + "identifiers": "Identifiers", + "ratings": "Ratings", + "reviews": "reviews", + "categories": "Categories", + "tags": "Tags", + "fileInformation": "File Information", + "fileSize": "File Size", + "fileName": "File Name", + "description": "Description" + }, + "noteDialog": { + "editNote": "Edit Note", + "addNote": "Add Note", + "selectedText": "Selected Text", + "yourNote": "Your Note", + "notePlaceholder": "Write your note here...", + "noteColor": "Note Color", + "updateNote": "Update Note", + "saveNote": "Save Note" + }, + "settingsDialog": { + "themeTab": "Theme", + "typographyTab": "Typography", + "layoutTab": "Layout", + "darkMode": "Dark Mode", + "themeColors": "Theme Colors", + "annotationHighlighter": "Annotation Highlighter", + "fontSettings": "Font Settings", + "fontSize": "Font Size", + "lineHeight": "Line Height", + "fontFamily": "Font Family", + "layout": "Layout", + "readingFlow": "Reading Flow", + "paginated": "Paginated", + "scrolled": "Scrolled", + "maxColumns": "Max Columns", + "columnGap": "Column Gap", + "maxWidth": "Max Width", + "maxHeight": "Max Height", + "textOptions": "Text Options", + "justifyText": "Justify Text", + "hyphenate": "Hyphenate" + }, + "footer": { + "previousSection": "Previous Section", + "nextSection": "Next Section", + "location": "Location", + "progress": "Progress", + "timeLeftInSection": "Time Left in Section", + "timeLeftInBook": "Time Left in Book", + "chapter": "Chapter", + "section": "Section", + "page": "Page", + "goTo": "Go to", + "go": "Go", + "goToPercentage": "Go to percentage", + "firstSection": "First Section", + "lastSection": "Last Section", + "jumpTo": "Jump To..." + }, + "header": { + "chapters": "Chapters", + "addBookmark": "Add Bookmark", + "removeBookmark": "Remove Bookmark", + "search": "Search", + "notes": "Notes", + "fullscreen": "Fullscreen", + "exitFullscreen": "Exit Fullscreen", + "keyboardShortcuts": "Keyboard Shortcuts", + "more": "More", + "settings": "Settings", + "closeReader": "Close Reader" + }, + "quickSettings": { + "darkMode": "Dark Mode", + "fontSize": "Font Size", + "lineSpacing": "Line Spacing", + "moreSettings": "More Settings" + }, + "panel": { + "searchTitle": "Search", + "notesTitle": "Notes", + "searchTab": "Search", + "notesTab": "Notes", + "searchPlaceholder": "Search in book...", + "clearSearch": "Clear search", + "searching": "Searching...", + "cancelSearch": "Cancel search", + "resultsFound": "{{ count }} result found", + "resultsFoundPlural": "{{ count }} results found", + "noResultsFound": "No results found", + "tryDifferentKeywords": "Try different keywords", + "searchThisBook": "Search this book", + "enterTextToFind": "Enter text to find in the book", + "searchNotes": "Search notes...", + "editNote": "Edit note", + "deleteNote": "Delete note", + "noMatchingNotes": "No matching notes", + "tryDifferentSearchTerms": "Try different search terms", + "noNotesYet": "No notes yet", + "noNotesHint": "Select text and tap the note icon to add a note" + }, + "sidebar": { + "bookCoverAlt": "Book cover", + "contentsTab": "Contents", + "bookmarksTab": "Bookmarks", + "highlightsTab": "Highlights", + "deleteBookmark": "Delete bookmark", + "noBookmarksYet": "No bookmarks yet", + "noBookmarksHint": "Tap the bookmark icon to save your place", + "deleteHighlight": "Delete highlight", + "noHighlightsYet": "No highlights yet", + "noHighlightsHint": "Select text to create a highlight" + }, + "selectionPopup": { + "copyText": "Copy Text", + "searchInBook": "Search in book", + "annotate": "Annotate", + "addNote": "Add Note", + "deleteAnnotation": "Delete Annotation" + }, + "shortcutsHelp": { + "title": "Keyboard Shortcuts", + "gotIt": "Got it", + "navigation": "Navigation", + "previousPage": "Previous page", + "nextPage": "Next page", + "firstSection": "First section", + "lastSection": "Last section", + "panels": "Panels", + "tableOfContents": "Table of contents", + "searchShortcut": "Search", + "notesShortcut": "Notes", + "display": "Display", + "toggleFullscreen": "Toggle fullscreen", + "exitFullscreenCloseDialogs": "Exit fullscreen / Close dialogs", + "other": "Other", + "showHelpDialog": "Show this help dialog", + "swipeRight": "Swipe right", + "swipeLeft": "Swipe left" + }, + "headerFooterUtil": { + "timeRemainingInSection": "Time remaining in section: {{ time }}" + }, + "toast": { + "noteSavedSummary": "Note Saved", + "noteSavedDetail": "Your note has been saved successfully.", + "noteUpdatedSummary": "Note Updated", + "noteUpdatedDetail": "Your note has been updated successfully.", + "saveFailedSummary": "Save Failed", + "saveFailedDetail": "Failed to save the note. Please try again.", + "updateFailedSummary": "Update Failed", + "updateFailedDetail": "Failed to update the note. Please try again.", + "bookmarkAddedSummary": "Bookmark Added", + "bookmarkAddedDetail": "Your bookmark was added successfully.", + "bookmarkExistsSummary": "Bookmark Already Exists", + "bookmarkExistsDetail": "You already have a bookmark at this location.", + "bookmarkFailedSummary": "Unable to Add Bookmark", + "bookmarkFailedDetail": "Something went wrong while adding the bookmark. Please try again.", + "highlightAddedSummary": "Highlight Added", + "highlightAddedDetail": "Your highlight was saved successfully.", + "highlightExistsSummary": "Highlight Already Exists", + "highlightExistsDetail": "You already have a highlight at this location.", + "highlightFailedSummary": "Unable to Add Highlight", + "highlightFailedDetail": "Something went wrong while adding the highlight. Please try again.", + "highlightRemovedSummary": "Highlight Removed", + "highlightRemovedDetail": "Your highlight was removed successfully.", + "highlightRemoveFailedSummary": "Unable to Remove Highlight", + "highlightRemoveFailedDetail": "Something went wrong while removing the highlight. Please try again.", + "noteAnnotationUpdatedSummary": "Note Updated", + "noteAnnotationUpdatedDetail": "Your note was saved successfully.", + "noteAnnotationUpdateFailedSummary": "Unable to Update Note", + "noteAnnotationUpdateFailedDetail": "Something went wrong while updating the note. Please try again." + } +} diff --git a/booklore-ui/src/i18n/en/settings-reader.json b/booklore-ui/src/i18n/en/settings-reader.json index 4cee32813..43c81490c 100644 --- a/booklore-ui/src/i18n/en/settings-reader.json +++ b/booklore-ui/src/i18n/en/settings-reader.json @@ -125,6 +125,10 @@ "black": "Black", "white": "White" }, + "toast": { + "preferencesUpdated": "Preferences Updated", + "preferencesUpdatedDetail": "Your preferences have been saved successfully." + }, "fonts": { "sectionTitle": "Custom Font Library", "sectionDesc": "Personalize your reading experience by uploading custom fonts to use with eBook formats (EPUB, FB2, MOBI, AZW3). Upload up to 10 custom font files that will be available for selection in the eBook reader settings.", diff --git a/booklore-ui/src/i18n/en/settings-tasks.json b/booklore-ui/src/i18n/en/settings-tasks.json index e427e4ad2..ca819d99a 100644 --- a/booklore-ui/src/i18n/en/settings-tasks.json +++ b/booklore-ui/src/i18n/en/settings-tasks.json @@ -59,6 +59,11 @@ "cancelFailed": "Cancellation Failed", "cancelError": "Failed to cancel the task. The task may already be completed or failed.", "cancelNoId": "Cannot cancel task without ID.", - "loadError": "Failed to load tasks" + "loadError": "Failed to load tasks", + "metadataScheduled": "Metadata Update Scheduled", + "metadataScheduledDetail": "The metadata update for the selected books has been successfully scheduled.", + "metadataAlreadyRunningDetail": "A metadata refresh task is already in progress. Please wait for it to complete before starting another one.", + "metadataFailed": "Metadata Update Failed", + "metadataFailedDetail": "An unexpected error occurred while scheduling the metadata update. Please try again later or contact support if the issue persists." } } diff --git a/booklore-ui/src/i18n/en/shared.json b/booklore-ui/src/i18n/en/shared.json index 2587a2e10..6f8fb4b7b 100644 --- a/booklore-ui/src/i18n/en/shared.json +++ b/booklore-ui/src/i18n/en/shared.json @@ -89,6 +89,26 @@ "foldersWillBeSelected": "{{ count }} folders will be selected", "selectDirectoriesBtn": "Select Directories" }, + "liveNotification": { + "defaultMessage": "No recent notifications..." + }, + "metadataProgress": { + "taskStalled": "Task stalled or backend unavailable", + "taskCancelled": "Task cancelled by user", + "cancellationScheduledSummary": "Cancellation Scheduled", + "cancellationScheduledDetail": "Task cancellation has been successfully scheduled", + "cancelFailedSummary": "Cancel Failed", + "cancelFailedDetail": "Failed to cancel the task. Please try again." + }, + "reader": { + "failedToLoadPages": "Failed to load pages", + "failedToLoadBook": "Failed to load the book" + }, + "settingsHelper": { + "settingsSavedSummary": "Settings Saved", + "settingsSavedDetail": "The settings were successfully saved!", + "saveErrorDetail": "There was an error saving the settings." + }, "setup": { "title": "Welcome to Booklore", "subtitle": "Setup your initial admin account to get started", diff --git a/booklore-ui/src/i18n/es/auth.json b/booklore-ui/src/i18n/es/auth.json index 68135dce1..7d83012f3 100644 --- a/booklore-ui/src/i18n/es/auth.json +++ b/booklore-ui/src/i18n/es/auth.json @@ -1,4 +1,8 @@ { + "oidc": { + "loginFailedSummary": "Error de inicio de sesión OIDC", + "redirectingDetail": "Redirigiendo al inicio de sesión local..." + }, "login": { "title": "Bienvenido de nuevo", "subtitle": "Inicia sesión para continuar tu viaje", diff --git a/booklore-ui/src/i18n/es/book.json b/booklore-ui/src/i18n/es/book.json new file mode 100644 index 000000000..26cc13a8d --- /dev/null +++ b/booklore-ui/src/i18n/es/book.json @@ -0,0 +1,649 @@ +{ + "browser": { + "confirm": { + "deleteMessage": "¿Estás seguro de que deseas eliminar {{ count }} libro(s)?\n\nEsto eliminará permanentemente los archivos de libros de tu sistema de archivos.\n\nEsta acción no se puede deshacer.", + "deleteHeader": "Confirmar eliminación", + "regenCoverMessage": "¿Estás seguro de que deseas regenerar las portadas de {{ count }} libro(s)?", + "regenCoverHeader": "Confirmar regeneración de portada", + "customCoverMessage": "¿Estás seguro de que deseas generar portadas personalizadas para {{ count }} libro(s)?", + "customCoverHeader": "Confirmar generación de portada personalizada" + }, + "toast": { + "sortSavedSummary": "Orden guardado", + "sortSavedGlobalDetail": "Configuración de orden predeterminada guardada.", + "sortSavedEntityDetail": "Configuración de orden guardada para este/a {{ entityType }}.", + "regenCoverStartedSummary": "Regeneración de portada iniciada", + "regenCoverStartedDetail": "Regenerando portadas para {{ count }} libro(s). Actualiza la página cuando termine.", + "customCoverStartedSummary": "Generación de portada personalizada iniciada", + "customCoverStartedDetail": "Generando portadas personalizadas para {{ count }} libro(s).", + "failedSummary": "Error", + "regenCoverFailedDetail": "No se pudo iniciar la regeneración de portadas.", + "customCoverFailedDetail": "No se pudo iniciar la generación de portadas personalizadas.", + "unshelveSuccessDetail": "Estantes de libros actualizados", + "unshelveFailedDetail": "Error al actualizar los estantes de libros", + "noEligibleBooksSummary": "Sin libros elegibles", + "noEligibleBooksDetail": "Los libros seleccionados deben ser de un solo archivo (sin formatos alternativos).", + "multipleLibrariesSummary": "Múltiples bibliotecas", + "multipleLibrariesDetail": "Todos los libros seleccionados deben ser de la misma biblioteca." + }, + "loading": { + "deleting": "Eliminando {{ count }} libro(s)...", + "unshelving": "Quitando {{ count }} libro(s) del estante..." + }, + "labels": { + "allBooks": "Todos los libros", + "unshelvedBooks": "Libros sin estante", + "unshelvedBooksFiltered": "Libros sin estante (Filtrados)", + "filteredSuffix": "(Filtrados)", + "seriesCollapsedInfo": "Mostrando {{ count }} {{ itemWord }} (series contraídas)", + "item": "elemento", + "items": "elementos", + "selected": "seleccionados", + "setAsDefault": "¿Establecer como predeterminado?", + "save": "Guardar", + "collapseSeries": "Contraer series", + "gridColumns": "Columnas de cuadrícula", + "gridItemSize": "Tamaño de elemento", + "bookTypeOverlay": "Indicador de tipo de libro", + "noBooks": "¡Esta colección no tiene libros!", + "failedLibrary": "¡Error al cargar los libros de la biblioteca!", + "failedShelf": "¡Error al cargar los libros del estante!", + "activeFilters": "{{ count }} filtros activos" + }, + "tooltip": { + "clearFilters": "Limpiar filtros aplicados", + "visibleColumns": "Columnas visibles", + "displaySettings": "Ajustes de visualización", + "selectSorting": "Seleccionar ordenamiento", + "toggleView": "Alternar entre vista de cuadrícula y tabla", + "search": "Buscar", + "toggleSidebar": "Alternar filtros laterales", + "metadataActions": "Acciones de metadatos", + "assignToShelf": "Asignar a estante", + "removeFromShelf": "Quitar de este estante", + "lockUnlockMetadata": "Bloquear/Desbloquear metadatos", + "organizeFiles": "Organizar archivos", + "attachToBook": "Adjuntar a otro libro", + "moreActions": "Más acciones", + "selectAll": "Seleccionar todos los libros", + "deselectAll": "Deseleccionar todos los libros", + "deleteSelected": "Eliminar libros seleccionados" + }, + "placeholder": { + "selectColumns": "Seleccionar columnas", + "search": "Título, Autor, Serie, Género o ISBN..." + } + }, + "card": { + "menu": { + "assignShelf": "Asignar estante", + "viewDetails": "Ver detalles", + "download": "Descargar", + "delete": "Eliminar", + "emailBook": "Enviar por correo", + "quickSend": "Envío rápido", + "customSend": "Envío personalizado", + "metadata": "Metadatos", + "searchMetadata": "Buscar metadatos", + "autoFetch": "Obtener automáticamente", + "customFetch": "Obtención personalizada", + "regenerateCover": "Regenerar portada (archivo)", + "generateCustomCover": "Generar portada personalizada", + "moreActions": "Más acciones", + "organizeFile": "Organizar archivo", + "readStatus": "Estado de lectura", + "resetBookloreProgress": "Restablecer progreso de Booklore", + "resetKOReaderProgress": "Restablecer progreso de KOReader", + "book": "Libro", + "loading": "Cargando..." + }, + "confirm": { + "deleteBookMessage": "¿Estás seguro de que deseas eliminar \"{{ title }}\"?\n\nEsto eliminará permanentemente el archivo del libro de tu sistema de archivos.\n\nEsta acción no se puede deshacer.", + "deleteBookHeader": "Confirmar eliminación", + "deleteFileMessage": "¿Estás seguro de que deseas eliminar el archivo adicional \"{{ fileName }}\"?", + "deleteFileHeader": "Confirmar eliminación de archivo" + }, + "toast": { + "quickSendSuccessDetail": "El envío del libro ha sido programado.", + "quickSendErrorDetail": "Ocurrió un error al enviar el libro.", + "deleteFileSuccessDetail": "Archivo adicional \"{{ fileName }}\" eliminado correctamente", + "deleteFileErrorDetail": "Error al eliminar el archivo adicional: {{ error }}", + "readStatusUpdatedSummary": "Estado de lectura actualizado", + "readStatusUpdatedDetail": "Marcado como \"{{ label }}\"", + "readStatusFailedSummary": "Error al actualizar", + "readStatusFailedDetail": "No se pudo actualizar el estado de lectura.", + "progressResetSummary": "Progreso restablecido", + "progressResetBookloreDetail": "El progreso de lectura de Booklore ha sido restablecido.", + "progressResetKOReaderDetail": "El progreso de lectura de KOReader ha sido restablecido.", + "progressResetFailedSummary": "Error", + "progressResetBookloreFailedDetail": "No se pudo restablecer el progreso de Booklore.", + "progressResetKOReaderFailedDetail": "No se pudo restablecer el progreso de KOReader.", + "coverRegenSuccessSummary": "Correcto", + "coverRegenSuccessDetail": "Regeneración de portada iniciada", + "coverRegenFailedDetail": "Error al regenerar la portada", + "customCoverSuccessSummary": "Correcto", + "customCoverSuccessDetail": "Portada generada correctamente", + "customCoverFailedDetail": "Error al generar la portada" + }, + "alt": { + "cover": "Portada de {{ title }}", + "titleTooltip": "Título: {{ title }}", + "seriesCollapsed": "Series contraídas: {{ count }} libros" + } + }, + "notes": { + "confirm": { + "deleteMessage": "¿Estás seguro de que deseas eliminar la nota \"{{ title }}\"?", + "deleteHeader": "Confirmar eliminación" + }, + "toast": { + "loadFailedDetail": "Error al cargar las notas de este libro.", + "validationSummary": "Error de validación", + "validationDetail": "El título y el contenido son obligatorios.", + "createSuccessDetail": "Nota creada correctamente.", + "createFailedDetail": "Error al crear la nota.", + "updateSuccessDetail": "Nota actualizada correctamente.", + "updateFailedDetail": "Error al actualizar la nota.", + "deleteSuccessDetail": "Nota eliminada correctamente.", + "deleteFailedDetail": "Error al eliminar la nota." + } + }, + "bookService": { + "toast": { + "someFilesNotDeletedSummary": "Algunos archivos no pudieron ser eliminados", + "someFilesNotDeletedDetail": "Libros: {{ fileNames }}", + "booksDeletedSummary": "Libros eliminados", + "booksDeletedDetail": "{{ count }} libro(s) eliminado(s) correctamente.", + "deleteFailedSummary": "Error al eliminar", + "deleteFailedDetail": "Ocurrió un error al eliminar los libros.", + "physicalBookCreatedSummary": "Libro físico creado", + "physicalBookCreatedDetail": "\"{{ title }}\" ha sido añadido a tu biblioteca.", + "creationFailedSummary": "Error al crear", + "creationFailedDetail": "Ocurrió un error al crear el libro físico.", + "fileDeletedSummary": "Archivo eliminado", + "additionalFileDeletedDetail": "Archivo adicional eliminado correctamente.", + "bookFileDeletedDetail": "Archivo de libro eliminado correctamente.", + "fileDeleteFailedSummary": "Error al eliminar", + "fileDeleteFailedDetail": "Ocurrió un error al eliminar el archivo.", + "fileUploadedSummary": "Archivo subido", + "fileUploadedDetail": "Archivo adicional subido correctamente.", + "uploadFailedSummary": "Error al subir", + "uploadFailedDetail": "Ocurrió un error al subir el archivo.", + "fieldLockFailedSummary": "Error al actualizar bloqueo de campo", + "fieldLockFailedDetail": "No se pudieron actualizar los bloqueos de campos de metadatos. Inténtalo de nuevo.", + "filesAttachedSummary": "Archivos adjuntados", + "filesAttachedDetail": "{{ count }} archivo(s) de libro adjuntado(s) correctamente.", + "attachmentFailedSummary": "Error al adjuntar", + "attachmentFailedDetail": "Ocurrió un error al adjuntar los archivos." + } + }, + "menuService": { + "menu": { + "autoFetchMetadata": "Obtener metadatos automáticamente", + "customFetchMetadata": "Obtención personalizada de metadatos", + "bulkMetadataEditor": "Editor masivo de metadatos", + "multiBookMetadataEditor": "Editor de metadatos multi-libro", + "regenerateCovers": "Regenerar portadas", + "generateCustomCovers": "Generar portadas personalizadas", + "updateReadStatus": "Actualizar estado de lectura", + "setAgeRating": "Establecer clasificación por edad", + "clearAgeRating": "Borrar clasificación por edad", + "setContentRating": "Establecer clasificación de contenido", + "clearContentRating": "Borrar clasificación de contenido", + "removeFromAllShelves": "Eliminar de todos los estantes", + "resetBookloreProgress": "Restablecer progreso de Booklore", + "resetKOReaderProgress": "Restablecer progreso de KOReader" + }, + "confirm": { + "readStatusMessage": "¿Estás seguro de que deseas marcar {{ count }} libro(s) como \"{{ label }}\"?", + "readStatusHeader": "Confirmar actualización de estado de lectura", + "ageRatingMessage": "¿Estás seguro de que deseas establecer la clasificación por edad a \"{{ label }}\" para {{ count }} libro(s)?", + "ageRatingHeader": "Confirmar actualización de clasificación por edad", + "clearAgeRatingMessage": "¿Estás seguro de que deseas borrar la clasificación por edad de {{ count }} libro(s)?", + "clearAgeRatingHeader": "Confirmar borrado de clasificación por edad", + "contentRatingMessage": "¿Estás seguro de que deseas establecer la clasificación de contenido a \"{{ label }}\" para {{ count }} libro(s)?", + "contentRatingHeader": "Confirmar actualización de clasificación de contenido", + "clearContentRatingMessage": "¿Estás seguro de que deseas borrar la clasificación de contenido de {{ count }} libro(s)?", + "clearContentRatingHeader": "Confirmar borrado de clasificación de contenido", + "unshelveMessage": "¿Estás seguro de que deseas eliminar {{ count }} libro(s) de TODOS sus estantes?", + "unshelveHeader": "Confirmar eliminación de estantes", + "resetBookloreMessage": "¿Estás seguro de que deseas restablecer el progreso de lectura de Booklore para {{ count }} libro(s)?", + "resetKOReaderMessage": "¿Estás seguro de que deseas restablecer el progreso de lectura de KOReader para {{ count }} libro(s)?", + "resetHeader": "Confirmar restablecimiento" + }, + "toast": { + "readStatusUpdatedSummary": "Estado de lectura actualizado", + "readStatusUpdatedDetail": "Marcado como \"{{ label }}\"", + "updateFailedSummary": "Error al actualizar", + "readStatusFailedDetail": "No se pudo actualizar el estado de lectura.", + "ageRatingUpdatedSummary": "Clasificación por edad actualizada", + "ageRatingUpdatedDetail": "Establecido a \"{{ label }}\"", + "ageRatingFailedDetail": "No se pudo actualizar la clasificación por edad.", + "ageRatingClearedSummary": "Clasificación por edad borrada", + "ageRatingClearedDetail": "La clasificación por edad ha sido borrada.", + "clearAgeRatingFailedDetail": "No se pudo borrar la clasificación por edad.", + "contentRatingUpdatedSummary": "Clasificación de contenido actualizada", + "contentRatingUpdatedDetail": "Establecido a \"{{ label }}\"", + "contentRatingFailedDetail": "No se pudo actualizar la clasificación de contenido.", + "contentRatingClearedSummary": "Clasificación de contenido borrada", + "contentRatingClearedDetail": "La clasificación de contenido ha sido borrada.", + "clearContentRatingFailedDetail": "No se pudo borrar la clasificación de contenido.", + "noBooksOnShelvesDetail": "Los libros seleccionados no están en ningún estante.", + "unshelveSuccessDetail": "Libros eliminados de todos los estantes", + "unshelveFailedDetail": "Error al actualizar los estantes de libros", + "progressResetSummary": "Progreso restablecido", + "bookloreProgressResetDetail": "El progreso de lectura de Booklore ha sido restablecido.", + "koreaderProgressResetDetail": "El progreso de lectura de KOReader ha sido restablecido.", + "failedSummary": "Error", + "progressResetFailedDetail": "No se pudo restablecer el progreso." + }, + "loading": { + "updatingReadStatus": "Actualizando estado de lectura para {{ count }} libro(s)...", + "settingAgeRating": "Estableciendo clasificación por edad para {{ count }} libro(s)...", + "clearingAgeRating": "Borrando clasificación por edad de {{ count }} libro(s)...", + "settingContentRating": "Estableciendo clasificación de contenido para {{ count }} libro(s)...", + "clearingContentRating": "Borrando clasificación de contenido de {{ count }} libro(s)...", + "removingFromShelves": "Eliminando {{ count }} libro(s) de los estantes...", + "resettingBookloreProgress": "Restableciendo progreso de Booklore para {{ count }} libro(s)...", + "resettingKOReaderProgress": "Restableciendo progreso de KOReader para {{ count }} libro(s)..." + } + }, + "shelfMenuService": { + "library": { + "optionsLabel": "Opciones", + "addPhysicalBook": "Añadir libro físico", + "editLibrary": "Editar biblioteca", + "rescanLibrary": "Re-escanear biblioteca", + "customFetchMetadata": "Obtención personalizada de metadatos", + "autoFetchMetadata": "Obtener metadatos automáticamente", + "deleteLibrary": "Eliminar biblioteca" + }, + "shelf": { + "publicShelfPrefix": "Estante público - ", + "readOnly": "Solo lectura", + "optionsLabel": "Opciones", + "editShelf": "Editar estante", + "deleteShelf": "Eliminar estante" + }, + "magicShelf": { + "optionsLabel": "Opciones", + "editMagicShelf": "Editar estante mágico", + "deleteMagicShelf": "Eliminar estante mágico" + }, + "confirm": { + "rescanLibraryMessage": "¿Estás seguro de que deseas actualizar la biblioteca: {{ name }}?", + "deleteLibraryMessage": "¿Estás seguro de que deseas eliminar la biblioteca: {{ name }}?", + "deleteShelfMessage": "¿Estás seguro de que deseas eliminar el estante: {{ name }}?", + "deleteMagicShelfMessage": "¿Estás seguro de que deseas eliminar el estante mágico: {{ name }}?", + "header": "Confirmación", + "rescanLabel": "Re-escanear" + }, + "toast": { + "libraryRefreshSuccessDetail": "Actualización de biblioteca programada", + "libraryRefreshFailedDetail": "Error al actualizar la biblioteca", + "libraryDeletedDetail": "Biblioteca eliminada", + "libraryDeleteFailedDetail": "Error al eliminar la biblioteca", + "shelfDeletedDetail": "Estante eliminado", + "shelfDeleteFailedDetail": "Error al eliminar el estante", + "magicShelfDeletedDetail": "Estante mágico eliminado", + "magicShelfDeleteFailedDetail": "Error al eliminar el estante", + "failedSummary": "Error" + }, + "loading": { + "deletingLibrary": "Eliminando biblioteca '{{ name }}'..." + } + }, + "columnPref": { + "columns": { + "readStatus": "Lectura", + "title": "Título", + "authors": "Autores", + "publisher": "Editorial", + "seriesName": "Serie", + "seriesNumber": "Serie #", + "categories": "Géneros", + "publishedDate": "Publicado", + "lastReadTime": "Última lectura", + "addedOn": "Añadido", + "fileName": "Nombre de archivo", + "fileSizeKb": "Tamaño", + "language": "Idioma", + "isbn": "ISBN", + "pageCount": "Páginas", + "amazonRating": "Amazon", + "amazonReviewCount": "AZ #", + "goodreadsRating": "Goodreads", + "goodreadsReviewCount": "GR #", + "hardcoverRating": "Hardcover", + "hardcoverReviewCount": "HC #", + "ranobedbRating": "Ranobedb" + }, + "toast": { + "savedSummary": "Preferencias guardadas", + "savedDetail": "Tu diseño de columnas ha sido guardado." + } + }, + "coverPref": { + "toast": { + "savedSummary": "Tamaño de portada guardado", + "savedDetail": "Tamaño de portada establecido a {{ scale }}x.", + "saveFailedSummary": "Error al guardar", + "saveFailedDetail": "No se pudo guardar la preferencia de tamaño de portada localmente." + } + }, + "filterPref": { + "toast": { + "saveFailedSummary": "Error al guardar", + "saveFailedDetail": "No se pudo guardar la preferencia del filtro lateral localmente." + } + }, + "reviews": { + "confirm": { + "deleteAllMessage": "¿Estás seguro de que deseas eliminar las {{ count }} reseñas de este libro? Esta acción no se puede deshacer.", + "deleteAllHeader": "Confirmar eliminación de todas", + "deleteMessage": "¿Estás seguro de que deseas eliminar esta reseña de {{ reviewer }}?", + "deleteHeader": "Confirmar eliminación" + }, + "toast": { + "loadFailedSummary": "Error al cargar reseñas", + "loadFailedDetail": "No se pudieron cargar las reseñas de este libro.", + "reviewsUpdatedSummary": "Reseñas actualizadas", + "reviewsUpdatedDetail": "Las últimas reseñas se han obtenido correctamente.", + "fetchFailedSummary": "Error al obtener", + "fetchFailedDetail": "No se pudieron obtener las nuevas reseñas de este libro.", + "allDeletedSummary": "Todas las reseñas eliminadas", + "allDeletedDetail": "Todas las reseñas han sido eliminadas correctamente.", + "deleteAllFailedSummary": "Error al eliminar", + "deleteAllFailedDetail": "No se pudieron eliminar todas las reseñas.", + "deleteSuccessSummary": "Reseña eliminada", + "deleteSuccessDetail": "La reseña ha sido eliminada correctamente.", + "deleteFailedSummary": "Error al eliminar", + "deleteFailedDetail": "No se pudo eliminar la reseña.", + "lockedSummary": "Reseñas bloqueadas", + "lockedDetail": "Las reseñas ahora están protegidas contra modificaciones y actualizaciones.", + "unlockedSummary": "Reseñas desbloqueadas", + "unlockedDetail": "Las reseñas ahora pueden ser modificadas y actualizadas.", + "lockFailedSummary": "Error al cambiar bloqueo", + "lockFailedDetail": "No se pudo cambiar el estado de bloqueo de las reseñas." + }, + "labels": { + "loadingReviews": "Obteniendo las últimas reseñas...", + "showSpoiler": "Mostrar spoiler", + "anonymous": "Anónimo", + "spoiler": "Spoiler" + }, + "empty": { + "noReviews": "No hay reseñas disponibles para este libro", + "downloadsDisabled": "La descarga de reseñas está desactivada. Actívala en la configuración de metadatos para obtener reseñas.", + "fetchPrompt": "Haz clic en \"Obtener reseñas\" para descargar reseñas de los proveedores configurados", + "noContent": "No hay contenido de reseña disponible" + }, + "tooltip": { + "fetchReviews": "Obtener reseñas", + "reviewsLocked": "Las reseñas están bloqueadas", + "deleteReview": "Eliminar reseña", + "pleaseWait": "Espere mientras se obtienen las reseñas", + "enableDownloads": "Active la descarga de reseñas en la configuración para usar esta función", + "unlockReviews": "Desbloquear reseñas", + "lockReviews": "Bloquear reseñas", + "fetchNewReviews": "Obtener nuevas reseñas", + "deleteAllReviews": "Eliminar todas las reseñas", + "hideAllSpoilers": "Ocultar todos los spoilers", + "revealAllSpoilers": "Revelar todos los spoilers", + "sortNewest": "Ordenar por más recientes", + "sortOldest": "Ordenar por más antiguos" + } + }, + "addPhysicalBook": { + "title": "Agregar libro físico", + "description": "Catalogar un libro físico sin archivo digital", + "closeTooltip": "Cerrar", + "libraryLabel": "Biblioteca", + "libraryPlaceholder": "Seleccionar una biblioteca", + "titleLabel": "Título", + "titlePlaceholder": "ej., El gran Gatsby", + "isbnLabel": "ISBN", + "isbnPlaceholder": "ej., 9780134685991", + "authorsLabel": "Autores", + "authorsPlaceholder": "Escriba el nombre del autor y presione Enter", + "descriptionLabel": "Descripción", + "descriptionPlaceholder": "Breve descripción del libro...", + "publisherLabel": "Editorial", + "publisherPlaceholder": "Nombre de la editorial", + "publishedDateLabel": "Fecha de publicación", + "publishedDatePlaceholder": "ej., 2020 o 2020-05-15", + "languageLabel": "Idioma", + "languagePlaceholder": "ej., Español", + "pageCountLabel": "Número de páginas", + "pageCountPlaceholder": "Número de páginas", + "categoriesLabel": "Categorías/Géneros", + "categoriesPlaceholder": "Escriba la categoría y presione Enter", + "validationLibraryRequired": "La biblioteca es obligatoria", + "validationTitleOrIsbn": "Se requiere un título o ISBN", + "validationReady": "Listo para crear", + "cancelButton": "Cancelar", + "addButton": "Agregar libro físico" + }, + "fileUploader": { + "title": "Subir archivo adicional", + "unknownTitle": "Título desconocido", + "selectFileType": "Seleccionar tipo de archivo", + "typeAlternativeFormat": "Formato alternativo", + "typeSupplementary": "Archivo complementario", + "descriptionLabel": "Descripción (Opcional)", + "descriptionPlaceholder": "Agregar una descripción para este archivo...", + "statusUploading": "Subiendo", + "statusUploaded": "Subido", + "statusUploadFailed": "Error al subir", + "statusTooLarge": "Demasiado grande", + "statusReady": "Listo", + "statusFailed": "Fallido", + "dragDropText": "Arrastre y suelte un archivo aquí para subir.", + "uploadForBook": "Subir un archivo adicional para {{ title }}.", + "thisBook": "este libro", + "toast": { + "fileTooLargeSummary": "Archivo demasiado grande", + "fileTooLargeDetail": "{{ fileName }} excede el tamaño máximo de {{ maxSize }}", + "fileTooLargeError": "El archivo excede el tamaño máximo de {{ maxSize }}", + "uploadFailedUnknown": "La subida falló por un error desconocido." + } + }, + "filter": { + "title": "Filtros", + "showingFirst100": "Mostrando los primeros 100 elementos", + "footerNote": "Nota: Se muestran los 100 primeros elementos por categoría de filtro" + }, + "shelfAssigner": { + "title": "Asignar libros a estantes", + "descriptionMulti": "Seleccionar estantes para {{ count }} libro(s)", + "descriptionSingle": "Organizar \"{{ title }}\" en tus estantes", + "shelvesAvailable": "{{ count }} estante(s) disponible(s)", + "emptyTitle": "No hay estantes disponibles", + "emptyDescription": "Crea tu primer estante para empezar a organizar tus libros.
Haz clic en el botón de abajo para comenzar.", + "createShelf": "Crear estante", + "cancelButton": "Cancelar", + "saveChanges": "Guardar cambios", + "loading": { + "updatingShelves": "Actualizando estantes para {{ count }} libro(s)..." + }, + "toast": { + "updateSuccessDetail": "Estantes de libros actualizados", + "updateFailedDetail": "Error al actualizar los estantes de libros" + } + }, + "shelfCreator": { + "title": "Crear nuevo estante", + "description": "Agrega un estante personalizado para organizar tus libros", + "closeTooltip": "Cerrar", + "shelfNameLabel": "Nombre del estante", + "shelfNamePlaceholder": "ej., Favoritos, Por leer, Leyendo actualmente", + "shelfIconLabel": "Icono del estante (Opcional)", + "chooseIcon": "Elegir un icono", + "chooseIconSubtitle": "Seleccionar de los iconos disponibles", + "selectedIcon": "Icono seleccionado", + "removeIconTooltip": "Eliminar icono", + "visibilityLabel": "Visibilidad", + "makePublicLabel": "Hacer este estante público (solo lectura para otros)", + "validationRequired": "El nombre del estante es obligatorio", + "validationReady": "Listo para crear", + "cancelButton": "Cancelar", + "createButton": "Crear estante", + "toast": { + "createSuccessDetail": "Estante creado: {{ name }}", + "createFailedDetail": "Error al crear el estante" + } + }, + "shelfEditDialog": { + "title": "Editar estante", + "description": "Personaliza el nombre y el icono de tu estante", + "shelfNameLabel": "Nombre del estante:", + "shelfNamePlaceholder": "Ingrese el nombre del estante...", + "shelfIconLabel": "Icono del estante:", + "selectIcon": "Seleccionar icono", + "visibilityLabel": "Visibilidad:", + "publicShelfLabel": "Estante público", + "cancelButton": "Cancelar", + "saveChanges": "Guardar cambios", + "toast": { + "updateSuccessSummary": "Estante actualizado", + "updateSuccessDetail": "El estante se actualizó correctamente.", + "updateFailedSummary": "Error al actualizar", + "updateFailedDetail": "Ocurrió un error al actualizar el estante. Por favor, inténtelo de nuevo." + } + }, + "table": { + "bookCoverAlt": "Portada del libro", + "statusPrefix": "Estado: ", + "locked": "Bloqueado", + "unlocked": "Desbloqueado", + "toast": { + "metadataLockedSummary": "Metadatos bloqueados", + "metadataLockedDetail": "Los metadatos del libro se han bloqueado correctamente.", + "metadataUnlockedSummary": "Metadatos desbloqueados", + "metadataUnlockedDetail": "Los metadatos del libro se han desbloqueado correctamente.", + "lockFailedSummary": "Error al bloquear", + "lockFailedDetail": "Se produjo un error al bloquear los metadatos.", + "unlockFailedSummary": "Error al desbloquear", + "unlockFailedDetail": "Se produjo un error al desbloquear los metadatos." + } + }, + "lockUnlockDialog": { + "title": "Bloquear o desbloquear metadatos", + "selectedCount": "{{ count }} libro(s) seleccionados", + "reset": "Restablecer", + "lockAll": "Bloquear todo", + "unlockAll": "Desbloquear todo", + "save": "Guardar", + "saving": "Guardando...", + "locked": "Bloqueado", + "unlocked": "Desbloqueado", + "unselected": "Sin seleccionar", + "toast": { + "updatingFieldLocks": "Actualizando bloqueos de campos...", + "updatedSummary": "Bloqueos actualizados", + "updatedDetail": "Los campos de metadatos seleccionados se han actualizado correctamente.", + "failedSummary": "Error al actualizar bloqueos", + "failedDetail": "Se produjo un error al actualizar los estados de bloqueo de campos." + } + }, + "sorting": { + "sortOrder": "Orden de clasificación", + "removeTooltip": "Eliminar", + "addSortFieldPlaceholder": "Agregar campo de orden...", + "saveAsDefault": "Guardar como predeterminado", + "ascendingTooltip": "Ascendente - clic para cambiar", + "descendingTooltip": "Descendente - clic para cambiar" + }, + "fileAttacher": { + "title": "Adjuntar archivo a otro libro", + "titleBulk": "Adjuntar archivos a otro libro", + "description": "Mover el archivo de este libro a otro libro como formato alternativo", + "descriptionBulk": "Mover los archivos de estos libros a otro libro como formatos alternativos", + "sourceBookLabel": "Libro de origen ({{ count }})", + "sourceBooksLabel": "Libros de origen ({{ count }})", + "unknownTitle": "Titulo desconocido", + "selectTargetBook": "Seleccionar libro de destino", + "searchPlaceholder": "Buscar un libro en la misma biblioteca...", + "deleteSource": "Eliminar libro de origen tras adjuntar", + "deleteSourceBulk": "Eliminar libros de origen tras adjuntar", + "warningMove": "Esto moverá el archivo del libro de origen al libro de destino como formato alternativo.", + "warningMoveBulk": "Esto moverá los archivos de los libros seleccionados al libro de destino como formatos alternativos.", + "warningDelete": "El registro del libro de origen será eliminado (el archivo se conservará en el libro de destino).", + "warningDeleteBulk": "Los registros de los libros de origen serán eliminados (los archivos se conservarán en el libro de destino).", + "warningKeep": "El registro del libro de origen permanecerá, pero no tendrá archivos legibles.", + "warningKeepBulk": "Los registros de los libros de origen permanecerán, pero no tendrán archivos legibles.", + "attachFile": "Adjuntar archivo", + "attachFilesBulk": "Adjuntar archivos", + "unknownFile": "Archivo desconocido", + "unknownFormat": "Desconocido", + "unknownFilename": "Nombre de archivo desconocido" + }, + "searcher": { + "placeholder": "Título, autor, serie, género o ISBN...", + "clearSearch": "Borrar búsqueda", + "bookCoverAlt": "Portada del libro", + "byPrefix": "por", + "noResults": "No se encontraron resultados", + "unknownAuthor": "Autor desconocido" + }, + "sender": { + "title": "Enviar libro", + "description": "Enviar este libro por correo electrónico a un destinatario", + "emailProvider": "Proveedor de correo", + "selectProvider": "Seleccionar proveedor de correo", + "recipient": "Destinatario", + "selectRecipient": "Seleccionar destinatario del libro", + "fileFormat": "Formato de archivo", + "unknownFormat": "Desconocido", + "primaryBadge": "Principal", + "largeFileWarning": "Este archivo supera los 25 MB. Algunos proveedores de correo pueden rechazar archivos adjuntos grandes.", + "sendBook": "Enviar libro", + "toast": { + "emailScheduledSummary": "Correo programado", + "emailScheduledDetail": "El libro ha sido programado para su envío correctamente.", + "sendingFailedSummary": "Error de envío", + "sendingFailedDetail": "Hubo un problema al programar el envío del libro. Inténtelo de nuevo más tarde.", + "providerMissingSummary": "Proveedor de correo no seleccionado", + "providerMissingDetail": "Seleccione un proveedor de correo para continuar.", + "recipientMissingSummary": "Destinatario no seleccionado", + "recipientMissingDetail": "Seleccione un destinatario para enviar el libro.", + "bookNotSelectedSummary": "Libro no seleccionado", + "bookNotSelectedDetail": "Seleccione un libro para enviar." + } + }, + "seriesPage": { + "seriesDetailsTab": "Detalles de la serie", + "publisher": "Editorial:", + "years": "Años:", + "numberOfBooks": "Número de libros:", + "language": "Idioma:", + "readStatus": "Estado de lectura:", + "noDescription": "No hay descripción disponible.", + "showLess": "Mostrar menos", + "showMore": "Mostrar más", + "noBooksFound": "No se encontraron libros para esta serie.", + "selected": "seleccionados", + "loadingSeriesDetails": "Cargando detalles de la serie...", + "status": { + "unread": "NO LEÍDO", + "reading": "LEYENDO", + "reReading": "RELEYENDO", + "read": "LEÍDO", + "partiallyRead": "PARCIALMENTE LEÍDO", + "paused": "EN PAUSA", + "abandoned": "ABANDONADO", + "wontRead": "NO LEERÉ", + "unset": "SIN ESTADO" + }, + "tooltip": { + "metadataActions": "Acciones de metadatos", + "assignToShelf": "Asignar a estante", + "lockUnlockMetadata": "Bloquear/Desbloquear metadatos", + "organizeFiles": "Organizar archivos", + "moreActions": "Más acciones", + "selectAll": "Seleccionar todos los libros", + "deselectAll": "Deseleccionar todos los libros", + "deleteSelected": "Eliminar libros seleccionados" + } + } +} diff --git a/booklore-ui/src/i18n/es/index.ts b/booklore-ui/src/i18n/es/index.ts index 34200c0c6..57ef8c05a 100644 --- a/booklore-ui/src/i18n/es/index.ts +++ b/booklore-ui/src/i18n/es/index.ts @@ -24,8 +24,12 @@ import libraryCreator from './library-creator.json'; import bookdrop from './bookdrop.json'; import metadata from './metadata.json'; import notebook from './notebook.json'; +import book from './book.json'; +import readerAudiobook from './reader-audiobook.json'; +import readerCbx from './reader-cbx.json'; +import readerEbook from './reader-ebook.json'; // To add a new domain: create the JSON file and add it here. // Settings tabs each get their own file: settings-email, settings-reader, settings-view, etc. -const translations: Translation = {common, auth, nav, dashboard, settings, settingsEmail, settingsReader, settingsView, settingsMeta, settingsLibMeta, settingsApp, settingsUsers, settingsNaming, settingsOpds, settingsTasks, settingsAuth, settingsDevice, settingsProfile, app, shared, layout, libraryCreator, bookdrop, metadata, notebook}; +const translations: Translation = {common, auth, nav, dashboard, settings, settingsEmail, settingsReader, settingsView, settingsMeta, settingsLibMeta, settingsApp, settingsUsers, settingsNaming, settingsOpds, settingsTasks, settingsAuth, settingsDevice, settingsProfile, app, shared, layout, libraryCreator, bookdrop, metadata, notebook, book, readerAudiobook, readerCbx, readerEbook}; export default translations; diff --git a/booklore-ui/src/i18n/es/reader-audiobook.json b/booklore-ui/src/i18n/es/reader-audiobook.json new file mode 100644 index 000000000..763269170 --- /dev/null +++ b/booklore-ui/src/i18n/es/reader-audiobook.json @@ -0,0 +1,64 @@ +{ + "loading": "Cargando audiolibro...", + "untitled": "Sin titulo", + "unknownAuthor": "Autor desconocido", + "coverAlt": "Portada", + "header": { + "backTooltip": "Volver", + "bookmarksTooltip": "Marcadores", + "chaptersTracksTooltip": "Capitulos/Pistas" + }, + "trackInfo": { + "trackOf": "Pista {{ current }} de {{ total }}", + "chapterOf": "Capitulo {{ current }} de {{ total }}" + }, + "controls": { + "previousTrackTooltip": "Pista anterior", + "previousChapterTooltip": "Capitulo anterior", + "rewindTooltip": "-30s", + "forwardTooltip": "+30s", + "nextTrackTooltip": "Siguiente pista", + "nextChapterTooltip": "Siguiente capitulo" + }, + "extra": { + "addBookmark": "Agregar marcador", + "sleepTimer": "Temporizador", + "endOfChapter": "Fin del capitulo" + }, + "details": { + "narratedBy": "Narrado por {{ narrator }}", + "kbps": "{{ bitrate }} kbps", + "totalDuration": "{{ duration }} en total" + }, + "sidebar": { + "tracks": "Pistas", + "chapters": "Capitulos" + }, + "bookmarks": { + "title": "Marcadores", + "empty": "Sin marcadores", + "emptyHint": "Agrega un marcador para guardar tu posicion", + "deleteTooltip": "Eliminar", + "bookmarkAt": "Marcador en {{ time }}" + }, + "sleepTimerMenu": { + "minutes15": "15 minutos", + "minutes30": "30 minutos", + "minutes45": "45 minutos", + "minutes60": "60 minutos", + "endOfChapter": "Fin del capitulo", + "cancelTimer": "Cancelar temporizador" + }, + "toast": { + "loadFailed": "Error al cargar el audiolibro", + "audioLoadFailed": "Error al cargar el audio", + "sleepTimerSet": "La reproduccion se detendra en {{ minutes }} minutos", + "sleepTimerEndOfChapter": "La reproduccion se detendra al final del capitulo", + "sleepTimerStopped": "Reproduccion detenida por el temporizador", + "bookmarkAdded": "Marcador agregado", + "bookmarkExists": "El marcador ya existe", + "bookmarkExistsDetail": "Ya existe un marcador en esta posicion", + "bookmarkFailed": "Error al agregar el marcador", + "bookmarkDeleted": "Marcador eliminado" + } +} diff --git a/booklore-ui/src/i18n/es/reader-cbx.json b/booklore-ui/src/i18n/es/reader-cbx.json new file mode 100644 index 000000000..c24461b81 --- /dev/null +++ b/booklore-ui/src/i18n/es/reader-cbx.json @@ -0,0 +1,127 @@ +{ + "reader": { + "continueToNextBook": "Continuar al siguiente libro", + "noPagesAvailable": "No hay páginas disponibles.", + "loadingBook": "Cargando libro...", + "slideshow": "Presentación" + }, + "noteDialog": { + "editNote": "Editar nota", + "addNote": "Añadir nota", + "pageLabel": "Página", + "pageInfo": "Página {{ pageNumber }}", + "yourNote": "Tu nota", + "placeholder": "Escribe tu nota aquí...", + "noteColor": "Color de la nota", + "updateNote": "Actualizar nota", + "saveNote": "Guardar nota", + "colorAmber": "Ámbar", + "colorGreen": "Verde", + "colorBlue": "Azul", + "colorPink": "Rosa", + "colorPurple": "Púrpura", + "colorDeepOrange": "Naranja intenso" + }, + "shortcutsHelp": { + "title": "Atajos de teclado", + "gotIt": "Entendido", + "groupNavigation": "Navegación", + "groupDisplay": "Visualización", + "groupPlayback": "Reproducción", + "groupOther": "Otros", + "previousNextPage": "Página anterior / siguiente", + "swipeLeftRight": "Deslizar izquierda/derecha", + "nextPage": "Página siguiente", + "previousPage": "Página anterior", + "firstPage": "Primera página", + "lastPage": "Última página", + "toggleFullscreen": "Alternar pantalla completa", + "toggleReadingDirection": "Alternar dirección de lectura (IZQ/DER)", + "exitFullscreenCloseDialogs": "Salir de pantalla completa / Cerrar diálogos", + "toggleZoom": "Alternar zoom (ajustar página / tamaño real)", + "doubleTap": "Doble toque", + "toggleSlideshow": "Alternar presentación / reproducción automática", + "showHelpDialog": "Mostrar este diálogo de ayuda" + }, + "footer": { + "prevBook": "Libro anterior", + "nextBook": "Libro siguiente", + "firstPage": "Primera página", + "previousPage": "Página anterior", + "nextPage": "Página siguiente", + "lastPage": "Última página", + "of": "de", + "pageSlider": "Control de página", + "pagePlaceholder": "Página", + "go": "Ir", + "noPreviousBook": "No hay libro anterior", + "noNextBook": "No hay libro siguiente", + "previousBookTooltip": "Anterior: {{ title }}", + "nextBookTooltip": "Siguiente: {{ title }}" + }, + "header": { + "contents": "Contenido", + "addBookmark": "Añadir marcador", + "removeBookmark": "Eliminar marcador", + "addNote": "Añadir nota", + "pageHasNotesAddAnother": "La página tiene notas - Añadir otra", + "stopSlideshow": "Detener presentación (P)", + "startSlideshow": "Iniciar presentación (P)", + "exitFullscreen": "Salir de pantalla completa (F)", + "fullscreen": "Pantalla completa (F)", + "keyboardShortcuts": "Atajos de teclado (?)", + "more": "Más", + "stopSlideshowLabel": "Detener presentación", + "startSlideshowLabel": "Iniciar presentación", + "exitFullscreenLabel": "Salir de pantalla completa", + "fullscreenLabel": "Pantalla completa", + "keyboardShortcutsLabel": "Atajos de teclado", + "settings": "Ajustes", + "closeReader": "Cerrar lector" + }, + "quickSettings": { + "fitMode": "Modo de ajuste", + "fitPage": "Ajustar a página", + "fitWidth": "Ajustar al ancho", + "fitHeight": "Ajustar a la altura", + "actualSize": "Tamaño real", + "automatic": "Automático", + "scrollMode": "Modo de desplazamiento", + "paginated": "Paginado", + "infinite": "Infinito", + "longStrip": "Tira larga", + "pageView": "Vista de página", + "twoPage": "Dos páginas", + "single": "Simple", + "pageSpread": "Disposición de páginas", + "oddFirst": "Impar primero", + "evenFirst": "Par primero", + "readingDirection": "Dirección de lectura", + "leftToRight": "Izquierda a derecha", + "rightToLeft": "Derecha a izquierda", + "slideshowInterval": "Intervalo de presentación", + "background": "Fondo", + "black": "Negro", + "gray": "Gris", + "white": "Blanco" + }, + "sidebar": { + "contentTab": "Contenido", + "bookmarksTab": "Marcadores", + "notesTab": "Notas", + "noPagesFound": "No se encontraron páginas", + "noBookmarksYet": "Aún no hay marcadores", + "bookmarkHint": "Toca el icono de marcador para guardar tu posición", + "searchNotesPlaceholder": "Buscar notas...", + "noMatchingNotes": "No hay notas coincidentes", + "tryDifferentSearch": "Prueba con otros términos de búsqueda", + "noNotesYet": "Aún no hay notas", + "notesHint": "Toca el icono de notas para añadir una nota a la página actual", + "deleteBookmark": "Eliminar marcador", + "editNote": "Editar nota", + "deleteNote": "Eliminar nota", + "clearSearch": "Limpiar búsqueda", + "page": "Página", + "untitled": "Sin título" + } +} diff --git a/booklore-ui/src/i18n/es/reader-ebook.json b/booklore-ui/src/i18n/es/reader-ebook.json new file mode 100644 index 000000000..4fca81b67 --- /dev/null +++ b/booklore-ui/src/i18n/es/reader-ebook.json @@ -0,0 +1,194 @@ +{ + "reader": { + "loadingBook": "Cargando libro..." + }, + "metadataDialog": { + "title": "Información del libro", + "basicInformation": "Información básica", + "titleLabel": "Título", + "subtitle": "Subtítulo", + "authors": "Autor(es)", + "publisher": "Editorial", + "published": "Publicado", + "language": "Idioma", + "pages": "Páginas", + "unknown": "Desconocido", + "na": "N/D", + "series": "Serie", + "seriesName": "Nombre de la serie", + "bookNumber": "Número del libro", + "identifiers": "Identificadores", + "ratings": "Valoraciones", + "reviews": "reseñas", + "categories": "Categorías", + "tags": "Etiquetas", + "fileInformation": "Información del archivo", + "fileSize": "Tamaño del archivo", + "fileName": "Nombre del archivo", + "description": "Descripción" + }, + "noteDialog": { + "editNote": "Editar nota", + "addNote": "Añadir nota", + "selectedText": "Texto seleccionado", + "yourNote": "Tu nota", + "notePlaceholder": "Escribe tu nota aquí...", + "noteColor": "Color de la nota", + "updateNote": "Actualizar nota", + "saveNote": "Guardar nota" + }, + "settingsDialog": { + "themeTab": "Tema", + "typographyTab": "Tipografía", + "layoutTab": "Diseño", + "darkMode": "Modo oscuro", + "themeColors": "Colores del tema", + "annotationHighlighter": "Resaltador de anotaciones", + "fontSettings": "Configuración de fuente", + "fontSize": "Tamaño de fuente", + "lineHeight": "Altura de línea", + "fontFamily": "Familia de fuente", + "layout": "Diseño", + "readingFlow": "Flujo de lectura", + "paginated": "Paginado", + "scrolled": "Desplazamiento", + "maxColumns": "Columnas máximas", + "columnGap": "Espacio entre columnas", + "maxWidth": "Ancho máximo", + "maxHeight": "Altura máxima", + "textOptions": "Opciones de texto", + "justifyText": "Justificar texto", + "hyphenate": "Separar sílabas" + }, + "footer": { + "previousSection": "Sección anterior", + "nextSection": "Sección siguiente", + "location": "Ubicación", + "progress": "Progreso", + "timeLeftInSection": "Tiempo restante en la sección", + "timeLeftInBook": "Tiempo restante en el libro", + "chapter": "Capítulo", + "section": "Sección", + "page": "Página", + "goTo": "Ir a", + "go": "Ir", + "goToPercentage": "Ir al porcentaje", + "firstSection": "Primera sección", + "lastSection": "Última sección", + "jumpTo": "Saltar a..." + }, + "header": { + "chapters": "Capítulos", + "addBookmark": "Añadir marcador", + "removeBookmark": "Quitar marcador", + "search": "Buscar", + "notes": "Notas", + "fullscreen": "Pantalla completa", + "exitFullscreen": "Salir de pantalla completa", + "keyboardShortcuts": "Atajos de teclado", + "more": "Más", + "settings": "Configuración", + "closeReader": "Cerrar lector" + }, + "quickSettings": { + "darkMode": "Modo oscuro", + "fontSize": "Tamaño de fuente", + "lineSpacing": "Interlineado", + "moreSettings": "Más configuraciones" + }, + "panel": { + "searchTitle": "Buscar", + "notesTitle": "Notas", + "searchTab": "Buscar", + "notesTab": "Notas", + "searchPlaceholder": "Buscar en el libro...", + "clearSearch": "Borrar búsqueda", + "searching": "Buscando...", + "cancelSearch": "Cancelar búsqueda", + "resultsFound": "{{ count }} resultado encontrado", + "resultsFoundPlural": "{{ count }} resultados encontrados", + "noResultsFound": "No se encontraron resultados", + "tryDifferentKeywords": "Prueba con otras palabras clave", + "searchThisBook": "Buscar en este libro", + "enterTextToFind": "Escribe texto para buscar en el libro", + "searchNotes": "Buscar notas...", + "editNote": "Editar nota", + "deleteNote": "Eliminar nota", + "noMatchingNotes": "No se encontraron notas", + "tryDifferentSearchTerms": "Prueba con otros términos de búsqueda", + "noNotesYet": "Aún no hay notas", + "noNotesHint": "Selecciona texto y toca el icono de nota para añadir una" + }, + "sidebar": { + "bookCoverAlt": "Portada del libro", + "contentsTab": "Contenido", + "bookmarksTab": "Marcadores", + "highlightsTab": "Resaltados", + "deleteBookmark": "Eliminar marcador", + "noBookmarksYet": "Aún no hay marcadores", + "noBookmarksHint": "Toca el icono de marcador para guardar tu posición", + "deleteHighlight": "Eliminar resaltado", + "noHighlightsYet": "Aún no hay resaltados", + "noHighlightsHint": "Selecciona texto para crear un resaltado" + }, + "selectionPopup": { + "copyText": "Copiar texto", + "searchInBook": "Buscar en el libro", + "annotate": "Anotar", + "addNote": "Añadir nota", + "deleteAnnotation": "Eliminar anotación" + }, + "shortcutsHelp": { + "title": "Atajos de teclado", + "gotIt": "Entendido", + "navigation": "Navegación", + "previousPage": "Página anterior", + "nextPage": "Página siguiente", + "firstSection": "Primera sección", + "lastSection": "Última sección", + "panels": "Paneles", + "tableOfContents": "Tabla de contenido", + "searchShortcut": "Buscar", + "notesShortcut": "Notas", + "display": "Pantalla", + "toggleFullscreen": "Alternar pantalla completa", + "exitFullscreenCloseDialogs": "Salir de pantalla completa / Cerrar diálogos", + "other": "Otros", + "showHelpDialog": "Mostrar este diálogo de ayuda", + "swipeRight": "Deslizar a la derecha", + "swipeLeft": "Deslizar a la izquierda" + }, + "headerFooterUtil": { + "timeRemainingInSection": "Tiempo restante en la sección: {{ time }}" + }, + "toast": { + "noteSavedSummary": "Nota guardada", + "noteSavedDetail": "Tu nota se ha guardado correctamente.", + "noteUpdatedSummary": "Nota actualizada", + "noteUpdatedDetail": "Tu nota se ha actualizado correctamente.", + "saveFailedSummary": "Error al guardar", + "saveFailedDetail": "No se pudo guardar la nota. Inténtalo de nuevo.", + "updateFailedSummary": "Error al actualizar", + "updateFailedDetail": "No se pudo actualizar la nota. Inténtalo de nuevo.", + "bookmarkAddedSummary": "Marcador añadido", + "bookmarkAddedDetail": "Tu marcador se ha añadido correctamente.", + "bookmarkExistsSummary": "El marcador ya existe", + "bookmarkExistsDetail": "Ya tienes un marcador en esta ubicación.", + "bookmarkFailedSummary": "No se pudo añadir el marcador", + "bookmarkFailedDetail": "Algo salió mal al añadir el marcador. Inténtalo de nuevo.", + "highlightAddedSummary": "Resaltado añadido", + "highlightAddedDetail": "Tu resaltado se ha guardado correctamente.", + "highlightExistsSummary": "El resaltado ya existe", + "highlightExistsDetail": "Ya tienes un resaltado en esta ubicación.", + "highlightFailedSummary": "No se pudo añadir el resaltado", + "highlightFailedDetail": "Algo salió mal al añadir el resaltado. Inténtalo de nuevo.", + "highlightRemovedSummary": "Resaltado eliminado", + "highlightRemovedDetail": "Tu resaltado se ha eliminado correctamente.", + "highlightRemoveFailedSummary": "No se pudo eliminar el resaltado", + "highlightRemoveFailedDetail": "Algo salió mal al eliminar el resaltado. Inténtalo de nuevo.", + "noteAnnotationUpdatedSummary": "Nota actualizada", + "noteAnnotationUpdatedDetail": "Tu nota se ha guardado correctamente.", + "noteAnnotationUpdateFailedSummary": "No se pudo actualizar la nota", + "noteAnnotationUpdateFailedDetail": "Algo salió mal al actualizar la nota. Inténtalo de nuevo." + } +} diff --git a/booklore-ui/src/i18n/es/settings-reader.json b/booklore-ui/src/i18n/es/settings-reader.json index f9c46d718..9f30a216e 100644 --- a/booklore-ui/src/i18n/es/settings-reader.json +++ b/booklore-ui/src/i18n/es/settings-reader.json @@ -125,6 +125,10 @@ "black": "Negro", "white": "Blanco" }, + "toast": { + "preferencesUpdated": "Preferencias actualizadas", + "preferencesUpdatedDetail": "Tus preferencias se han guardado correctamente." + }, "fonts": { "sectionTitle": "Biblioteca de fuentes personalizadas", "sectionDesc": "Personaliza tu experiencia de lectura subiendo fuentes personalizadas para usar con formatos eBook (EPUB, FB2, MOBI, AZW3). Sube hasta 10 archivos de fuentes personalizadas que estarán disponibles para seleccionar en los ajustes del lector de eBooks.", diff --git a/booklore-ui/src/i18n/es/settings-tasks.json b/booklore-ui/src/i18n/es/settings-tasks.json index 7a0729435..d29841fe7 100644 --- a/booklore-ui/src/i18n/es/settings-tasks.json +++ b/booklore-ui/src/i18n/es/settings-tasks.json @@ -59,6 +59,11 @@ "cancelFailed": "Error de cancelación", "cancelError": "Error al cancelar la tarea. La tarea puede haber finalizado o fallado.", "cancelNoId": "No se puede cancelar la tarea sin ID.", - "loadError": "Error al cargar las tareas" + "loadError": "Error al cargar las tareas", + "metadataScheduled": "Actualización de metadatos programada", + "metadataScheduledDetail": "La actualización de metadatos para los libros seleccionados se ha programado correctamente.", + "metadataAlreadyRunningDetail": "Ya hay una tarea de actualización de metadatos en progreso. Por favor, espera a que finalice antes de iniciar otra.", + "metadataFailed": "Error en la actualización de metadatos", + "metadataFailedDetail": "Ocurrió un error inesperado al programar la actualización de metadatos. Por favor, inténtalo de nuevo más tarde o contacta con soporte si el problema persiste." } } diff --git a/booklore-ui/src/i18n/es/shared.json b/booklore-ui/src/i18n/es/shared.json index b55f1091e..bf7ce6b45 100644 --- a/booklore-ui/src/i18n/es/shared.json +++ b/booklore-ui/src/i18n/es/shared.json @@ -89,6 +89,26 @@ "foldersWillBeSelected": "{{ count }} carpetas serán seleccionadas", "selectDirectoriesBtn": "Seleccionar directorios" }, + "liveNotification": { + "defaultMessage": "Sin notificaciones recientes..." + }, + "metadataProgress": { + "taskStalled": "Tarea detenida o servidor no disponible", + "taskCancelled": "Tarea cancelada por el usuario", + "cancellationScheduledSummary": "Cancelación programada", + "cancellationScheduledDetail": "La cancelación de la tarea se ha programado correctamente", + "cancelFailedSummary": "Error al cancelar", + "cancelFailedDetail": "No se pudo cancelar la tarea. Inténtalo de nuevo." + }, + "reader": { + "failedToLoadPages": "Error al cargar las páginas", + "failedToLoadBook": "Error al cargar el libro" + }, + "settingsHelper": { + "settingsSavedSummary": "Configuración guardada", + "settingsSavedDetail": "¡La configuración se guardó correctamente!", + "saveErrorDetail": "Hubo un error al guardar la configuración." + }, "setup": { "title": "Bienvenido a Booklore", "subtitle": "Configura tu cuenta de administrador inicial para comenzar",