From d16620791243201f2e2eb0910201f73e2c2975f7 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 7 Apr 2021 10:10:56 +0800 Subject: [PATCH 01/39] fix(webui): series year incorrectly formatted --- komga-webui/src/views/BrowseSeries.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/komga-webui/src/views/BrowseSeries.vue b/komga-webui/src/views/BrowseSeries.vue index 598f70070..e6f091579 100644 --- a/komga-webui/src/views/BrowseSeries.vue +++ b/komga-webui/src/views/BrowseSeries.vue @@ -92,7 +92,7 @@ {{ $t('browse_series.earliest_year_from_release_dates') }} From 81142ab570ea9ce1cfd964e7c3205d0c1a9ead7a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 7 Apr 2021 04:15:16 +0200 Subject: [PATCH 02/39] feat: added translation using Weblate (Finnish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Spanish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Norwegian Bokmål) Currently translated at 90.6% (351 of 387 strings) fix: translated using Weblate (Spanish) Currently translated at 60.2% (233 of 387 strings) fix: translated using Weblate (Spanish) Currently translated at 60.2% (233 of 387 strings) fix: translated using Weblate (Italian) Currently translated at 36.1% (140 of 387 strings) fix: translated using Weblate (Finnish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Italian) Currently translated at 33.5% (130 of 387 strings) feat: added translation using Weblate (Finnish) fix: translated using Weblate (Italian) Currently translated at 29.9% (116 of 387 strings) fix: translated using Weblate (German) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Swedish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (German) Currently translated at 99.7% (386 of 387 strings) fix: translated using Weblate (Spanish) Currently translated at 58.3% (226 of 387 strings) fix: translated using Weblate (Swedish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (French) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (French) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Italian) Currently translated at 22.0% (85 of 386 strings) fix: translated using Weblate (Swedish) Currently translated at 100.0% (386 of 386 strings) fix: translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (386 of 386 strings) fix: translated using Weblate (Spanish) Currently translated at 58.5% (226 of 386 strings) fix: translated using Weblate (French) Currently translated at 100.0% (386 of 386 strings) Co-authored-by: Allan Nordhøy Co-authored-by: Gauthier Co-authored-by: Hosted Weblate Co-authored-by: M Co-authored-by: Nathan Co-authored-by: Nicklas Stafford Co-authored-by: Rurick Maqueo Poisot Co-authored-by: Shjosan Co-authored-by: Simone Chiavaccini Co-authored-by: f00f Co-authored-by: little cookie Co-authored-by: 峰裕 <1006945671@qq.com> Translate-URL: https://hosted.weblate.org/projects/komga/webui/de/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/es/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/fi/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/fr/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/it/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/sv/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/zh_Hans/ Translation: komga/webui Co-authored-by: Allan Nordhøy Co-authored-by: Gauthier Co-authored-by: M Co-authored-by: Nathan Co-authored-by: Nicklas Stafford Co-authored-by: Rurick Maqueo Poisot Co-authored-by: Shjosan Co-authored-by: Simone Chiavaccini Co-authored-by: f00f Co-authored-by: little cookie Co-authored-by: 峰裕 <1006945671@qq.com> --- komga-webui/src/locales/de.json | 27 +- komga-webui/src/locales/es.json | 482 +++++++++++++++++------- komga-webui/src/locales/fi.json | 523 +++++++++++++++++++++++++++ komga-webui/src/locales/fr.json | 8 +- komga-webui/src/locales/it.json | 487 ++++++++++++++++++++++++- komga-webui/src/locales/nb.json | 5 + komga-webui/src/locales/sv.json | 8 +- komga-webui/src/locales/zh-Hans.json | 8 +- 8 files changed, 1406 insertions(+), 142 deletions(-) create mode 100644 komga-webui/src/locales/fi.json diff --git a/komga-webui/src/locales/de.json b/komga-webui/src/locales/de.json index 196d2bdc0..9ce8c06a4 100644 --- a/komga-webui/src/locales/de.json +++ b/komga-webui/src/locales/de.json @@ -26,12 +26,18 @@ "penciller": "Zeichner", "writer": "Schriftsteller" }, + "book_card": { + "error": "Störung", + "unknown": "Noch nicht analisiert", + "unsupported": "Unbekanntes Format" + }, "bookreader": { "beginning_of_book": "Sie sind am Anfang des Buches.", "changing_reading_direction": "Ändere Leserichtung zu", "cycling_page_layout": "Ändere Seitendarstellung", "cycling_scale": "Ändere die Skalierung", "cycling_side_padding": "Seitenrand ändern", + "download_current_page": "Lade die aktuelle Seite herunter", "end_of_book": "Sie haben das Ende des Buches erreicht.", "from_series_metadata": "Metadaten von Serie", "move_next": "Klicken oder drücken Sie \"Weiter\" um zum nächsten Buch zu gelangen.", @@ -157,6 +163,18 @@ "recently_added_series": "Kürzlich hinzugefügte Serien", "recently_updated_series": "Kürzlich aktualisierte Serien" }, + "data_import": { + "book_number": "Buch Nummer: {name}", + "book_series": "Serie: {name}", + "button_import": "Übernehme", + "comicrack_preambule_html": "Es ist möglich ComicRack Leselisten im Format .cbl zu übernehmen.
Komga versucht die übernommenen Serien und Buchnummern mit den Serien und Büchern in der Bibliothek abzugleichen.", + "field_files_label": "ComicRack Leseliste (.cbl)", + "import_read_lists": "Übernehme Leseliste", + "imported_as": "Übernehme als {name}", + "results_preambule": "Das Ergebnis der Übernahme wird unten angezeigt. Es ist möglich für jedes Buch, das nicht gefunden wurde, eine Einzelauswahl durchzuführen.", + "size_limit": "Die Größe darf {size} MB nicht überschreiten", + "tab_title": "Datenübernahme" + }, "dialog": { "add_to_collection": { "button_create": "Erstelle", @@ -367,7 +385,14 @@ "ERR_1005": "Unbekannter Fehler beim Analysieren des Buchs", "ERR_1006": "Das Buch enthält keine Seiten", "ERR_1007": "Einige Einträge konnten nicht ausgewertet werden", - "ERR_1008": "Unbekannter Fehler beim Abrufen der Bucheinträge" + "ERR_1008": "Unbekannter Fehler beim Abrufen der Bucheinträge", + "ERR_1009": "Eine Leseliste mit diesem Namen existiert bereits", + "ERR_1010": "Für dies Bücher in der Leseliste gab es keine Übereinstimmung in der Bibliothek", + "ERR_1011": "Kein eindeutige Übereinstimmung für diese Serie", + "ERR_1012": "Keine Übereinstimmung für die Serie", + "ERR_1013": "Kein eindeutige Übereinstimmung für die Bücher der Serie", + "ERR_1014": "Keine Übereinstimmung für die Bücher für die Serie", + "ERR_1015": "Fehler bei der Abarbeitung der ComicRack Leseliste" }, "filter": { "age_rating": "Altersfreigabe", diff --git a/komga-webui/src/locales/es.json b/komga-webui/src/locales/es.json index 3fca6299e..65ef77ee0 100644 --- a/komga-webui/src/locales/es.json +++ b/komga-webui/src/locales/es.json @@ -5,7 +5,7 @@ }, "dataTable": { "itemsPerPageText": "Filas por página:", - "sortBy": "Ordenado por" + "sortBy": "Ordenar por" }, "fileInput": { "counter": "{0} archivos", @@ -14,298 +14,510 @@ "noDataText": "No hay datos disponibles" }, "account_settings": { - "account_settings": "Configuración de cuenta", - "change_password": "Cambiar contraseña" + "account_settings": "Configuración de Cuenta", + "change_password": "cambiar contraseña" }, "author_roles": { "colorist": "coloristas", - "cover": "cobertura", + "cover": "portada", "editor": "editores", "inker": "entintadores", "letterer": "letristas", "penciller": "dibujantes", "writer": "escritores" }, + "book_card": { + "error": "Error", + "unknown": "Análisis pendiente", + "unsupported": "No soportado" + }, "bookreader": { "beginning_of_book": "Estás al inicio del libro.", "changing_reading_direction": "Cambiar dirección de lectura a", - "cycling_page_layout": "Disposición de la página", - "cycling_scale": "Formato de la Página", + "cycling_page_layout": "Cambiar disposición de página", + "cycling_scale": "Cambiar escalado de página", "cycling_side_padding": "Cambiar el borde lateral", - "end_of_book": "Had llegado al final del libro.", - "from_series_metadata": "desde serie metadatos", - "move_next": "Has click o presiona siguiente de nuevo para moverte al siguiente libro.", - "move_next_exit": "Has click o presiona siguiente de nuevo para salir del lector.", - "move_previous": "Has click o presiona anterior de nuevo para moverte al libro anterior.", + "download_current_page": "Descargar página actual", + "end_of_book": "Has llegado al final del libro.", + "from_series_metadata": "desde metadatos de serie", + "move_next": "Haz click o presiona \"Siguiente\" de nuevo para moverte al siguiente libro.", + "move_next_exit": "Haz click o presiona \"Siguiente\" de nuevo para salir del lector.", + "move_previous": "Haz click o presiona \"Anterior\" de nuevo para moverte al libro anterior.", "paged_reader_layout": { - "double": "doble página", - "double_no_cover": "Doble página (sin cobertura)", + "double": "Doble página", + "double_no_cover": "Doble página (sin portada)", "single": "Página única" }, - "reader_settings": "Configuraciones del lector", + "reader_settings": "Configuración de Lector", "scale_type": { "continuous_original": "Original", - "continuous_width": "Estiramiento horizontal", - "height": "Estiramiento vertical", + "continuous_width": "Ajustar a ancho", + "height": "Ajustar a altura", "original": "Original", "screen": "Pantalla", - "width": "Estiramiento horizontal" + "width": "Ajustar a ancho" }, "settings": { - "animate_page_transitions": "Animar transición de página", + "animate_page_transitions": "Animar transiciones de página", "background_color": "Color de fondo", "background_colors": { - "black": "Oscuro", + "black": "Negro", "white": "Blanco" }, - "display": "Pantalla", + "display": "Visualización", "general": "General", "gestures": "Gestos", - "page_layout": "Diseño de página", - "paged": "Opción de lector", + "page_layout": "Disposición de página", + "paged": "Opciones de Paginado", "reading_mode": "Modo de lectura", - "scale_type": "Tipo de escala", + "scale_type": "Escalado", "side_padding": "Borde lateral", - "side_padding_none": "Ningún", - "webtoon": "Opciones del lector Webtoon" + "side_padding_none": "Ninguno", + "webtoon": "Opciones del Lector de Webtoon" }, "shortcuts": { "close": "Cerrar", - "cycle_page_layout": "Cambiar la disposición de páginas", - "cycle_scale": "Escalado de páginas", + "cycle_page_layout": "Cambiar disposición de página", + "cycle_scale": "Cambiar escalado de página", "cycle_side_padding": "Cambiar el borde lateral", "first_page": "Primera página", "last_page": "Última página", - "left_to_right": "De izquierda a derecha", + "left_to_right": "Izquierda a Derecha", "menus": "Menús", "next_page": "Página siguiente", "previous_page": "Página anterior", - "reader_navigation": "Navegador en el lector", - "right_to_left": "De derecha a izquierda", + "reader_navigation": "Navegación en Lector", + "right_to_left": "Derecha a Izquierda", "settings": "Configuración", "show_hide_help": "Mostrar/ocultar ayuda", - "show_hide_settings": "Mostrar/ocultar configuración", - "show_hide_thumbnails": "Mostrar/ocultar imágenes en miniatura", - "show_hide_toolbars": "Mostrar/ocultar barra de herramientas", + "show_hide_settings": "Mostrar/ocultar menú de configuración", + "show_hide_thumbnails": "Mostrar/ocultar miniaturas", + "show_hide_toolbars": "Mostrar/ocultar barras de herramientas", "vertical": "Vertical", "webtoon": "Webtoon" } }, "browse_book": { "comment": "COMENTARIO", - "download_file": "Descargar el archivo", - "file": "Archivo", - "format": "FORMAT", + "download_file": "Descargar archivo", + "file": "ARCHIVO", + "format": "FORMATO", "isbn": "ISBN", "navigation_within_readlist": "Navegación en la lista: {name}", - "read_book": "Leer el libro", - "size": "TALLA" + "read_book": "Leer libro", + "size": "TAMAÑO" }, "browse_collection": { - "edit_collection": "Editar la colección", - "edit_elements": "Cambiar los elementos", - "manual_ordering": "clasificación manual" + "edit_collection": "Editar colección", + "edit_elements": "Editar elementos", + "manual_ordering": "orden manual" }, "browse_readlist": { - "edit_elements": "Cambiar los elementos", - "edit_readlist": "Modificar lista de lectura" + "edit_elements": "Editar elementos", + "edit_readlist": "Editar lista de lectura" }, "browse_series": { - "earliest_year_from_release_dates": "Este es el año más antiguo que figura entre unos de los libros de la serie", - "series_no_summary": "Esta serie no tiene resumen, hemos seleccionado uno para usted !", + "earliest_year_from_release_dates": "Este es el año de lanzamiento del libro más antiguo de la serie", + "series_no_summary": "Esta serie no tiene resumen, ¡así que escogimos uno para usted!", "summary_from_book": "Resumen del libro {number}:" }, "collections_expansion_panel": { - "manage_collection": "Gestionar la colección", - "title": "{name} colección" + "manage_collection": "Administrar colección", + "title": "Colección {name}" }, "common": { - "all_libraries": "Todas las bibliotecas", + "all_libraries": "Todas las Bibliotecas", "books": "Libros", "books_n": "Sin libros | 1 libro | {count} libros", "cancel": "Cancelar", "close": "Cerrar", "collections": "Colecciones", "create": "Crear", - "delete": "Borrar", + "delete": "Eliminar", "download": "Descargar", "email": "Email", - "filter_no_matches": "Los filtros activos no tienen ninguna coincidencia", + "filter_no_matches": "El filtro activo no tiene coincidencias", "genre": "Género", "go_to_library": "Ir a la biblioteca", "locale_name": "Español", "locale_rtl": "false", - "n_selected": "{count} seleccionado", + "n_selected": "Nada seleccionado | 1 seleccionado | {count} seleccionados", "nothing_to_show": "Nada que mostrar", "pages": "páginas", "pages_n": "Sin páginas | 1 página | {count} páginas", "password": "Contraseña", "publisher": "Editor", "read": "Leer", - "readlists": "Listas de lectura", + "readlists": "Listas de Lectura", "required": "Requerido", - "roles": "Rol", - "series": "Series", + "roles": "Funciones", + "series": "Serie", "tags": "Etiquetas", - "use_filter_panel_to_change_filter": "Uso el panel de filtros para cambiar los filtros activos", + "use_filter_panel_to_change_filter": "Use el panel de filtros para cambiar el filtro activo", "year": "año" }, "dashboard": { - "keep_reading": "Seguir leyendo", + "keep_reading": "Seguir Leyendo", "on_deck": "En curso", - "recently_added_books": "Libros recién agregados", - "recently_added_series": "Series agregadas recientemente", - "recently_updated_series": "Series recientemente actualizadas" + "recently_added_books": "Libros Recientemente Agregados", + "recently_added_series": "Series Recientemente Agregadas", + "recently_updated_series": "Series Recientemente Actualizadas" + }, + "data_import": { + "book_number": "Número de libro: {name}", + "book_series": "Serie: {name}", + "button_import": "Importar", + "comicrack_preambule_html": "Puedes importar Listas de Lectura de ComicRack en formato .cbl.
Komga intentará hacer coincidir la serie y número de libro suministrados con los existentes en tus bibliotecas.", + "field_files_label": "Listas de Lectura de ComicRack (.cbl)", + "import_read_lists": "Importar Listas de Lectura", + "imported_as": "Importado como {name}", + "results_preambule": "El resultado de la importación se muestra abajo. También puedes revisar los libros de cada archivo suministrado para los cuales no se encontró coincidencia.", + "size_limit": "El tamaño debe ser menor a {size} MB", + "tab_title": "Importar Datos" }, "dialog": { "add_to_collection": { "button_create": "Crear", - "card_collection_subtitle": "No series | 1 series | {count} series", - "dialog_title": "Añadir a la colección", + "card_collection_subtitle": "Sin series | 1 serie | {count} series", + "dialog_title": "Añadir a colección", "field_search_create": "Buscar o crear una colección", "field_search_create_error": "Ya existe una colección con este nombre", - "label_no_matching_collection": "Ninguna colección correspondiente" + "label_no_matching_collection": "Ninguna colección coincide" }, "add_to_readlist": { "button_create": "Crear", - "card_readlist_subtitle": "No book | 1 book | {count} books", - "dialog_title": "Añadir a la lista de lectura", + "card_readlist_subtitle": "Sin libros | 1 libro | {count} libros", + "dialog_title": "Añadir a lista de lectura", "field_search_create": "Buscar o crear una lista de lectura", "field_search_create_error": "Ya existe una lista de lectura con este nombre", - "label_no_matching_readlist": "No hay lista de lectura correspondiente" + "label_no_matching_readlist": "Ninguna lista de lectura coincide" }, "add_user": { - "button_cancel": "Anular", - "button_confirm": "Añadir", - "dialog_title": "Añadir un usuario", + "button_cancel": "Cancelar", + "button_confirm": "Agregar", + "dialog_title": "Agregar Usuario", "field_email": "Email", "field_email_error": "Debe ser una dirección de email válida", - "field_password": "Código de acceso", + "field_password": "Contraseña", "field_role_administrator": "Administrador", "field_role_file_download": "Descarga de archivos", "field_role_page_streaming": "Lectura en línea", "label_roles": "Funciones" }, "delete_collection": { - "button_cancel": "Anular", - "button_confirm": "Suprimir", - "confirm_delete": "Si, suprimir la colección \"{name}\"", - "dialog_title": "Suprimir la colección", - "warning_html": "La colección {name} será removida de este servidor. Sus archivos nunca serán eliminados. Esta acción es irreversible ¿Desea continuar?" + "button_cancel": "Cancelar", + "button_confirm": "Eliminar", + "confirm_delete": "Sí, eliminar la colección \"{name}\"", + "dialog_title": "Eliminar Colección", + "warning_html": "La colección {name} será removida de este servidor. Los archivos no serán eliminados. Esta acción es irreversible ¿Desea continuar?" }, "delete_library": { - "button_cancel": "Anular", - "button_confirm": "Suprimir", - "confirm_delete": "Si, eliminar la biblioteca \"{name}\"", - "title": "Eliminar la biblioteca", - "warning_html": "La colección {name} será removida de este servidor. Sus archivos nunca serán eliminados. Esta acción es irreversible ¿Desea continuar?" + "button_cancel": "Cancelar", + "button_confirm": "Eliminar", + "confirm_delete": "Sí, eliminar la biblioteca \"{name}\"", + "title": "Eliminar Biblioteca", + "warning_html": "La biblioteca {name} será removida de este servidor. Los archivos no serán eliminados. Esta acción es irreversible ¿Desea continuar?" }, "delete_readlist": { - "button_cancel": "Anular", - "button_confirm": "Suprimir", - "confirm_delete": "Si, eliminar la lista de lectura \"{name}\"", - "dialog_title": "Eliminar la lista de lectura", - "warning_html": "La lista de lecura {name} será removida de este servidor. Sus archivos nunca serán eliminados. Esta acción es irreversible ¿Desea continuar?" + "button_cancel": "Cancelar", + "button_confirm": "Eliminar", + "confirm_delete": "Sí, eliminar la lista de lectura \"{name}\"", + "dialog_title": "Eliminar Lista de Lectura", + "warning_html": "La lista de lectura {name} será removida de este servidor. Los archivos no serán eliminados. Esta acción es irreversible ¿Desea continuar?" }, "delete_user": { - "button_cancel": "Anular", - "button_confirm": "Suprimir", - "confirm_delete": "Si, Suprimir al usuario \"{name}\"", - "dialog_title": "Suprimir el usuario", - "warning_html": "El usuario {name} será removido de este servidor. Esta acciónes irreversible ¿Desea continuar?" + "button_cancel": "Cancelar", + "button_confirm": "Eliminar", + "confirm_delete": "Sí, eliminar el usuario \"{name}\"", + "dialog_title": "Eliminar Usuario", + "warning_html": "El usuario {name} será removido de este servidor. Esta acción es irreversible ¿Desea continuar?" }, "edit_books": { - "authors_notice_multiple_edit": "Estás editando autores para varios libros. Esto suprimira a los autores existentes para cada uno de los libros.", - "button_cancel": "Anular", - "button_confirm": "Guardar los cambios", - "dialog_title_multiple": "Cambiar {count} livre | Cambiar {count} livros", - "dialog_title_single": "Cambiar {book}", + "authors_notice_multiple_edit": "Estás editando los autores de varios libros. Esto sobreescribirá los autores existentes para cada libro.", + "button_cancel": "Cancelar", + "button_confirm": "Guardar cambios", + "dialog_title_multiple": "Editar {count} libro | Editar {count} libros", + "dialog_title_single": "Editar {book}", "field_isbn": "ISBN", "field_isbn_error": "Debe ser un código ISBN 13 válido", - "field_number": "Numero", - "field_number_sort": "Ordenar por números", + "field_number": "Número", + "field_number_sort": "Ordenar por Número", "field_number_sort_hint": "Puede utilizar números decimales", - "field_release_date": "Fecha de publicación", - "field_release_date_error": "Debe ser una fecha en formato YYYY-MM-DD", + "field_release_date": "Fecha de Publicación", + "field_release_date_error": "Debe ser una fecha válida en formato AAAA-MM-DD", "field_summary": "Resumen", "field_tags": "Etiquetas", "field_title": "Título", "tab_authors": "Autores", "tab_general": "General", "tab_tags": "Etiquetas", - "tags_notice_multiple_edit": "Usted está cambiando las etiquetas para varios libros. Esto va a suprimir las etiquetas por cada livro." + "tags_notice_multiple_edit": "Estás editando las etiquetas de varios libros. Esto sobreescribirá las etiquetas existentes en cada uno." }, "edit_collection": { - "button_cancel": "Anular", - "button_confirm": "Guardar los cambios", - "dialog_title": "Editar la colección", - "field_manual_ordering": "Clasificación manual", - "label_ordering": "De forma predeterminada, las series de una colección se ordenarán por nombre. Puede habilitar la ordenación manual para definir su propria ordenación." + "button_cancel": "Cancelar", + "button_confirm": "Guardar cambios", + "dialog_title": "Editar colección", + "field_manual_ordering": "Orden manual", + "label_ordering": "Por defecto, las series de una colección se ordenan por nombre. Puedes habilitar el orden manual para definir tu propio orden." }, "edit_library": { - "button_browse": "Navegar", - "button_cancel": "Anular", - "button_confirm_add": "Añadir", + "button_browse": "Explorar", + "button_cancel": "Cancelar", + "button_confirm_add": "Agregar", "button_confirm_edit": "Editar", - "dialog_title_add": "Añadir biblioteca", - "dialot_title_edit": "Modificar la biblioteca", + "dialog_title_add": "Agregar Biblioteca", + "dialot_title_edit": "Modificar Biblioteca", "field_import_barcode_isbn": "Código de barras ISBN", - "field_import_comicinfo_book": "Metadatos del libro", + "field_import_comicinfo_book": "Metadatos de libro", "field_import_comicinfo_collections": "Colecciones", - "field_import_comicinfo_readlists": "Playlists", - "field_import_comicinfo_series": "Metadatos de series", - "field_import_epub_book": "Metadatos del libro", - "field_import_epub_series": "Metadatos de series", - "field_import_local_artwork": "Cobertura local", + "field_import_comicinfo_readlists": "Listas de lectura", + "field_import_comicinfo_series": "Metadatos de serie", + "field_import_epub_book": "Metadatos de libro", + "field_import_epub_series": "Metadatos de serie", + "field_import_local_artwork": "Portada local", "field_name": "Nombre", "field_root_folder": "Directorio raíz", "field_scanner_deep_scan": "Análisis en profundidad", "field_scanner_force_directory_modified_time": "Forzar hora de edición del directorio", - "file_browser_dialog_button_confirm": "Seleccionar", - "file_browser_dialog_title": "directorio raíz de la librería", - "label_import_barcode_isbn": "Importar ISBN con código de barras", + "file_browser_dialog_button_confirm": "Elegir", + "file_browser_dialog_title": "Directorio raíz de librería", + "label_import_barcode_isbn": "Importar ISBN en código de barras", "label_import_comicinfo": "Importar metadatos para archivos CBR/CBZ que contengan un archivo ComicInfo.xml", - "label_import_epub": "Importar metadatos de archivos EPUB", - "label_import_local": "Importar medios de comunicación locales", + "label_import_epub": "Importar metadatos desde archivos EPUB", + "label_import_local": "Importar medios locales", "label_scanner": "Escáner", "tab_general": "General", "tab_options": "Opciones" }, "edit_readlist": { - "button_cancel": "Anular", - "button_confirm": "Guardar los cambios", - "dialog_title": "Modificar la playlist", + "button_cancel": "Cancelar", + "button_confirm": "Guardar cambios", + "dialog_title": "Modificar lista de lectura", "field_name": "Nombre" }, "edit_series": { - "button_cancel": "Anular", - "button_confirm": "Guardar los cambios", - "dialog_title_multiple": "Rectifica {count} la serie | Rectifica {count} las series", - "dialog_title_single": "Rectifica {series}", + "button_cancel": "Cancelar", + "button_confirm": "Guardar cambios", + "dialog_title_multiple": "Editar {count} serie | Editar {count} series", + "dialog_title_single": "Editar {series}", "field_age_rating": "Edad mínima", "field_age_rating_error": "La edad mínima debe ser superior o igual a 0", - "field_genres": "Género", - "field_language": "Lengua", + "field_genres": "Géneros", + "field_language": "Idioma", "field_publisher": "Editor", "field_reading_direction": "Dirección de lectura", - "field_sort_title": "Título para la ordenación", - "field_status": "Estatuto", + "field_sort_title": "Título para el orden", + "field_status": "Estado", "field_summary": "Resumen", "field_tags": "Etiquetas", "field_title": "Título", "mixed": "MIXTO", "tab_general": "General", "tab_tags": "Etiquetas", - "tags_notice_multiple_edit": "Está editando etiquetas para varias series. Esto sobrescribirá las etiquetas de cada una de las series." + "tags_notice_multiple_edit": "Estás editando las etiquetas de varias series. Esto sobreescribirá las etiquetas existentes en cada una." + }, + "edit_user": { + "button_cancel": "Cancelar", + "button_confirm": "Guardar cambios", + "dialog_title": "Editar usuario", + "label_roles_for": "Funciones para {name}" + }, + "edit_user_shared_libraries": { + "button_cancel": "Cancelar", + "button_confirm": "Guardar cambios", + "dialog_title": "Editar bibliotecas compartidas", + "field_all_libraries": "Todas las bibliotecas", + "label_shared_with": "Compartida con {name}" + }, + "file_browser": { + "button_cancel": "Cancelar", + "button_confirm_default": "Elegir", + "dialog_title_default": "Explorador de Archivos", + "parent_directory": "Padre" + }, + "password_change": { + "button_cancel": "Cancelar", + "button_confirm": "Cambiar contraseña", + "dialog_title": "Cambiar contraseña", + "field_new_password": "Contraseña nueva", + "field_new_password_error": "Se requiere una contraseña nueva.", + "field_repeat_password": "Confirmar nueva contraseña", + "field_repeat_password_error": "Las contraseñas deben ser idénticas." }, "server_stop": { - "confirmation_message": "¿Estás seguro de querer apagar Komga?" + "button_cancel": "Cancelar", + "button_confirm": "Detener", + "confirmation_message": "¿Estás seguro de que quieres detener Komga?", + "dialog_title": "Apagar servidor" + }, + "shortcut_help": { + "label_description": "Descripción", + "label_key": "Tecla" } }, + "enums": { + "media_status": { + "ERROR": "Error", + "OUTDATED": "Desactualizado", + "READY": "Listo", + "UNKNOWN": "Desconocido", + "UNSUPPORTED": "No soportado" + }, + "reading_direction": { + "LEFT_TO_RIGHT": "De izquierda a derecha", + "RIGHT_TO_LEFT": "De derecha a izquierda", + "VERTICAL": "Vertical", + "WEBTOON": "Webtoon" + }, + "series_status": { + "ABANDONED": "Abandonada", + "ENDED": "Finalizada", + "HIATUS": "En hiato", + "ONGOING": "En curso" + } + }, + "error_codes": { + "ERR_1000": "No se pudo acceder al archivo durante el análisis", + "ERR_1001": "Tipo de medios no soportado", + "ERR_1002": "Archivos RAR encriptados no están soportados", + "ERR_1003": "Archivos RAR sólidos no están soportados", + "ERR_1004": "Archivos RAR multi-volúmen no están soportados", + "ERR_1005": "Error desconocido durante análisis de libro", + "ERR_1006": "El libro no contiene ninguna página", + "ERR_1007": "Algunas entradas no pudieron ser analizadas", + "ERR_1008": "Error desconocido al obtener las entradas del libro", + "ERR_1009": "Ya existe una lista de lectura con este nombre", + "ERR_1010": "No se encontró coincidencia para ningún libro dentro de la lista de lectura", + "ERR_1011": "No existe una única coincidencia para la serie", + "ERR_1012": "No existe coincidencia para la serie", + "ERR_1013": "No existe una única coincidencia para el número de libro dentro de la serie", + "ERR_1014": "No existe coincidencia para el número de libro dentro de la serie", + "ERR_1015": "Error al deserializar la Lista de Lectura ComicRack" + }, + "filter": { + "age_rating": "edad mínima", + "age_rating_none": "Ninguna", + "genre": "género", + "language": "idioma", + "library": "biblioteca", + "publisher": "editor", + "release_date": "fecha de lanzamiento", + "status": "estado", + "tag": "etiqueta", + "unread": "Sin leer" + }, + "filter_drawer": { + "filter": "filtro", + "sort": "orden" + }, + "home": { + "theme": "Tema", + "translation": "Traducción" + }, + "library_navigation": { + "browse": "Explorar", + "collections": "Colecciones", + "readlists": "Listas de lectura" + }, "login": { - "unclaimed_html": "Este Komga server no está respondiendo, Primero necesitas crear una cuenta de usuario para poderse conectar.

Elegir un email ypassword y haga clic en Crear una cuenta de usuario." + "create_user_account": "Crear cuenta de usuario", + "login": "Iniciar sesión", + "unclaimed_html": "Este servidor Komga no está activo aún, debes crear una cuenta de usuario para poder acceder a él.

Escoge un email y contraseña, y haz clic en Crear cuenta de usuario." + }, + "media_analysis": { + "comment": "Comentario", + "media_analysis": "Análisis de medios", + "media_type": "Tipo de medio", + "name": "Nombre", + "size": "Tamaño", + "status": "Estado", + "url": "URL" + }, + "menu": { + "add_to_collection": "Añadir a colección", + "add_to_readlist": "Añadir a lista de lectura", + "analyze": "Analizar", + "delete": "Eliminar", + "download_series": "Descargar serie", + "edit": "Editar", + "edit_metadata": "Editar metadatos", + "mark_read": "Marcar como leído", + "mark_unread": "Marcar como no leído", + "refresh_metadata": "Actualizar metadatos", + "scan_library_files": "Escanear archivos de biblioteca" + }, + "navigation": { + "home": "Inicio", + "libraries": "Bibliotecas", + "logout": "Cerrar sesión" + }, + "page_not_found": { + "go_back_to_home_page": "Volver a la página principal", + "page_does_not_exist": "La página que buscas no existe.", + "page_not_found": "No se encontró la página" + }, + "read_more": { + "less": "Leer menos", + "more": "Leer más" + }, + "readlists_expansion_panel": { + "manage_readlist": "Administrar lista de lectura", + "title": "Lista de lectura {name}" + }, + "search": { + "no_results": "La búsqueda no arrojó resultados", + "search": "Buscar", + "search_for_something_else": "Intenta buscar otra cosa", + "search_results_for": "Resultados de búsqueda para \"{name}\"" + }, + "searchbox": { + "no_results": "No hay resultados", + "search_all": "Buscar todo…" + }, + "server": { + "server_management": { + "button_shutdown": "Apagar", + "section_title": "Administración de Servidor" + }, + "tab_title": "Servidor" + }, + "server_settings": { + "server_settings": "Configuración de Servidor" + }, + "settings_user": { + "edit_shared_libraries": "Editar bibliotecas compartidas", + "edit_user": "Editar usuario", + "role_administrator": "Administrador", + "role_user": "Usuario" + }, + "sort": { + "books_count": "Número de libros", + "date_added": "Fecha de adición", + "date_updated": "Fecha de actualización", + "file_name": "Nombre de archivo", + "file_size": "Tamaño de archivo", + "folder_name": "Nombre de carpeta", + "name": "Nombre", + "number": "Número", + "release_date": "Fecha de lanzamiento" + }, + "theme": { + "dark": "Oscuro", + "light": "Claro", + "system": "Sistema" + }, + "user_roles": { + "ADMIN": "Administrador", + "FILE_DOWNLOAD": "Descarga de archivos", + "PAGE_STREAMING": "Lectura en línea", + "USER": "Usuario" + }, + "users": { + "users": "Usuarios" }, "welcome": { - "welcome_message": "Bienvenidos a Komga" + "add_library": "Añadir biblioteca", + "no_libraries_yet": "¡Aún no has agregado bibliotecas!", + "welcome_message": "Bienvenido a Komga" } } diff --git a/komga-webui/src/locales/fi.json b/komga-webui/src/locales/fi.json new file mode 100644 index 000000000..3e0de47a5 --- /dev/null +++ b/komga-webui/src/locales/fi.json @@ -0,0 +1,523 @@ +{ + "$vuetify": { + "dataFooter": { + "pageText": "{0}-{1}/{2}" + }, + "dataTable": { + "itemsPerPageText": "Riviä/sivu:", + "sortBy": "Lajittelu" + }, + "fileInput": { + "counter": "{0} tiedostoa", + "counterSize": "{0} tiedostoa ({1} yhteensä)" + }, + "noDataText": "Tietoja ei ole saatavilla" + }, + "account_settings": { + "account_settings": "Tilin asetukset", + "change_password": "Vaihda salasana" + }, + "author_roles": { + "colorist": "värittäjät", + "cover": "kansi", + "editor": "toimittajat", + "inker": "ääriviivatyöntekijät", + "letterer": "kirjoittajat", + "penciller": "piirtäjät", + "writer": "kirjailijat" + }, + "book_card": { + "error": "Virhe", + "unknown": "Ei vielä analysoitu", + "unsupported": "Ei tuettu" + }, + "bookreader": { + "beginning_of_book": "Olet kirjan alussa.", + "changing_reading_direction": "Vaihda lukusuunta", + "cycling_page_layout": "Vaihda sivun asettelua", + "cycling_scale": "Vaihda skaalaa", + "cycling_side_padding": "Vaihda sivuntäyttöä", + "download_current_page": "Lataa tämänhetkinen sivu", + "end_of_book": "Olet saavuttanut kirjan lopun.", + "from_series_metadata": "sarjan metatiedoista", + "move_next": "Siirry seuraavaan kirjaan napsauttamalla tai painamalla \"Seuraava\" uudelleen.", + "move_next_exit": "Poistu lukijasta napsauttamalla tai painamalla \"Seuraava\" uudelleen.", + "move_previous": "Siirry edelliseen kirjaan napsauttamalla tai painamalla \"Edellinen\" uudelleen.", + "paged_reader_layout": { + "double": "Kaksoissivut", + "double_no_cover": "Kaksoissivut (ei kantta)", + "single": "Yksi sivu" + }, + "reader_settings": "Lukijan asetukset", + "scale_type": { + "continuous_original": "Alkuperäinen", + "continuous_width": "Sovita leveys", + "height": "Sovita korkeus", + "original": "Alkuperäinen", + "screen": "Kuvaruutu", + "width": "Sovita leveys" + }, + "settings": { + "animate_page_transitions": "Animoi sivusiirtymät", + "background_color": "Taustan väri", + "background_colors": { + "black": "Musta", + "white": "Valkoinen" + }, + "display": "Näytä", + "general": "Yleiset", + "gestures": "Eleet", + "page_layout": "Sivun asettelu", + "paged": "Sivutetun lukijan asetukset", + "reading_mode": "Lukutila", + "scale_type": "Skaalauksen tyyppi", + "side_padding": "Sivuntäyttö", + "side_padding_none": "Ei mitään", + "webtoon": "Webtoon-lukijan asetukset" + }, + "shortcuts": { + "close": "Sulje", + "cycle_page_layout": "Vaihda sivun asettelua", + "cycle_scale": "Vaihda skaalaa", + "cycle_side_padding": "Vaihda sivuntäyttöä", + "first_page": "Ensimmäinen sivu", + "last_page": "Viimeinen sivu", + "left_to_right": "Vasemmalta oikealle", + "menus": "Valikot", + "next_page": "Seuraava sivu", + "previous_page": "Edellinen sivu", + "reader_navigation": "Lukijan navigointi", + "right_to_left": "Oikealta vasemmalle", + "settings": "Asetukset", + "show_hide_help": "Näytä/piilota apu", + "show_hide_settings": "Näytä/piilota asetusvalikko", + "show_hide_thumbnails": "Näytä/piilota pikkukuvaselain", + "show_hide_toolbars": "Näytä/piilota työkalupalkit", + "vertical": "Pystysuora", + "webtoon": "Webtoon-tyylinen" + } + }, + "browse_book": { + "comment": "KOMMENTTI", + "download_file": "Lataa tiedosto", + "file": "TIEDOSTO", + "format": "MUOTO", + "isbn": "ISBN", + "navigation_within_readlist": "Navigointi lukulistan sisällä: {name}", + "read_book": "Lue kirja", + "size": "KOKO" + }, + "browse_collection": { + "edit_collection": "Muokkaa kokoelmaa", + "edit_elements": "Muokkaa elementtejä", + "manual_ordering": "manuaalinen järjestely" + }, + "browse_readlist": { + "edit_elements": "Muokkaa elementtejä", + "edit_readlist": "Muokkaa lukulistaa" + }, + "browse_series": { + "earliest_year_from_release_dates": "Tämä on aikaisin vuosi kaikista sarjassa olevien kirjojen julkaisuvuosista", + "series_no_summary": "Tällä sarjalla ei ole yhteenvetoa, joten valitsimme sinulle sellaisen!", + "summary_from_book": "Kirjan {number} yhteenveto:" + }, + "collections_expansion_panel": { + "manage_collection": "Hallitse kokoelmaa", + "title": "{name} kokoelma" + }, + "common": { + "all_libraries": "Kaikki kirjastot", + "books": "Kirjat", + "books_n": "Ei kirjaa | Yksi kirja | {count} kirjaa", + "cancel": "Peruuta", + "close": "Sulje", + "collections": "Kokoelmat", + "create": "Luo", + "delete": "Poista", + "download": "Lataa", + "email": "Sähköposti", + "filter_no_matches": "Aktiiviselle suodattimelle ei löytynyt tuloksia", + "genre": "Tyylilaji", + "go_to_library": "Siirry kirjastoon", + "locale_name": "Suomi", + "locale_rtl": "false", + "n_selected": "{count} valittu", + "nothing_to_show": "Ei näytettävää", + "pages": "sivua", + "pages_n": "Ei sivuja | Yksi sivu | {count} sivua", + "password": "Salasana", + "publisher": "Julkaisija", + "read": "Lue", + "readlists": "Lukulistat", + "required": "Pakollinen", + "roles": "Roolit", + "series": "Sarja", + "tags": "Tunnisteet", + "use_filter_panel_to_change_filter": "Käytä suodatinpaneelia aktiivisen suodattimen vaihtamiseen", + "year": "vuosi" + }, + "dashboard": { + "keep_reading": "Jatka lukemista", + "on_deck": "Kannella", + "recently_added_books": "Viimeksi lisätyt kirjat", + "recently_added_series": "Viimeksi lisätyt sarjat", + "recently_updated_series": "Viimeksi päivitetyt sarjat" + }, + "data_import": { + "book_number": "Kirjan numero: {name}", + "book_series": "Sarja: {name}", + "button_import": "Tuo", + "comicrack_preambule_html": "Voit tuoda valmiita ComicRack -lukulistoja .cbl -formaatissa.
Komga yrittää sovittaa annetun sarjan ja kirjan numeron kirjastossasi oleviin sarjoihin ja kirjoihin.", + "field_files_label": "ComicRack lukulistat (.cbl)", + "import_read_lists": "Tuo lukulistat", + "imported_as": "Tuotu nimellä {name}", + "results_preambule": "Tuonnin tulokset on näytetty alla. Voit myös tarkistaa sovittamattomat kirjat jokaiselle annetulle tiedostolle.", + "size_limit": "Koon pitäisi olla alle {size} MB", + "tab_title": "Tietojen tuonti" + }, + "dialog": { + "add_to_collection": { + "button_create": "Luo", + "card_collection_subtitle": "Ei sarjaa | Yksi sarja | {count} sarjaa", + "dialog_title": "Lisää kokoelmaan", + "field_search_create": "Etsi tai luo kokoelma", + "field_search_create_error": "Tämänniminen kokoelma on jo olemassa", + "label_no_matching_collection": "Ei vastaavia kokoelmia" + }, + "add_to_readlist": { + "button_create": "Luo", + "card_readlist_subtitle": "Ei kirjaa | Yksi kirja | {count} kirjaa", + "dialog_title": "Lisää lukulistaan", + "field_search_create": "Etsi tai luo lukulista", + "field_search_create_error": "Tämänniminen lukulista on jo olemassa", + "label_no_matching_readlist": "Ei vastaavaa lukulistaa" + }, + "add_user": { + "button_cancel": "Peruuta", + "button_confirm": "Lisää", + "dialog_title": "Lisää käyttäjä", + "field_email": "Sähköposti", + "field_email_error": "Täytyy olla pätevä sähköpostiosoite", + "field_password": "Salasana", + "field_role_administrator": "Järjestelmänvalvoja", + "field_role_file_download": "Tiedostojen lataus", + "field_role_page_streaming": "Sivujen suoratoisto", + "label_roles": "Roolit" + }, + "delete_collection": { + "button_cancel": "Peruuta", + "button_confirm": "Poista", + "confirm_delete": "Kyllä, poista kokoelma \"{name}\"", + "dialog_title": "Poista kokoelma", + "warning_html": "Kokoelma {name} poistetaan. Tämä ei vaikuta mediatiedostoihisi. Tätä ei voi kumota. Jatka?" + }, + "delete_library": { + "button_cancel": "Peruuta", + "button_confirm": "Poista", + "confirm_delete": "Kyllä, poista kirjasto \"{name}\"", + "title": "Poista kirjasto", + "warning_html": "Kirjasto {name} poistetaan palvelimelta. Tämä ei vaikuta mediatiedostoihisi. Tätä ei voi kumota. Jatka?" + }, + "delete_readlist": { + "button_cancel": "Peruuta", + "button_confirm": "Poista", + "confirm_delete": "Kyllä, poista lukulista \"{name}\"", + "dialog_title": "Poista lukulista", + "warning_html": "Lukulista {name} poistetaan palvelimelta. Tämä ei vaikuta mediatiedostoihisi. Tätä ei voi kumota. Jatka?" + }, + "delete_user": { + "button_cancel": "Peruuta", + "button_confirm": "Poista", + "confirm_delete": "Kyllä, poista käyttäjä \"{name}\"", + "dialog_title": "Poista käyttäjä", + "warning_html": "Käyttäjä {name} poistetaan palvelimelta. Tätä ei voi kumota. Jatka?" + }, + "edit_books": { + "authors_notice_multiple_edit": "Olet muokkaamassa useiden kirjojen kirjailijoita. Tämä ohittaa jokaisen kirjan nykyiset kirjailijat.", + "button_cancel": "Peruuta", + "button_confirm": "Tallenna muutokset", + "dialog_title_multiple": "Muokkaa {count} kirjaa | Muokkaa {count} kirjaa", + "dialog_title_single": "Muokkaa {book}", + "field_isbn": "ISBN", + "field_isbn_error": "Täytyy olla kelvollinen ISBN 13", + "field_number": "Numero", + "field_number_sort": "Lajittelunumero", + "field_number_sort_hint": "Voit käyttää desimaalilukuja", + "field_release_date": "Julkaisupäivämäärä", + "field_release_date_error": "Täytyy olla kelvollinen päivämäärä VVVV-KK-PP -muodossa", + "field_summary": "Yhteenveto", + "field_tags": "Tunnisteet", + "field_title": "Nimi", + "tab_authors": "Kirjailijat", + "tab_general": "Yleiset", + "tab_tags": "Tunnisteet", + "tags_notice_multiple_edit": "Olet muokkaamassa useiden kirjojen tunnisteita. Tämä ohittaa jokaisen kirjan nykyiset tunnisteet." + }, + "edit_collection": { + "button_cancel": "Peruuta", + "button_confirm": "Tallenna muutokset", + "dialog_title": "Muokkaa kokoelmaa", + "field_manual_ordering": "Manuaalinen järjestely", + "label_ordering": "Kokoelman sarjat lajitellaan oletusarvoisesti nimen mukaan. Voit ottaa manuaalisen järjestelyn käyttöön määritelläksesi oman järjestyksesi." + }, + "edit_library": { + "button_browse": "Selaa", + "button_cancel": "Peruuta", + "button_confirm_add": "Lisää", + "button_confirm_edit": "Muokkaa", + "dialog_title_add": "Lisää kirjasto", + "dialot_title_edit": "Muokkaa kirjastoa", + "field_import_barcode_isbn": "ISBN-viivakoodi", + "field_import_comicinfo_book": "Kirjan metatiedot", + "field_import_comicinfo_collections": "Kokoelmat", + "field_import_comicinfo_readlists": "Lukulistat", + "field_import_comicinfo_series": "Sarjan metatiedot", + "field_import_epub_book": "Kirjan metatiedot", + "field_import_epub_series": "Sarjan metatiedot", + "field_import_local_artwork": "Paikallinen kuvamateriaali", + "field_name": "Nimi", + "field_root_folder": "Juurikansio", + "field_scanner_deep_scan": "Syvä skannaus", + "field_scanner_force_directory_modified_time": "Pakota hakemiston muokattu aika", + "file_browser_dialog_button_confirm": "Valitse", + "file_browser_dialog_title": "Kirjaston juurikansio", + "label_import_barcode_isbn": "Tuo ISBN viivakoodin sisällä", + "label_import_comicinfo": "Tuo ComedInfo.xml -tiedoston sisältävien CBR/CBZ -tiedostojen metatiedot", + "label_import_epub": "Tuo metatiedot EPUB-tiedostoista", + "label_import_local": "Tuo paikalliset mediatiedot", + "label_scanner": "Skanneri", + "tab_general": "Yleiset", + "tab_options": "Vaihtoehdot" + }, + "edit_readlist": { + "button_cancel": "Peruuta", + "button_confirm": "Tallenna muutokset", + "dialog_title": "Muokkaa lukulistaa", + "field_name": "Nimi" + }, + "edit_series": { + "button_cancel": "Peruuta", + "button_confirm": "Tallenna muutokset", + "dialog_title_multiple": "Muokkaa {count} sarjaa | Muokkaa {count} sarjaa", + "dialog_title_single": "Muokkaa {series}", + "field_age_rating": "Ikäluokitus", + "field_age_rating_error": "Ikäluokituksen on oltava vähintään 0", + "field_genres": "Tyylilajit", + "field_language": "Kieli", + "field_publisher": "Julkaisija", + "field_reading_direction": "Lukusuunta", + "field_sort_title": "Lajitteluotsikko", + "field_status": "Tila", + "field_summary": "Yhteenveto", + "field_tags": "Tunnisteet", + "field_title": "Nimi", + "mixed": "SEKALAINEN", + "tab_general": "Yleiset", + "tab_tags": "Tunnisteet", + "tags_notice_multiple_edit": "Olet muokkaamassa usean sarjan tunnisteita. Tämä ohittaa jokaisen sarjan nykyiset tunnisteet." + }, + "edit_user": { + "button_cancel": "Peruuta", + "button_confirm": "Tallenna muutokset", + "dialog_title": "Muokkaa käyttäjää", + "label_roles_for": "Roolit käyttäjälle {name}" + }, + "edit_user_shared_libraries": { + "button_cancel": "Peruuta", + "button_confirm": "Tallenna muutokset", + "dialog_title": "Muokkaa jaettuja kirjastoja", + "field_all_libraries": "Kaikki kirjastot", + "label_shared_with": "Jaettu käyttäjän {name} kanssa" + }, + "file_browser": { + "button_cancel": "Peruuta", + "button_confirm_default": "Valitse", + "dialog_title_default": "Tiedostoselain", + "parent_directory": "Emohakemisto" + }, + "password_change": { + "button_cancel": "Peruuta", + "button_confirm": "Vaihda salasana", + "dialog_title": "Vaihda salasana", + "field_new_password": "Uusi salasana", + "field_new_password_error": "Uusi salasana vaaditaan.", + "field_repeat_password": "Toista uusi salasana", + "field_repeat_password_error": "Salasanojen on oltava identtisiä." + }, + "server_stop": { + "button_cancel": "Peruuta", + "button_confirm": "Pysäytä", + "confirmation_message": "Oletko varma, että haluat lopettaa Komgan?", + "dialog_title": "Sammuta palvelin" + }, + "shortcut_help": { + "label_description": "Kuvaus", + "label_key": "Avain" + } + }, + "enums": { + "media_status": { + "ERROR": "Virhe", + "OUTDATED": "Vanhentunut", + "READY": "Valmis", + "UNKNOWN": "Tuntematon", + "UNSUPPORTED": "Ei tuettu" + }, + "reading_direction": { + "LEFT_TO_RIGHT": "Vasemmalta oikealle", + "RIGHT_TO_LEFT": "Oikealta vasemmalle", + "VERTICAL": "Pystysuora", + "WEBTOON": "Webtoon-tyylinen" + }, + "series_status": { + "ABANDONED": "Hylätty", + "ENDED": "Päättynyt", + "HIATUS": "Tauolla", + "ONGOING": "Jatkuva" + } + }, + "error_codes": { + "ERR_1000": "Tiedostoa ei voitu käyttää analyysin aikana", + "ERR_1001": "Mediatyyppiä ei tueta", + "ERR_1002": "Salattuja RAR-arkistoja ei tueta", + "ERR_1003": "Kiinteitä RAR-arkistoja ei tueta", + "ERR_1004": "Moniosaisia RAR-arkistoja ei tueta", + "ERR_1005": "Tuntematon virhe kirjaa analysoitaessa", + "ERR_1006": "Kirja ei sisällä yhtään sivua", + "ERR_1007": "Joitain merkintöjä ei voitu analysoida", + "ERR_1008": "Tuntematon virhe kirjan merkintöjä haettaessa", + "ERR_1009": "Tämänniminen lukulista on jo olemassa", + "ERR_1010": "Yksikään kirja ei vastannut lukuluetteloa", + "ERR_1011": "Ei yksiselitteistä vastinetta sarjalle", + "ERR_1012": "Ei vastinetta sarjalle", + "ERR_1013": "Kirjan numerolle ei löytynyt yksiselitteistä vastinetta sarjan sisältä", + "ERR_1014": "Kirjan numerolle ei löytynyt vastinetta sarjan sisältä", + "ERR_1015": "Virhe deserialisoidessa ComicRack lukulistaa" + }, + "filter": { + "age_rating": "ikäluokitus", + "age_rating_none": "Ei mitään", + "genre": "tyylilaji", + "language": "kieli", + "library": "kirjasto", + "publisher": "julkaisija", + "release_date": "julkaisupäivämäärä", + "status": "tila", + "tag": "tunniste", + "unread": "Lukemattomat" + }, + "filter_drawer": { + "filter": "suodata", + "sort": "lajittele" + }, + "home": { + "theme": "Teema", + "translation": "Käännös" + }, + "library_navigation": { + "browse": "Selaa", + "collections": "Kokoelmat", + "readlists": "Lukulistat" + }, + "login": { + "create_user_account": "Luo käyttäjätili", + "login": "Kirjaudu", + "unclaimed_html": "Tämä Komga-palvelin ei ole vielä aktiivinen. Sinun täytyy luoda käyttäjätili voidaksesi käyttää sitä.

Valitse sähköpostiosoite ja salasana ja klikkaa Luo käyttäjätunnus." + }, + "media_analysis": { + "comment": "Kommentti", + "media_analysis": "Media-analyysi", + "media_type": "Mediatyyppi", + "name": "Nimi", + "size": "Koko", + "status": "Tila", + "url": "URL" + }, + "menu": { + "add_to_collection": "Lisää kokoelmaan", + "add_to_readlist": "Lisää lukulistaan", + "analyze": "Analysoi", + "delete": "Poista", + "download_series": "Lataa sarja", + "edit": "Muokkaa", + "edit_metadata": "Muokkaa metatietoja", + "mark_read": "Merkitse luetuksi", + "mark_unread": "Merkitse lukemattomaksi", + "refresh_metadata": "Päivitä metatiedot", + "scan_library_files": "Skannaa kirjastotiedostot" + }, + "navigation": { + "home": "Koti", + "libraries": "Kirjastot", + "logout": "Kirjaudu ulos" + }, + "page_not_found": { + "go_back_to_home_page": "Takaisin etusivulle", + "page_does_not_exist": "Etsimääsi sivua ei ole olemassa.", + "page_not_found": "Sivua ei löytynyt" + }, + "read_more": { + "less": "Lue vähemmän", + "more": "Lue lisää" + }, + "readlists_expansion_panel": { + "manage_readlist": "Hallitse lukulistaa", + "title": "{name} lukulista" + }, + "search": { + "no_results": "Haku ei tuottanut yhtään tulosta", + "search": "Etsi", + "search_for_something_else": "Yritä etsiä jotain muuta", + "search_results_for": "Tulokset haulle \"{name}\"" + }, + "searchbox": { + "no_results": "Ei tuloksia", + "search_all": "Hae kaikista…" + }, + "server": { + "server_management": { + "button_shutdown": "Sammuta", + "section_title": "Palvelimen hallinta" + }, + "tab_title": "Palvelin" + }, + "server_settings": { + "server_settings": "Palvelimen asetukset" + }, + "settings_user": { + "edit_shared_libraries": "Muokkaa jaettuja kirjastoja", + "edit_user": "Muokkaa käyttäjää", + "role_administrator": "Järjestelmänvalvoja", + "role_user": "Käyttäjä" + }, + "sort": { + "books_count": "Kirjojen määrä", + "date_added": "Lisäyspäivämäärä", + "date_updated": "Päivitetty päivämäärä", + "file_name": "Tiedostonimi", + "file_size": "Tiedoston koko", + "folder_name": "Kansion nimi", + "name": "Nimi", + "number": "Numero", + "release_date": "Julkaisupäivämäärä" + }, + "theme": { + "dark": "Tumma", + "light": "Vaalea", + "system": "Järjestelmä" + }, + "user_roles": { + "ADMIN": "Järjestelmänvalvoja", + "FILE_DOWNLOAD": "Tiedostojen lataus", + "PAGE_STREAMING": "Sivujen suoratoisto", + "USER": "Käyttäjä" + }, + "users": { + "users": "Käyttäjät" + }, + "welcome": { + "add_library": "Lisää kirjasto", + "no_libraries_yet": "Kirjastoja ei ole vielä lisätty!", + "welcome_message": "Tervetuloa Komgaan" + } +} diff --git a/komga-webui/src/locales/fr.json b/komga-webui/src/locales/fr.json index 78a8aa207..b71513e5c 100644 --- a/komga-webui/src/locales/fr.json +++ b/komga-webui/src/locales/fr.json @@ -26,12 +26,18 @@ "penciller": "dessinateurs", "writer": "scénaristes" }, + "book_card": { + "error": "Erreur", + "unknown": "À analyser", + "unsupported": "Non pris en charge" + }, "bookreader": { "beginning_of_book": "Vous êtes au début du livre.", "changing_reading_direction": "Modifier le sens de lecture vers", "cycling_page_layout": "Changer la disposition des pages", "cycling_scale": "Changer la mise à l'échelle", "cycling_side_padding": "Changer la bordure latérale", + "download_current_page": "Télécharger la page courante", "end_of_book": "Vous avez atteint la fin du livre.", "from_series_metadata": "à partir des métadonnées de la série", "move_next": "Cliquez ou appuyez à nouveau sur \"Suivant\" pour passer au livre suivant.", @@ -161,7 +167,7 @@ "book_number": "Nombre de livres : {name}", "book_series": "Série : {name}", "button_import": "Importer", - "comicrack_preambule_html": "Vous pouvez importer des listes de lecture ComicRack au format .cbl.
Komga tentera de faire correspondre les séries fournies et le numéro des livres avec les séries et livres de votre bibliothèque.", + "comicrack_preambule_html": "Vous pouvez importer des listes de lecture ComicRack au format .cbl.
Komga tentera de faire correspondre les séries fournies et le numéro des livres avec les séries et livres de votre bibliothèque.", "field_files_label": "Listes de lecture ComicRack (.cbl)", "import_read_lists": "Importer des listes de lecture", "imported_as": "Importé comme {name}", diff --git a/komga-webui/src/locales/it.json b/komga-webui/src/locales/it.json index d92f79076..16ab71ec5 100644 --- a/komga-webui/src/locales/it.json +++ b/komga-webui/src/locales/it.json @@ -8,7 +8,7 @@ "sortBy": "Ordina per" }, "fileInput": { - "counter": "{0} files", + "counter": "{0} file", "counterSize": "{0} files ({1} in totale)" }, "noDataText": "Nessun elemento disponibile" @@ -18,25 +18,506 @@ "change_password": "cambia password" }, "author_roles": { + "colorist": "coloristi", "cover": "copertina", "editor": "editori", + "inker": "inchiostratori", + "letterer": "letteristi", + "penciller": "disegnatori", "writer": "scrittori" }, + "book_card": { + "error": "Errore", + "unknown": "Da analizzare", + "unsupported": "Non supportato" + }, "bookreader": { "beginning_of_book": "Sei all'inizio del libro.", + "changing_reading_direction": "Cambia la direzione di lettura in", + "cycling_page_layout": "Cambia Layout Pagina", + "cycling_scale": "Cambia Scala", + "cycling_side_padding": "Cambia Distanziamento Laterale", + "download_current_page": "Scarica pagina corrente", "end_of_book": "Hai raggiunto la fine del libro.", "from_series_metadata": "dai metadati della serie", + "move_next": "Clicca o premi nuovamente \"Avanti\" per passare al libro successivo.", + "move_next_exit": "Clicca o premi nuovamente \"Avanti\" per uscire dal lettore.", + "move_previous": "Clicca o premi nuovamente \"Indietro\" per passare al libro precedente.", "paged_reader_layout": { + "double": "Pagine doppie", "double_no_cover": "Pagine doppie (senza copertina)", "single": "Pagina singola" }, "reader_settings": "Impostazioni Lettore", "scale_type": { "continuous_original": "Originale", - "continuous_width": "Adatta a larghezza" + "continuous_width": "Adatta a larghezza", + "height": "Adatta ad altezza", + "original": "Originale", + "screen": "Schermo", + "width": "Adatta a larghezza" + }, + "settings": { + "animate_page_transitions": "Anima le transizioni di pagina", + "background_color": "Colore sfondo", + "background_colors": { + "black": "Nero", + "white": "Bianco" + }, + "display": "Schermo", + "general": "Generali", + "gestures": "Gesti", + "page_layout": "Formato pagina", + "paged": "Opzioni Impaginazione", + "reading_mode": "Modalità di lettura", + "scale_type": "Scala", + "side_padding": "Distanziamento laterale", + "side_padding_none": "Nessuno", + "webtoon": "Opzioni Lettore Webtoon" + }, + "shortcuts": { + "close": "Chiudi", + "cycle_page_layout": "Cambia layout pagina", + "cycle_scale": "Cambia scala", + "cycle_side_padding": "Cambia distanziamento laterale", + "first_page": "Prima pagina", + "last_page": "Ultima pagina", + "left_to_right": "Da sinistra a destra", + "menus": "Menu", + "next_page": "Pagina successiva", + "previous_page": "Pagina precedente", + "reader_navigation": "Navigazione Lettore", + "right_to_left": "Da destra a sinistra", + "settings": "impostazioni", + "show_hide_help": "Mostra/nascondi aiuto", + "show_hide_settings": "Mostra/nascondi le impostazioni", + "show_hide_thumbnails": "Mostra/nascondi miniature", + "show_hide_toolbars": "Mostra/nascondi barre strumenti", + "vertical": "Verticale", + "webtoon": "Webtoon" } }, + "browse_book": { + "comment": "COMMENTO", + "download_file": "Scarica file", + "file": "FILE", + "format": "FORMATO", + "isbn": "ISBN", + "navigation_within_readlist": "Navigazione all'interno della lista di lettura: {name}", + "read_book": "Leggi libro", + "size": "DIMENSIONE" + }, + "browse_collection": { + "edit_collection": "Modifica raccolta", + "edit_elements": "Modifica elementi", + "manual_ordering": "ordinamento manuale" + }, + "browse_readlist": { + "edit_elements": "Modifica elementi", + "edit_readlist": "Modifica elenco lettura" + }, + "browse_series": { + "earliest_year_from_release_dates": "Questo è la data di uscita del primo fumetto della serie", + "series_no_summary": "Questa serie non ha riassunto, quindi ne ho scelto uno per te!", + "summary_from_book": "Riassunto dal libro {number}:" + }, + "collections_expansion_panel": { + "manage_collection": "Gestisci raccolta", + "title": "Raccolta {name}" + }, "common": { - "locale_name": "Italiano" + "all_libraries": "Tutte le librerie", + "books": "Libri", + "books_n": "Nessun libro | 1 libro | {count} libri", + "cancel": "Annulla", + "close": "Chiudi", + "collections": "Raccolte", + "create": "Crea", + "delete": "Elimina", + "download": "Scarica", + "email": "Email", + "filter_no_matches": "Il filtro attivo non ha corrispondenze", + "genre": "Genere", + "go_to_library": "Vai alla libreria", + "locale_name": "Italiano", + "locale_rtl": "false", + "n_selected": "{count} selezionati", + "nothing_to_show": "Niente da mostrare", + "pages": "pagine", + "pages_n": "Nessuna pagina | 1 pagina | {count} pagine", + "password": "Password", + "publisher": "Editore", + "read": "Leggi", + "readlists": "Liste di lettura", + "required": "Richiesto", + "roles": "Ruoli", + "series": "Serie", + "tags": "Etichette", + "use_filter_panel_to_change_filter": "Usa il pannello dei filtri per cambiare il filtro attivo", + "year": "anno" + }, + "dashboard": { + "keep_reading": "Continua a leggere", + "on_deck": "Primo Piano", + "recently_added_books": "Libri aggiunti di recente", + "recently_added_series": "Serie aggiunte di recente", + "recently_updated_series": "Serie aggiornate di recente" + }, + "data_import": { + "book_number": "Libro numero: {name}", + "book_series": "Serie: {name}", + "button_import": "Importa", + "comicrack_preambule_html": "Puoi importare liste di lettura ComicRack esistenti in formato .cbl.
Komga cercherà di far corrispondere la serie e il numero di libri forniti con le serie e i libri nelle tue librerie.", + "field_files_label": "Liste di lettura ComicRack (.cbl)", + "import_read_lists": "Importa Liste Lettura", + "imported_as": "Importato come {name}", + "results_preambule": "Il risultato dell'importazione è mostrato qui sotto. Puoi anche controllare i libri senza corrispondenza per ogni file fornito.", + "size_limit": "Deve essere meno di {size} MB", + "tab_title": "Importa Dati" + }, + "dialog": { + "add_to_collection": { + "button_create": "Crea", + "card_collection_subtitle": "Nessuna serie | 1 serie | {count} serie", + "dialog_title": "Aggiungi alla raccolta", + "field_search_create": "Cerca o crea raccolta", + "field_search_create_error": "Esiste già una raccolta con questo nome", + "label_no_matching_collection": "Nessuna raccolta corrispondente" + }, + "add_to_readlist": { + "button_create": "Crea", + "card_readlist_subtitle": "Nessun libro | 1 libro | {count} libri", + "dialog_title": "Aggiungi all'elenco di lettura", + "field_search_create": "Cerca o crea una lista di lettura", + "field_search_create_error": "Una lista di lettura con questo nome esiste già", + "label_no_matching_readlist": "Nessuna lista di lettura corrispondente" + }, + "add_user": { + "button_cancel": "Annulla", + "button_confirm": "Aggiungi", + "dialog_title": "Aggiungi Utente", + "field_email": "Email", + "field_email_error": "Deve essere un indirizzo e-mail valido", + "field_password": "Password", + "field_role_administrator": "Amministratore", + "field_role_file_download": "Scarica File", + "field_role_page_streaming": "Lettura online", + "label_roles": "Ruoli" + }, + "delete_collection": { + "button_cancel": "Annulla", + "button_confirm": "Elimina", + "confirm_delete": "Sì, elimina la raccolta \"{name}\"", + "dialog_title": "Elimina Raccolta", + "warning_html": "La raccolta {name} sarà rimossa da questo server. I tuoi file non saranno interessati. Questo non può essere annullato. Continuare?" + }, + "delete_library": { + "button_cancel": "Annulla", + "button_confirm": "Elimina", + "confirm_delete": "Sì, elimina la libreria \"{name}\"", + "title": "Elimina Libreria", + "warning_html": "La libreria {name} sarà rimossa da questo server. I tuoi file multimediali non saranno interessati. Questo non può essere annullato. Continuare?" + }, + "delete_readlist": { + "button_cancel": "Annulla", + "button_confirm": "Elimina", + "confirm_delete": "Sì, cancella la lista di lettura \"{name}\"", + "dialog_title": "Elimina Elenco Lettura", + "warning_html": "La lista di lettura {name} sarà rimossa da questo server. I tuoi file multimediali non saranno interessati. Questo non può essere annullato. Continuare?" + }, + "delete_user": { + "button_cancel": "Annulla", + "button_confirm": "Elimina", + "confirm_delete": "Sì, elimina l'utente \"{name}\"", + "dialog_title": "Elimina Utente", + "warning_html": "L'utente {name} sarà cancellato da questo server. Questo non può essere annullato. Continuare?" + }, + "edit_books": { + "authors_notice_multiple_edit": "Stai modificando gli autori per più libri. Questo sovrascriverà gli autori esistenti di ogni libro.", + "button_cancel": "Annulla", + "button_confirm": "Salva modifiche", + "dialog_title_multiple": "Modifica {count} libro | Modifica {count} libri", + "dialog_title_single": "Modifica {book}", + "field_isbn": "ISBN", + "field_isbn_error": "Deve essere un ISBN 13 valido", + "field_number": "Numero", + "field_number_sort": "Numero di ordinamento", + "field_number_sort_hint": "Puoi usare numeri decimali", + "field_release_date": "Data di Uscita", + "field_release_date_error": "Deve essere una data valida nel formato AAAA-MM-GG", + "field_summary": "Riassunto", + "field_tags": "Tag", + "field_title": "Titolo", + "tab_authors": "Autori", + "tab_general": "Generale", + "tab_tags": "Tag", + "tags_notice_multiple_edit": "Stai modificando i tag per più libri. Questo sovrascriverà i tag esistenti di ogni libro." + }, + "edit_collection": { + "button_cancel": "Annulla", + "button_confirm": "Salva modifiche", + "dialog_title": "Modifica raccolta", + "field_manual_ordering": "Ordinamento manuale", + "label_ordering": "Per impostazione predefinita, le serie in una collezione saranno ordinate per nome. Puoi abilitare l'ordinamento manuale per definire il proprio ordine." + }, + "edit_library": { + "button_browse": "Sfoglia", + "button_cancel": "Annulla", + "button_confirm_add": "Aggiungi", + "button_confirm_edit": "Modifica", + "dialog_title_add": "Aggiungi Libreria", + "dialot_title_edit": "Modifica Libreria", + "field_import_barcode_isbn": "Codice a barre ISBN", + "field_import_comicinfo_book": "Metadati del libro", + "field_import_comicinfo_collections": "Raccolte", + "field_import_comicinfo_readlists": "Liste di lettura", + "field_import_comicinfo_series": "Metadati della serie", + "field_import_epub_book": "Metadati del libro", + "field_import_epub_series": "Metadati della serie", + "field_import_local_artwork": "Copertine locali", + "field_name": "Nome", + "field_root_folder": "Cartella principale", + "field_scanner_deep_scan": "Scansione profonda", + "field_scanner_force_directory_modified_time": "Usa data di modifica delle cartelle", + "file_browser_dialog_button_confirm": "Scegli", + "file_browser_dialog_title": "Cartella principale della libreria", + "label_import_barcode_isbn": "Importa ISBN all'interno del codice a barre", + "label_import_comicinfo": "Importa metadati per CBR/CBZ contenenti un file ComicInfo.xml", + "label_import_epub": "Importa metadati dai file EPUB", + "label_import_local": "Importa risorse multimediali locali", + "label_scanner": "Scanner", + "tab_general": "Generale", + "tab_options": "Opzioni" + }, + "edit_readlist": { + "button_cancel": "Annulla", + "button_confirm": "Salva modifice", + "dialog_title": "Modifica elenco di lettura", + "field_name": "Nome" + }, + "edit_series": { + "button_cancel": "Annulla", + "button_confirm": "Salva modifice", + "dialog_title_multiple": "Modifica {count} serie | Modifica {count} serie", + "dialog_title_single": "Modifica {series}", + "field_age_rating": "Fascia d'età", + "field_age_rating_error": "La fascia d'età deve essere pari o superiore a 0", + "field_genres": "Generi", + "field_language": "Lingua", + "field_publisher": "Editore", + "field_reading_direction": "Direzione Lettura", + "field_sort_title": "Titolo di Ordinamento", + "field_status": "Stato", + "field_summary": "Riassunto", + "field_tags": "Tag", + "field_title": "Titolo", + "mixed": "MISTO", + "tab_general": "Generale", + "tab_tags": "Tag", + "tags_notice_multiple_edit": "Stai modificando i tag per più serie. Questo sovrascriverà i tag esistenti di ogni serie." + }, + "edit_user": { + "button_cancel": "Annulla", + "button_confirm": "Salva modifiche", + "dialog_title": "Modifica utente", + "label_roles_for": "Ruoli per {name}" + }, + "edit_user_shared_libraries": { + "button_cancel": "Annulla", + "button_confirm": "Salva modifiche", + "dialog_title": "Modifica librerie condivise", + "field_all_libraries": "Tutte le librerie", + "label_shared_with": "Condiviso con {name}" + }, + "file_browser": { + "button_cancel": "Annulla", + "button_confirm_default": "Scegli", + "dialog_title_default": "Browser di File", + "parent_directory": "Cartella Superiore" + }, + "password_change": { + "button_cancel": "Annulla", + "button_confirm": "Cambia password", + "dialog_title": "Cambia password", + "field_new_password": "Nuova password", + "field_new_password_error": "È richiesta una nuova password.", + "field_repeat_password": "Ripeti la nuova password", + "field_repeat_password_error": "Le password devono essere identiche." + }, + "server_stop": { + "button_cancel": "Annulla", + "button_confirm": "Spegni", + "confirmation_message": "Sei sicuro di voler spegnere Komga?", + "dialog_title": "Spegni il server" + }, + "shortcut_help": { + "label_description": "Descrizione", + "label_key": "Chiave" + } + }, + "enums": { + "media_status": { + "ERROR": "Errore", + "OUTDATED": "Scaduto", + "READY": "Pronto", + "UNKNOWN": "Sconosciuto", + "UNSUPPORTED": "Non supportato" + }, + "reading_direction": { + "LEFT_TO_RIGHT": "Da sinistra a destra", + "RIGHT_TO_LEFT": "Da destra a sinistra", + "VERTICAL": "Verticale", + "WEBTOON": "Webtoon" + }, + "series_status": { + "ABANDONED": "Abbandonato", + "ENDED": "Conclusa", + "HIATUS": "Hiatus", + "ONGOING": "In corso" + } + }, + "error_codes": { + "ERR_1000": "Impossibile accedere al file durante l'analisi", + "ERR_1001": "Tipo di media non supportato", + "ERR_1002": "Gli archivi RAR crittografati non sono supportati", + "ERR_1003": "Gli archivi RAR solidi non sono supportati", + "ERR_1004": "Gli archivi RAR multivolume non sono supportati", + "ERR_1005": "Errore sconosciuto durante l'analisi del libro", + "ERR_1006": "Il libro non contiene alcuna pagina", + "ERR_1007": "Non è stato possibile analizzare alcune voci", + "ERR_1008": "Errore sconosciuto durante il recupero delle voci del libro", + "ERR_1009": "Esiste già una lista di lettura con quel nome", + "ERR_1010": "Nessun libro è stato abbinato all'interno della lista di lettura richiesta", + "ERR_1011": "Nessuna corrispondenza unica per la serie", + "ERR_1012": "Nessuna corrispondenza per la serie", + "ERR_1013": "Nessuna corrispondenza per il numero del fumetto nella serie", + "ERR_1014": "Nessuna corrispondenza per il numero del fumetto nella serie", + "ERR_1015": "Errore durante la deserializzazione della lista di lettura di ComicRack" + }, + "filter": { + "age_rating": "Fascia d'età", + "age_rating_none": "Nessuna", + "genre": "genere", + "language": "lingua", + "library": "libreria", + "publisher": "editore", + "release_date": "data di rilascio", + "status": "stato", + "tag": "tag", + "unread": "Non lette" + }, + "filter_drawer": { + "filter": "filtro", + "sort": "ordina" + }, + "home": { + "theme": "Tema", + "translation": "Traduzione" + }, + "library_navigation": { + "browse": "Esplora", + "collections": "Raccolte", + "readlists": "Liste Lettura" + }, + "login": { + "create_user_account": "Crea account utente", + "login": "Entra", + "unclaimed_html": "Questo server Komga non è ancora attivo, devi creare un account utente per potervi accedere.

Scegli una email e una password e clicca su Crea account utente." + }, + "media_analysis": { + "comment": "Commento", + "media_analysis": "Analisi dei media", + "media_type": "Tipo Media", + "name": "Nome", + "size": "Dimensione", + "status": "Stato", + "url": "URL" + }, + "menu": { + "add_to_collection": "Aggiungi alla raccolta", + "add_to_readlist": "Aggiungi all'elenco di lettura", + "analyze": "Analizza", + "delete": "Elimina", + "download_series": "Scarica serie", + "edit": "Modifica", + "edit_metadata": "Modifica metadati", + "mark_read": "Segna come letto", + "mark_unread": "Segna come non letto", + "refresh_metadata": "Aggiorna metadati", + "scan_library_files": "Scansiona i file della libreria" + }, + "navigation": { + "home": "Home", + "libraries": "Librerie", + "logout": "Esci" + }, + "page_not_found": { + "go_back_to_home_page": "Torna alla pagina iniziale", + "page_does_not_exist": "La pagina che stai cercando non esiste.", + "page_not_found": "Pagina non trovata" + }, + "read_more": { + "less": "Mostra meno", + "more": "Mostra di più" + }, + "readlists_expansion_panel": { + "manage_readlist": "Gestire lista di lettura", + "title": "{name} lista di lettura" + }, + "search": { + "no_results": "La ricerca non ha dato risultati", + "search": "Cerca", + "search_for_something_else": "Prova a cercare qualcos'altro", + "search_results_for": "Risultati della ricerca per \"{name}\"" + }, + "searchbox": { + "no_results": "Nessun risultato", + "search_all": "Cerca tutto…" + }, + "server": { + "server_management": { + "button_shutdown": "Spegni", + "section_title": "Gestione Server" + }, + "tab_title": "Server" + }, + "server_settings": { + "server_settings": "Impostazioni Server" + }, + "settings_user": { + "edit_shared_libraries": "Modifica le librerie condivise", + "edit_user": "Modifica utente", + "role_administrator": "Amministratore", + "role_user": "Utente" + }, + "sort": { + "books_count": "Numero fumetti", + "date_added": "Data di aggiunta", + "date_updated": "Data di aggiornamento", + "file_name": "Nome del file", + "file_size": "Dimensioni del file", + "folder_name": "Nome della cartella", + "name": "Nome", + "number": "Numero", + "release_date": "Data di uscita" + }, + "theme": { + "dark": "Scuro", + "light": "Chiaro", + "system": "Sistema" + }, + "user_roles": { + "ADMIN": "Amministratore", + "FILE_DOWNLOAD": "Scarica File", + "PAGE_STREAMING": "Leggi online", + "USER": "Utente" + }, + "users": { + "users": "Utenti" + }, + "welcome": { + "add_library": "Aggiungi libreria", + "no_libraries_yet": "Non sono ancora state aggiunte librerie!", + "welcome_message": "Benvenuto su Komga" } } diff --git a/komga-webui/src/locales/nb.json b/komga-webui/src/locales/nb.json index 779cdc5ed..15c55f2d0 100644 --- a/komga-webui/src/locales/nb.json +++ b/komga-webui/src/locales/nb.json @@ -26,12 +26,17 @@ "penciller": "tegnere", "writer": "skribenter" }, + "book_card": { + "error": "Feil", + "unsupported": "Ustøttet" + }, "bookreader": { "beginning_of_book": "Du er på begynnelsen av boka.", "changing_reading_direction": "Endrer leseretning til", "cycling_page_layout": "Endrer sideutforming til", "cycling_scale": "Endrer skalering til", "cycling_side_padding": "Endrer sidefyll til", + "download_current_page": "Last ned nåværende side", "end_of_book": "Du har nådd slutten på boka.", "from_series_metadata": "fra serie metadata", "move_next": "Klikk eller trykk «Neste» igjen for å gå til neste bok.", diff --git a/komga-webui/src/locales/sv.json b/komga-webui/src/locales/sv.json index 1a4e4df71..9a8d1bb0d 100644 --- a/komga-webui/src/locales/sv.json +++ b/komga-webui/src/locales/sv.json @@ -26,12 +26,18 @@ "penciller": "formgivare", "writer": "författare" }, + "book_card": { + "error": "Fel", + "unknown": "Ska analyserad", + "unsupported": "Stödjs inte" + }, "bookreader": { "beginning_of_book": "Du är i början av boken.", "changing_reading_direction": "Byt läsriktning till", "cycling_page_layout": "Växla Sidlayout", "cycling_scale": "Växla Skala", "cycling_side_padding": "Växla Spaltfyllnad", + "download_current_page": "Ladda ner aktuell sida", "end_of_book": "Du har kommit till slutet av boken.", "from_series_metadata": "från seriens metadata", "move_next": "Tryck \"Nästa\" igen för att gå till nästa bok.", @@ -161,7 +167,7 @@ "book_number": "Boknummer: {name}", "book_series": "Serie: {name}", "button_import": "Importera", - "comicrack_preambule_html": "Du kan importera existerande ComicRack Läslistor i .cbl-format.
Komga kommer att försöka matcha innehållet med serier och böker i dina bibliotek.", + "comicrack_preambule_html": "Du kan importera existerande ComicRack Läslistor i .cbl-format.
Komga kommer att försöka matcha innehållet med serier och böker i dina bibliotek.", "field_files_label": "ComicRack Läslistor (.cbl)", "import_read_lists": "Importera Läslistor", "imported_as": "Importerad som {name}", diff --git a/komga-webui/src/locales/zh-Hans.json b/komga-webui/src/locales/zh-Hans.json index 6e298d1f6..1b2897372 100644 --- a/komga-webui/src/locales/zh-Hans.json +++ b/komga-webui/src/locales/zh-Hans.json @@ -26,12 +26,18 @@ "penciller": "铅稿", "writer": "作者" }, + "book_card": { + "error": "错误", + "unknown": "待分析", + "unsupported": "不支持" + }, "bookreader": { "beginning_of_book": "你可以开始阅读这本书.", "changing_reading_direction": "将阅读方向更改为", "cycling_page_layout": "切换页模式", "cycling_scale": "切换缩放", "cycling_side_padding": "切换边距", + "download_current_page": "下载当前页面", "end_of_book": "你已经阅读完这本书了.", "from_series_metadata": "来自系列元数据", "move_next": "再次单击或按“下一步”移动到下一本书。", @@ -161,7 +167,7 @@ "book_number": "书号: {name}", "book_series": "系列: {name}", "button_import": "导入", - "comicrack_preambule_html": "您可以导入.cbl格式的阅读列表。
Komga将尝试将你提供的系列和书号与你的库中的系列和书号相匹配。", + "comicrack_preambule_html": "您可以导入.cbl格式的阅读列表。
Komga将尝试将你提供的系列和书号与你的库中的系列和书号相匹配。", "field_files_label": "阅读列表(.cbl)", "import_read_lists": "导入阅读列表", "imported_as": "导入为 {name}", From 03241a636a70c542aa91038836c08254862ec629 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 7 Apr 2021 02:31:22 +0000 Subject: [PATCH 03/39] chore(release): 0.86.0 [skip ci] # [0.86.0](https://github.com/gotson/komga/compare/v0.85.1...v0.86.0) (2021-04-07) ### Bug Fixes * **webui:** series year incorrectly formatted ([d166207](https://github.com/gotson/komga/commit/d16620791243201f2e2eb0910201f73e2c2975f7)) ### Features * added translation using Weblate (Finnish) ([81142ab](https://github.com/gotson/komga/commit/81142ab570ea9ce1cfd964e7c3205d0c1a9ead7a)) --- CHANGELOG.md | 12 ++++++++++++ gradle.properties | 2 +- komga/docs/openapi.json | 32 ++++++++++++++++---------------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9def350be..267c2622c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [0.86.0](https://github.com/gotson/komga/compare/v0.85.1...v0.86.0) (2021-04-07) + + +### Bug Fixes + +* **webui:** series year incorrectly formatted ([d166207](https://github.com/gotson/komga/commit/d16620791243201f2e2eb0910201f73e2c2975f7)) + + +### Features + +* added translation using Weblate (Finnish) ([81142ab](https://github.com/gotson/komga/commit/81142ab570ea9ce1cfd964e7c3205d0c1a9ead7a)) + ## [0.85.1](https://github.com/gotson/komga/compare/v0.85.0...v0.85.1) (2021-03-31) diff --git a/gradle.properties b/gradle.properties index 72f901d97..f85d2ebca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.85.1 +version=0.86.0 diff --git a/komga/docs/openapi.json b/komga/docs/openapi.json index ada7f3db0..0604335ed 100644 --- a/komga/docs/openapi.json +++ b/komga/docs/openapi.json @@ -485,6 +485,10 @@ "format": "int32", "type": "integer" }, + "size": { + "format": "int32", + "type": "integer" + }, "numberOfElements": { "format": "int32", "type": "integer" @@ -492,10 +496,6 @@ "last": { "type": "boolean" }, - "size": { - "format": "int32", - "type": "integer" - }, "totalPages": { "format": "int32", "type": "integer" @@ -616,6 +616,10 @@ "format": "int32", "type": "integer" }, + "size": { + "format": "int32", + "type": "integer" + }, "numberOfElements": { "format": "int32", "type": "integer" @@ -623,10 +627,6 @@ "last": { "type": "boolean" }, - "size": { - "format": "int32", - "type": "integer" - }, "totalPages": { "format": "int32", "type": "integer" @@ -662,6 +662,10 @@ "format": "int32", "type": "integer" }, + "size": { + "format": "int32", + "type": "integer" + }, "numberOfElements": { "format": "int32", "type": "integer" @@ -669,10 +673,6 @@ "last": { "type": "boolean" }, - "size": { - "format": "int32", - "type": "integer" - }, "totalPages": { "format": "int32", "type": "integer" @@ -758,6 +758,10 @@ "format": "int32", "type": "integer" }, + "size": { + "format": "int32", + "type": "integer" + }, "numberOfElements": { "format": "int32", "type": "integer" @@ -765,10 +769,6 @@ "last": { "type": "boolean" }, - "size": { - "format": "int32", - "type": "integer" - }, "totalPages": { "format": "int32", "type": "integer" From 9d3707534f0e016b3624dd6afa78f679da44532a Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 7 Apr 2021 11:10:51 +0800 Subject: [PATCH 04/39] ci: replace stalebot by actions/stale --- .github/stale.yml | 18 ------------------ .github/workflows/stale.yml | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) delete mode 100644 .github/stale.yml create mode 100644 .github/workflows/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index b2741c349..000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - - tech debt -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..3399368f4 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,23 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + stale-pr-message: > + This pull request has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + days-before-stale: 90 + days-before-close: 14 + exempt-issue-labels: 'pinned,security,tech debt' + exempt-all-assignees: true From f3cc6f6e916862741cd7ff3aafa98a4c587653c6 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 19 Apr 2021 11:31:15 +0200 Subject: [PATCH 05/39] feat: added translation using Weblate (Esperanto, Polish) Currently translated at 74.1% (287 of 387 strings) feat: added translation using Weblate (Esperanto) fix: translated using Weblate (Finnish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Italian) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Swedish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (German) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Spanish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (French) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (French) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Italian) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Swedish) Currently translated at 100.0% (387 of 387 strings) fix: translated using Weblate (Polish) Currently translated at 99.2% (384 of 387 strings) feat: added translation using Weblate (Polish) Co-authored-by: Gauthier Co-authored-by: Hosted Weblate Co-authored-by: J. Lavoie Co-authored-by: P K Co-authored-by: Shjosan Co-authored-by: Simone Chiavaccini Co-authored-by: phlostically Translate-URL: https://hosted.weblate.org/projects/komga/webui/de/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/eo/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/es/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/fi/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/fr/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/it/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/pl/ Translate-URL: https://hosted.weblate.org/projects/komga/webui/sv/ Translation: komga/webui Co-authored-by: Gauthier Co-authored-by: J. Lavoie Co-authored-by: P K Co-authored-by: Shjosan Co-authored-by: Simone Chiavaccini Co-authored-by: phlostically --- komga-webui/src/locales/de.json | 268 ++++++++-------- komga-webui/src/locales/eo.json | 415 +++++++++++++++++++++++++ komga-webui/src/locales/es.json | 46 +-- komga-webui/src/locales/fi.json | 16 +- komga-webui/src/locales/fr.json | 12 +- komga-webui/src/locales/it.json | 92 +++--- komga-webui/src/locales/pl.json | 523 ++++++++++++++++++++++++++++++++ komga-webui/src/locales/sv.json | 26 +- 8 files changed, 1168 insertions(+), 230 deletions(-) create mode 100644 komga-webui/src/locales/eo.json create mode 100644 komga-webui/src/locales/pl.json diff --git a/komga-webui/src/locales/de.json b/komga-webui/src/locales/de.json index 9ce8c06a4..3ebe0119d 100644 --- a/komga-webui/src/locales/de.json +++ b/komga-webui/src/locales/de.json @@ -19,7 +19,7 @@ }, "author_roles": { "colorist": "Kolorist", - "cover": "Titelbild (Cover)", + "cover": "Titelbild", "editor": "Editoren", "inker": "Tuscher", "letterer": "Setzer", @@ -27,34 +27,34 @@ "writer": "Schriftsteller" }, "book_card": { - "error": "Störung", + "error": "Fehler", "unknown": "Noch nicht analisiert", - "unsupported": "Unbekanntes Format" + "unsupported": "Nicht unterstützt" }, "bookreader": { "beginning_of_book": "Sie sind am Anfang des Buches.", - "changing_reading_direction": "Ändere Leserichtung zu", - "cycling_page_layout": "Ändere Seitendarstellung", - "cycling_scale": "Ändere die Skalierung", + "changing_reading_direction": "Leserichtung ändern zu", + "cycling_page_layout": "Seitendarstellung ändern", + "cycling_scale": "Skalierung ändern", "cycling_side_padding": "Seitenrand ändern", - "download_current_page": "Lade die aktuelle Seite herunter", + "download_current_page": "Aktuelle Seite herunterladen", "end_of_book": "Sie haben das Ende des Buches erreicht.", "from_series_metadata": "Metadaten von Serie", - "move_next": "Klicken oder drücken Sie \"Weiter\" um zum nächsten Buch zu gelangen.", - "move_next_exit": "Klicken oder drücken Sie \"Weiter\" erneut um den Lesemodus zu beenden.", - "move_previous": "Klicken oder drücken Sie erneut auf \"Vorherige Seite\", um zum letzten Buch zu wechseln.", + "move_next": "Klicken oder drücken Sie „Weiter“, um zum nächsten Buch zu gelangen.", + "move_next_exit": "Klicken oder drücken Sie „Weiter“ erneut, um den Lesemodus zu beenden.", + "move_previous": "Klicken oder drücken Sie erneut auf „Vorherige Seite“, um zum letzten Buch zu wechseln.", "paged_reader_layout": { "double": "Doppelseite", "double_no_cover": "Doppelseitig ohne Titelbild", - "single": "Einseitig" + "single": "Einzelne Seite" }, - "reader_settings": "Reader Einstellungen", + "reader_settings": "Lesemoduseinstellungen", "scale_type": { "continuous_original": "Original", "continuous_width": "An Breite ausrichten", "height": "An Höhe ausrichten", "original": "Original", - "screen": "Vollbild", + "screen": "Bildschirm", "width": "An Breite ausrichten" }, "settings": { @@ -65,29 +65,29 @@ "white": "Weiß" }, "display": "Anzeige", - "general": "Allgemeines", + "general": "Allgemein", "gestures": "Gesten", "page_layout": "Seiteneinstellungen", "paged": "Einstellungen zur Seitenausrichtung", "reading_mode": "Lesemodus", - "scale_type": "Maßstab", + "scale_type": "Skalierung", "side_padding": "Seitenränder", "side_padding_none": "Keine", "webtoon": "Webtoon Lese-Einstellungen" }, "shortcuts": { - "close": "Verlasse den Reader", + "close": "Schließen", "cycle_page_layout": "Wechsle Seitendarstellung", "cycle_scale": "Ändern der Skalierung", "cycle_side_padding": "Seitenrand ändern", "first_page": "Erste Seite", "last_page": "Letzte Seite", - "left_to_right": "von Links nach Rechts", - "menus": "Menü", + "left_to_right": "Von links nach rechts", + "menus": "Menüs", "next_page": "Nächste Seite", "previous_page": "Vorherige Seite", "reader_navigation": "Reader Steuerung", - "right_to_left": "von Rechts nach Links", + "right_to_left": "Von rechts nach links", "settings": "Einstellungen", "show_hide_help": "Zeige/Verstecke Hilfe", "show_hide_settings": "Zeige/Verstecke Einstellungsmenü", @@ -100,15 +100,15 @@ "browse_book": { "comment": "Kommentar", "download_file": "Datei herunterladen", - "file": "Datei", - "format": "Format", + "file": "DATEI", + "format": "FORMAT", "isbn": "ISBN", "navigation_within_readlist": "Wechseln zur Leseliste: {name}", "read_book": "Lese Buch", - "size": "Dateigröße" + "size": "GRÖẞE" }, "browse_collection": { - "edit_collection": "Ändere Sammlung", + "edit_collection": "Sammlung bearbeiten", "edit_elements": "Ändern", "manual_ordering": "Manuelle Sortierung" }, @@ -131,28 +131,28 @@ "books_n": "Kein Buch | 1 Buch | {count} Bücher", "cancel": "Abbrechen", "close": "Schließen", - "collections": "Sammlung", - "create": "Erstelle", + "collections": "Sammlungen", + "create": "Erstellen", "delete": "Löschen", - "download": "Download", + "download": "Herunterladen", "email": "E-Mail", "filter_no_matches": "Die Suchkriterien haben kein Ergebnis geliefert", - "genre": "Genre/Gattung", + "genre": "Genre", "go_to_library": "Gehe zu Bibliothek", "locale_name": "Deutsch", "locale_rtl": "false", "n_selected": "{count} markiert", - "nothing_to_show": "Keine Treffer gefunden die angezeigt werden könnten", + "nothing_to_show": "Nichts anzuzeigen", "pages": "Seiten", "pages_n": "Keine Seite | 1 Seite | {count} Seiten", "password": "Passwort", "publisher": "Verlag", - "read": "Lese", + "read": "Lesen", "readlists": "Leseliste", - "required": "Benötigt", + "required": "Notwendig", "roles": "Rollen", - "series": "Series/Reihe", - "tags": "Tags/Stichworte", + "series": "Serie", + "tags": "Stichwörter", "use_filter_panel_to_change_filter": "Mit dem Suchfeld können die Suchkriterien eingestellt werden", "year": "Jahr" }, @@ -166,7 +166,7 @@ "data_import": { "book_number": "Buch Nummer: {name}", "book_series": "Serie: {name}", - "button_import": "Übernehme", + "button_import": "Importieren", "comicrack_preambule_html": "Es ist möglich ComicRack Leselisten im Format .cbl zu übernehmen.
Komga versucht die übernommenen Serien und Buchnummern mit den Serien und Büchern in der Bibliothek abzugleichen.", "field_files_label": "ComicRack Leseliste (.cbl)", "import_read_lists": "Übernehme Leseliste", @@ -177,15 +177,15 @@ }, "dialog": { "add_to_collection": { - "button_create": "Erstelle", + "button_create": "Erstellen", "card_collection_subtitle": "Keine Serie | 1 Serie | {count} Serien", - "dialog_title": "Füge zu Sammlung hinzu", + "dialog_title": "Zu Sammlung hinzufügen", "field_search_create": "Suche oder erzeuge eine Sammlung", "field_search_create_error": "Eine Sammlung mit diesem Namen ist bereits vorhanden", "label_no_matching_collection": "Keine Sammlung gefunden" }, "add_to_readlist": { - "button_create": "Erzeuge", + "button_create": "Erstellen", "card_readlist_subtitle": "Kein Buch | 1 Buch| {count} Bücher", "dialog_title": "Zur Leseliste hinzufügen", "field_search_create": "Durchsuche oder Erzeuge eine Leseliste", @@ -193,49 +193,49 @@ "label_no_matching_readlist": "Keine passende Leseliste" }, "add_user": { - "button_cancel": "Abbruch", + "button_cancel": "Abbrechen", "button_confirm": "Hinzufügen", - "dialog_title": "Anwender hinzufügen", + "dialog_title": "Benutzer hinzufügen", "field_email": "E-Mail", - "field_email_error": "Die E-Mail Adresse muss eine gültige Adresse sein", + "field_email_error": "Die E-Mail-Adresse muss eine gültige Adresse sein", "field_password": "Passwort", - "field_role_administrator": "Administrator", - "field_role_file_download": "Datei herunterladen", + "field_role_administrator": "Systemverwalter", + "field_role_file_download": "Datei-Herunterladen", "field_role_page_streaming": "Seitenaufbau", "label_roles": "Rollen" }, "delete_collection": { - "button_cancel": "Abbruch", - "button_confirm": "Lösche", - "confirm_delete": "Ja, lösche die Sammlung \"{name}\"", - "dialog_title": "Lösche Sammmlung", + "button_cancel": "Abbrechen", + "button_confirm": "Löschen", + "confirm_delete": "Ja, die Sammlung „{name}“ löschen", + "dialog_title": "Sammlung löschen", "warning_html": "Die Sammlung {name} wird vom Server entfernt. Die physikalischen Dateien bleiben unverändert. Dies kann NICHT rückgängig gemacht werden. Fortfahren?" }, "delete_library": { - "button_cancel": "Abbruch", - "button_confirm": "Lösche", - "confirm_delete": "Ja, lösche die Bibliothek \"{name}\"", - "title": "Lösche Bibliothek", + "button_cancel": "Abbrechen", + "button_confirm": "Löschen", + "confirm_delete": "Ja, die Bibliothek „{name}“ löschen", + "title": "Bibliothek löschen", "warning_html": "Die Bibliothek {name} wird vom Server gelöscht. Die physikalischen Dateien bleiben unverändert. Dies kann NICHT rückgängig gemacht werden. Fortfahren?" }, "delete_readlist": { - "button_cancel": "Abbruch", + "button_cancel": "Abbrechen", "button_confirm": "Löschen", - "confirm_delete": "Ja, lösche die Leseliste \"{name}\"", - "dialog_title": "Lösche Leseliste", + "confirm_delete": "Ja, die Leseliste „{name}“ löschen", + "dialog_title": "Leseliste löschen", "warning_html": "Die Leseliste {name} wird vom Server entfernt. Die physikalischen Dateien bleiben unverändert. Dies kann NICHT rückgängig gemacht werden. Fortfahren?" }, "delete_user": { - "button_cancel": "Abbruch", - "button_confirm": "Lösche", - "confirm_delete": "Ja, lösche den Anwender \"{name}\"", - "dialog_title": "Anwender löschen", - "warning_html": "Der Anwender {name} wird gelöscht. Dies kann NICHT rückgängig gemacht werden. Fortfahren?" + "button_cancel": "Abbrechen", + "button_confirm": "Löschen", + "confirm_delete": "Ja, den Benutzer „{name}“ löschen", + "dialog_title": "Benutzer löschen", + "warning_html": "Der Benutzer {name} wird gelöscht. Dies kann NICHT rückgängig gemacht werden. Fortfahren?" }, "edit_books": { "authors_notice_multiple_edit": "Sie verändern mehrere Bücher. Dies wird die existierenden Einträge der Autoren jedes einzelnen Buches durch den neuen Eintrag ersetzen.", - "button_cancel": "Abbruch", - "button_confirm": "Speichere die Änderungen", + "button_cancel": "Abbrechen", + "button_confirm": "Änderungen speichern", "dialog_title_multiple": "Bearbeite {count} Bücher|Bearbeite {count} Bücher", "dialog_title_single": "Bearbeite {book}", "field_isbn": "ISBN", @@ -243,43 +243,43 @@ "field_number": "Nummer", "field_number_sort": "Reihenfolgennummer", "field_number_sort_hint": "Sie können Dezimalzahlen verwenden", - "field_release_date": "Erscheinungsdatum", - "field_release_date_error": "Das Datum muss im Format YYYY-MM-DD sein", + "field_release_date": "Veröffentlichungsdatum", + "field_release_date_error": "Das Datum muss im Format JJJJ-MM-TT sein", "field_summary": "Zusammenfassung", - "field_tags": "Stichworte (Tags)", + "field_tags": "Stichwörter", "field_title": "Titel", - "tab_authors": "Authoren", + "tab_authors": "Autoren", "tab_general": "Allgemein", - "tab_tags": "Stichworte (Tags)", + "tab_tags": "Stichwörter", "tags_notice_multiple_edit": "Sie verändern die Stichwörter von mehreren Büchern. Dies wird die existierenden Einträge der Stichwörter jedes einzelnen Buches durch den neuen Eintrag ersetzen." }, "edit_collection": { - "button_cancel": "Abbruch", - "button_confirm": "Speichere die Änderungen", - "dialog_title": "Bearbeite Sammlung", + "button_cancel": "Abrechen", + "button_confirm": "Änderungen speichern", + "dialog_title": "Sammlung bearbeiten", "field_manual_ordering": "Manuelle Sortierung", "label_ordering": "Sammlungen werden standardmäßig nach Namen sortiert. Sie können die manuelle Sortierung aktivieren und eine eigene Sortierfolge festlegen." }, "edit_library": { - "button_browse": "Stöbern", - "button_cancel": "Abbruch", + "button_browse": "Durchsuchen", + "button_cancel": "Abbrechen", "button_confirm_add": "Hinzufügen", - "button_confirm_edit": "Bearbeite", - "dialog_title_add": "Füge Bibliothek hinzu", - "dialot_title_edit": "Bearbeite Bibliothek", + "button_confirm_edit": "Bearbeiten", + "dialog_title_add": "Bibliothek hinzufügen", + "dialot_title_edit": "Bibliothek bearbeiten", "field_import_barcode_isbn": "ISBN Strichcode", "field_import_comicinfo_book": "Buch Metadaten", - "field_import_comicinfo_collections": "Sammlung", + "field_import_comicinfo_collections": "Sammlungen", "field_import_comicinfo_readlists": "Leseliste", "field_import_comicinfo_series": "Serien Metadaten", "field_import_epub_book": "Buch Metadaten", "field_import_epub_series": "Serien Metadaten", "field_import_local_artwork": "Lokale Artwork", "field_name": "Name", - "field_root_folder": "Hauptverzeichnis", + "field_root_folder": "Stammordner", "field_scanner_deep_scan": "Tiefensuche", "field_scanner_force_directory_modified_time": "Erzwinge das Durchsuchen des Verzeichnis auf Basis des Zeitstempels", - "file_browser_dialog_button_confirm": "Akzeptiert", + "file_browser_dialog_button_confirm": "Auswählen", "file_browser_dialog_title": "Bibliothekshauptverzeichnis", "label_import_barcode_isbn": "Importieren Sie die ISBN aus einem Barcode", "label_import_comicinfo": "Importiere aus einer vorhandenen ComicInfo.xml Datei die Metadaten für CBR/CBZ", @@ -290,63 +290,63 @@ "tab_options": "Optionen" }, "edit_readlist": { - "button_cancel": "Abbruch", + "button_cancel": "Abbrechen", "button_confirm": "Änderungen speichern", "dialog_title": "Bearbeite die Leseliste", - "field_name": "Name der Leseliste" + "field_name": "Name" }, "edit_series": { - "button_cancel": "Abbruch", + "button_cancel": "Abbrechen", "button_confirm": "Speichere die Änderungen", "dialog_title_multiple": "Bearbeite {count} Serien | Bearbeite {count} Serien", "dialog_title_single": "Bearbeite {series}", "field_age_rating": "Altersfreigabe", "field_age_rating_error": "Altersfreigabe muß größer 0 sein", - "field_genres": "Genre/Gattung", + "field_genres": "Genre", "field_language": "Sprache", "field_publisher": "Verlag", "field_reading_direction": "Leserichtung", "field_sort_title": "Nach Titel sortiert", "field_status": "Status", "field_summary": "Zusammenfassung", - "field_tags": "Stichworte (Tags)", + "field_tags": "Stichwörter", "field_title": "Titel", "mixed": "Vermischtes", "tab_general": "Allgemein", - "tab_tags": "Stichworte (Tags)", + "tab_tags": "Stichwörter", "tags_notice_multiple_edit": "Sie verändern mehrere Stichwörter. Dies wird die existierenden Einträge der Stichwörter aller Serien durch den neuen Eintrag ersetzen." }, "edit_user": { - "button_cancel": "Abbruch", + "button_cancel": "Abbrechen", "button_confirm": "Änderungen speichern", - "dialog_title": "Bearbeite Anwender", + "dialog_title": "Benutzerdaten bearbeiten", "label_roles_for": "Rollen für {name}" }, "edit_user_shared_libraries": { - "button_cancel": "Abbruch", + "button_cancel": "Abbrechen", "button_confirm": "Änderungen speichern", "dialog_title": "Bearbeite gemeinsame Bibliotheken", "field_all_libraries": "Alle Bibliotheken", "label_shared_with": "Teile mit {name}" }, "file_browser": { - "button_cancel": "Abbruch", - "button_confirm_default": "Akzeptiert", - "dialog_title_default": "Datei Browser", - "parent_directory": "Übergeordnetes Verzeichnis" + "button_cancel": "Abbrechen", + "button_confirm_default": "Auswählen", + "dialog_title_default": "Dateibrowser", + "parent_directory": "Übergeordnet" }, "password_change": { - "button_cancel": "Abbruch", - "button_confirm": "Ändere das Passwort", - "dialog_title": "Passwortänderung", + "button_cancel": "Abbrechen", + "button_confirm": "Passwort ändern", + "dialog_title": "Passwort ändern", "field_new_password": "Neues Passwort", "field_new_password_error": "Passworterneuerung ist notwendig.", - "field_repeat_password": "Geben Sie das Passwort erneut ein", + "field_repeat_password": "Neues Passwort wiederholen", "field_repeat_password_error": "Das Passwort muss übereinstimmen." }, "server_stop": { - "button_cancel": "Abbruch", - "button_confirm": "Stop", + "button_cancel": "Abbrechen", + "button_confirm": "Herunterfahren", "confirmation_message": "Sind Sie sicher das Sie Komga beenden wollen?", "dialog_title": "Server herunterfahren" }, @@ -359,13 +359,13 @@ "media_status": { "ERROR": "Fehler", "OUTDATED": "Veraltet", - "READY": "Bereit", + "READY": "Fertig", "UNKNOWN": "Unbekannt", "UNSUPPORTED": "Nicht unterstützt" }, "reading_direction": { - "LEFT_TO_RIGHT": "von Links nach Rechts", - "RIGHT_TO_LEFT": "von Rechts nach Links", + "LEFT_TO_RIGHT": "Von links nach rechts", + "RIGHT_TO_LEFT": "Von rechts nach links", "VERTICAL": "Vertikal", "WEBTOON": "Webtoon" }, @@ -396,53 +396,53 @@ }, "filter": { "age_rating": "Altersfreigabe", - "age_rating_none": "Bei Serien ohne Altersbeschränkungen soll dieser Wert im Filter angezeigt werden", - "genre": "Genre/Gattung", + "age_rating_none": "Keine", + "genre": "Genre", "language": "Sprache", "library": "Bibliothek", "publisher": "Verlag", - "release_date": "Erscheinungsdatum", + "release_date": "Veröffentlichungsdatum", "status": "Status", - "tag": "Stichwort (Tag)", + "tag": "Srichwort", "unread": "Ungelesen" }, "filter_drawer": { - "filter": "Auswahlkriterium (Filter)", + "filter": "Filter", "sort": "Sortierung" }, "home": { - "theme": "Vorlage", + "theme": "Design", "translation": "Übersetzung" }, "library_navigation": { - "browse": "Stöbern", - "collections": "Sammlung", + "browse": "Durchsuchen", + "collections": "Sammlungen", "readlists": "Leseliste" }, "login": { - "create_user_account": "Erstelle Anwenderkonto", - "login": "Login", - "unclaimed_html": "Dieser Komga Server ist noch nicht bereit. Sie müssen zuerst einen Anwenderkonto anlegen um sich anmelden zu können.

Hinterlegen Sie eine E-Mail Adresse und vergeben Sie ein Passwort. Klicken Sie dann auf Erstelle Anwenderkonto." + "create_user_account": "Benutzerkonto erstellen", + "login": "Anmelden", + "unclaimed_html": "Dieser Komga-Server ist noch nicht bereit. Sie müssen zuerst einen Benutzerkonto anlegen, um sich anmelden zu können.

Hinterlegen Sie eine E-Mail-Adresse und vergeben Sie ein Passwort. Klicken Sie dann auf Benutzerkonto erstellen." }, "media_analysis": { "comment": "Kommentar", "media_analysis": "Medien Analyse", - "media_type": "Medienart", + "media_type": "Medientyp", "name": "Name", "size": "Größe", "status": "Status", "url": "URL" }, "menu": { - "add_to_collection": "Füge zu Sammlung hinzu", + "add_to_collection": "Zu Sammlung hinzufügen", "add_to_readlist": "Füge zur Leseliste hinzu", - "analyze": "Analysiere", - "delete": "Lösche", + "analyze": "Analysieren", + "delete": "Löschen", "download_series": "Lade die gesamte Serie herunter", - "edit": "Ändere", - "edit_metadata": "Ändere Metadaten", - "mark_read": "Markiere als gelesen", - "mark_unread": "Markiere als ungelesen", + "edit": "Bearbeiten", + "edit_metadata": "Metadaten bearbeiten", + "mark_read": "Als gelesen markieren", + "mark_unread": "Als ungelesen markieren", "refresh_metadata": "Erneuere Metadaten", "scan_library_files": "Überprüfe (Scan) Bibliotheksdateien" }, @@ -452,13 +452,13 @@ "logout": "Abmelden" }, "page_not_found": { - "go_back_to_home_page": "Zurück zur Übersicht", + "go_back_to_home_page": "Zurück zur Startseite", "page_does_not_exist": "Die ausgewählte Seite existiert nicht.", "page_not_found": "Seite nicht gefunden" }, "read_more": { "less": "Zeige weniger an", - "more": "Zeige den ganzen Text an" + "more": "Weiterlesen" }, "readlists_expansion_panel": { "manage_readlist": "Leselistenverwaltung", @@ -468,10 +468,10 @@ "no_results": "Die Suche lieferte keine Treffer", "search": "Suche", "search_for_something_else": "Versuche ein anderes Suchkriterium", - "search_results_for": "Suchergegnis für \"{name}\"" + "search_results_for": "Suchergegnis für „{name}“" }, "searchbox": { - "no_results": "Kein Ergebnis", + "no_results": "Keine Ergebnisse", "search_all": "Durchsuche alles nach …" }, "server": { @@ -486,20 +486,20 @@ }, "settings_user": { "edit_shared_libraries": "Bearbeite gemeinsame Bibliotheken", - "edit_user": "Bearbeite den Anwender", - "role_administrator": "Administrator", - "role_user": "Anwender" + "edit_user": "Benutzerdaten bearbeiten", + "role_administrator": "Systemverwalter", + "role_user": "Benutzer" }, "sort": { "books_count": "Anzahl der Bücher", - "date_added": "Nach Hinzufügedatum sortiert", - "date_updated": "Nach letzter Aktuallisierung sortiert", + "date_added": "Hinzufügedatum", + "date_updated": "Aktualisierungsdatum", "file_name": "Dateiname", - "file_size": "Nach Dateigröße sortiert", - "folder_name": "Name des Ordners", + "file_size": "Dateigröße", + "folder_name": "Ordnername", "name": "Nach Name sortiert", - "number": "Nach Nummer sortiert", - "release_date": "Nach Erscheinungsdatum sortiert" + "number": "Nummer", + "release_date": "Veröffentlichungsdatum" }, "theme": { "dark": "Dunkel", @@ -507,16 +507,16 @@ "system": "System" }, "user_roles": { - "ADMIN": "Administrator", - "FILE_DOWNLOAD": "Datei herunterladen", + "ADMIN": "Systemverwalter", + "FILE_DOWNLOAD": "Datei-Herunterladen", "PAGE_STREAMING": "Online lesen", - "USER": "Anwender" + "USER": "Benutzer" }, "users": { - "users": "Anwender" + "users": "Benutzer" }, "welcome": { - "add_library": "Füge Bibliothek hinzu", + "add_library": "Bibliothek hinzufügen", "no_libraries_yet": "Es wurden bisher noch keine Bibliothken hinzugefügt!", "welcome_message": "Willkommen zu Komga" } diff --git a/komga-webui/src/locales/eo.json b/komga-webui/src/locales/eo.json new file mode 100644 index 000000000..bc7845e87 --- /dev/null +++ b/komga-webui/src/locales/eo.json @@ -0,0 +1,415 @@ +{ + "$vuetify": { + "dataFooter": { + "pageText": "{0}-{1} el {2}" + }, + "dataTable": { + "itemsPerPageText": "Rikordoj en paĝo:", + "sortBy": "Ordigi laŭ" + }, + "fileInput": { + "counter": "{0} dosieroj", + "counterSize": "{0} dosieroj ({1} entute)" + }, + "noDataText": "Neniuj datenoj disponeblas" + }, + "account_settings": { + "account_settings": "Agordoj pri Konto", + "change_password": "ŝanĝi pasvorton" + }, + "author_roles": { + "colorist": "kolorigistoj", + "cover": "kovrilartistoj", + "editor": "redaktoroj", + "inker": "inkistoj", + "letterer": "skribistoj", + "penciller": "desegnistoj", + "writer": "verkistoj" + }, + "book_card": { + "error": "Eraro", + "unknown": "Analizota", + "unsupported": "Ne subtenata" + }, + "bookreader": { + "beginning_of_book": "Vi estas ĉe la komenco de la libro.", + "download_current_page": "Elŝuti aktualan paĝon", + "end_of_book": "Vi atingis la finon de la libro.", + "from_series_metadata": "el metadatenoj de la serio", + "paged_reader_layout": { + "double": "Duope", + "double_no_cover": "Duope (sen kovrilo)", + "single": "Unuope" + }, + "reader_settings": "Agordoj pri Legilo", + "scale_type": { + "continuous_original": "Originalo", + "continuous_width": "Adapti al larĝo", + "height": "Adapti al alto", + "original": "Originalo", + "screen": "Ekrano", + "width": "Adapti al larĝo" + }, + "settings": { + "background_color": "Fona koloro", + "background_colors": { + "black": "Nigro", + "white": "Blanko" + }, + "general": "Ĝenerala", + "gestures": "Gestoj", + "reading_mode": "Reĝimo de legado", + "side_padding_none": "Nenio", + "webtoon": "Opcioj pri Legilo de Retkomiksoj" + }, + "shortcuts": { + "close": "Fermi", + "first_page": "Unua paĝo", + "last_page": "Lasta paĝo", + "left_to_right": "Demaldekstre Dekstren", + "menus": "Menuoj", + "next_page": "Sekva paĝo", + "previous_page": "Antaŭa paĝo", + "reader_navigation": "Navigado de Legilo", + "right_to_left": "Dedekstre Maldekstren", + "settings": "Agordoj", + "show_hide_help": "Montri/kaŝi helpon", + "show_hide_toolbars": "Montri/kaŝi ilobretojn", + "vertical": "Vertikala", + "webtoon": "Retkomikso" + } + }, + "browse_book": { + "comment": "KOMENTO", + "download_file": "Elŝuti dosieron", + "file": "DOSIERO", + "format": "FORMO", + "isbn": "ISBN", + "read_book": "Legi libron", + "size": "GRANDO" + }, + "browse_collection": { + "edit_collection": "Redakti kolekton", + "edit_elements": "Redakti elementojn" + }, + "browse_readlist": { + "edit_elements": "Redakti elementojn", + "edit_readlist": "Redakti legoliston" + }, + "browse_series": { + "summary_from_book": "Resumo el libro {number}:" + }, + "collections_expansion_panel": { + "manage_collection": "Mastrumi kolekton", + "title": "Kolekto {name}" + }, + "common": { + "all_libraries": "Ĉiuj Bibliotekoj", + "books": "Libroj", + "books_n": "Neniuj libroj | 1 libro | {count} libroj", + "cancel": "Nuligi", + "close": "Fermi", + "collections": "Kolektoj", + "create": "Krei", + "delete": "Forigi", + "download": "Elŝuti", + "email": "Retpoŝta adreso", + "genre": "Ĝenro", + "go_to_library": "Iri al biblioteko", + "locale_name": "Esperanto", + "locale_rtl": "false", + "n_selected": "{count} elektiĝis", + "pages": "paĝoj", + "pages_n": "Neniuj paĝoj | 1 paĝo | {count} paĝoj", + "password": "Pasvorto", + "publisher": "Eldonejo", + "read": "Legi", + "readlists": "Legolistoj", + "required": "Deviga", + "roles": "Roloj", + "series": "Serio", + "tags": "Etikedoj", + "year": "jaro" + }, + "data_import": { + "book_number": "Numero de libro: {name}", + "book_series": "Serio: {name}", + "button_import": "Enporti", + "field_files_label": "Legolistoj ComicRack (.cbl)", + "import_read_lists": "Enporti Legolistojn", + "imported_as": "Enportita kiel {name}", + "tab_title": "Enportado de Datenoj" + }, + "dialog": { + "add_to_collection": { + "button_create": "Krei", + "card_collection_subtitle": "Neniuj serioj | 1 serio | {count} serioj", + "dialog_title": "Aldoni en kolekton", + "field_search_create": "Serĉi aŭ krei kolekton" + }, + "add_to_readlist": { + "button_create": "Krei", + "card_readlist_subtitle": "Neniuj libroj | 1 libro | {count} libroj", + "dialog_title": "Aldoni en legoliston", + "field_search_create": "Serĉi aŭ krei legoliston" + }, + "add_user": { + "button_cancel": "Nuligi", + "button_confirm": "Aldoni", + "dialog_title": "Aldoni Uzanton", + "field_email": "Retpoŝta adreso", + "field_email_error": "Devas esti valida retpoŝta adreso", + "field_password": "Pasvorto", + "field_role_administrator": "Administranto", + "label_roles": "Roloj" + }, + "delete_collection": { + "button_cancel": "Nuligi", + "button_confirm": "Forigi", + "confirm_delete": "Jes, forigu la kolekton \"{name}\"", + "dialog_title": "Forigi Kolekton" + }, + "delete_library": { + "button_cancel": "Nuligi", + "button_confirm": "Forigi", + "confirm_delete": "Jes, forigu la bibliotekon \"{name}\"", + "title": "Forigi Bibliotekon" + }, + "delete_readlist": { + "button_cancel": "Nuligi", + "button_confirm": "Forigi", + "confirm_delete": "Jes, forigu la legoliston \"{name}\"", + "dialog_title": "Forigi Legoliston" + }, + "delete_user": { + "button_cancel": "Nuligi", + "button_confirm": "Forigi", + "confirm_delete": "Jes, forigu la uzanton \"{name}\"", + "dialog_title": "Forigi Uzanton" + }, + "edit_books": { + "button_cancel": "Nuligi", + "button_confirm": "Konservi ŝanĝojn", + "dialog_title_multiple": "Redakti {count} libron | Redakti {count} librojn", + "dialog_title_single": "Redakti libron {book}", + "field_isbn": "ISBN", + "field_isbn_error": "Devas esti valida ISBN-13", + "field_number": "Numero", + "field_number_sort": "Ordiga Numero", + "field_release_date": "Dato de Eldono", + "field_release_date_error": "Devas esti valida dato de la aranĝo JJJJ-MM-TT", + "field_summary": "Resumo", + "field_tags": "Etikedoj", + "field_title": "Titolo", + "tab_authors": "Aŭtoroj", + "tab_general": "Ĝenerala", + "tab_tags": "Etikedoj" + }, + "edit_collection": { + "button_cancel": "Nuligi", + "button_confirm": "Konservi ŝanĝojn", + "dialog_title": "Redakti kolekton", + "field_manual_ordering": "Mana ordigo" + }, + "edit_library": { + "button_browse": "Foliumi", + "button_cancel": "Nuligi", + "button_confirm_add": "Aldoni", + "button_confirm_edit": "Redakti", + "dialog_title_add": "Aldoni Bibliotekon", + "dialot_title_edit": "Redakti Bibliotekon", + "field_import_barcode_isbn": "Strikodo de ISBN", + "field_import_comicinfo_book": "Metadatenoj pri libro", + "field_import_comicinfo_collections": "Kolektoj", + "field_import_comicinfo_readlists": "Legolistoj", + "field_import_comicinfo_series": "Metadatenoj pri serio", + "field_import_epub_book": "Metadatenoj pri libro", + "field_import_epub_series": "Metadatenoj pri serio", + "field_import_local_artwork": "Lokaj bildoj", + "field_name": "Nomo", + "field_root_folder": "Radika dosierujo", + "field_scanner_deep_scan": "Profunde skani", + "file_browser_dialog_button_confirm": "Elekti", + "label_import_barcode_isbn": "Enporti ISBN el strikodo", + "label_scanner": "Skanilo", + "tab_general": "Ĝenerala", + "tab_options": "Opcioj" + }, + "edit_readlist": { + "button_cancel": "Nuligi", + "button_confirm": "Konservi ŝanĝojn", + "dialog_title": "Redakti legoliston", + "field_name": "Nomo" + }, + "edit_series": { + "button_cancel": "Nuligi", + "button_confirm": "Konservi ŝanĝojn", + "dialog_title_multiple": "Redakti {count} serion | Redakti {count} seriojn", + "dialog_title_single": "Redakti serion {series}", + "field_genres": "Ĝenroj", + "field_language": "Lingvo", + "field_publisher": "Eldonejo", + "field_sort_title": "Titolo por Ordigo", + "field_status": "Stato", + "field_summary": "Resumo", + "field_tags": "Etikedoj", + "field_title": "Titolo", + "mixed": "MIKSITA", + "tab_general": "Ĝenerala", + "tab_tags": "Etikedoj" + }, + "edit_user": { + "button_cancel": "Nuligi", + "button_confirm": "Konservi ŝanĝojn", + "dialog_title": "Redakti uzanton", + "label_roles_for": "Roloj de {name}" + }, + "edit_user_shared_libraries": { + "button_cancel": "Nuligi", + "button_confirm": "Konservi ŝanĝojn", + "dialog_title": "Redakti komunajn bibliotekojn", + "field_all_libraries": "Ĉiuj bibliotekoj", + "label_shared_with": "Komuna kun {name}" + }, + "file_browser": { + "button_cancel": "Nuligi", + "button_confirm_default": "Elekti", + "dialog_title_default": "Dosieradministrilo", + "parent_directory": "Patro" + }, + "password_change": { + "button_cancel": "Nuligi", + "button_confirm": "Ŝanĝi pasvorton", + "dialog_title": "Ŝanĝi pasvorton", + "field_new_password": "Nova pasvorto", + "field_repeat_password": "Ripetu novan pasvorton" + }, + "server_stop": { + "button_cancel": "Nuligi" + }, + "shortcut_help": { + "label_description": "Priskribo", + "label_key": "Ŝlosilo" + } + }, + "enums": { + "media_status": { + "ERROR": "Eraro" + }, + "reading_direction": { + "VERTICAL": "Vertikala", + "WEBTOON": "Retkomikso" + } + }, + "filter": { + "age_rating_none": "Nenio", + "genre": "ĝenro", + "language": "lingvo", + "library": "biblioteko", + "publisher": "eldonejo", + "release_date": "dato de eldono", + "status": "stato", + "tag": "etikedo" + }, + "filter_drawer": { + "filter": "filtri", + "sort": "ordigi" + }, + "home": { + "theme": "Etoso", + "translation": "Traduko" + }, + "library_navigation": { + "browse": "Foliumi", + "collections": "Kolektoj", + "readlists": "Legolistoj" + }, + "login": { + "create_user_account": "Krei konton de uzanto", + "login": "Saluti" + }, + "media_analysis": { + "comment": "Komento", + "name": "Nomo", + "size": "Grando", + "status": "Stato", + "url": "Reta adreso" + }, + "menu": { + "add_to_collection": "Aldoni en kolekton", + "add_to_readlist": "Aldoni en legoliston", + "analyze": "Analizi", + "delete": "Forigi", + "download_series": "Elŝuti serion", + "edit": "Redakti", + "edit_metadata": "Redakti metadatenojn", + "mark_read": "Marki kiel legitan", + "mark_unread": "Marki kiel ne legitan", + "refresh_metadata": "Reŝargi metadatenojn", + "scan_library_files": "Skani bibliotekajn dosierojn" + }, + "navigation": { + "home": "Hejmo", + "libraries": "Bibliotekoj", + "logout": "Adiaŭi" + }, + "page_not_found": { + "go_back_to_home_page": "Hejmen", + "page_does_not_exist": "La serĉata paĝo ne ekzistas.", + "page_not_found": "La paĝo ne troviĝis" + }, + "readlists_expansion_panel": { + "manage_readlist": "Mastrumi legoliston", + "title": "Legolisto {name}" + }, + "search": { + "search": "Serĉi", + "search_results_for": "Serĉi \"{name}\" en rezultoj" + }, + "searchbox": { + "no_results": "Neniuj rezultoj", + "search_all": "Serĉi ĉion…" + }, + "server": { + "server_management": { + "section_title": "Administrado de Servilo" + }, + "tab_title": "Servilo" + }, + "server_settings": { + "server_settings": "Agordoj pri Servilo" + }, + "settings_user": { + "edit_shared_libraries": "Redakti komunajn bibliotekojn", + "edit_user": "Redakti uzanton", + "role_administrator": "Administranto", + "role_user": "Uzanto" + }, + "sort": { + "books_count": "Nombro de libroj", + "date_added": "Dato de aldono", + "date_updated": "Dato de ĝisdatigo", + "file_name": "Nomo de dosiero", + "file_size": "Grando de dosiero", + "folder_name": "Nomo de dosierujo", + "name": "Nomo", + "number": "Numero", + "release_date": "Dato de eldono" + }, + "theme": { + "dark": "Malhela", + "light": "Hela", + "system": "Sistema" + }, + "user_roles": { + "ADMIN": "Administranto", + "USER": "Uzanto" + }, + "users": { + "users": "Uzantoj" + }, + "welcome": { + "add_library": "Aldoni bibliotekon", + "welcome_message": "Bonvenon al Komga" + } +} diff --git a/komga-webui/src/locales/es.json b/komga-webui/src/locales/es.json index 65ef77ee0..e650a8fe8 100644 --- a/komga-webui/src/locales/es.json +++ b/komga-webui/src/locales/es.json @@ -14,12 +14,12 @@ "noDataText": "No hay datos disponibles" }, "account_settings": { - "account_settings": "Configuración de Cuenta", + "account_settings": "Configuración de cuenta", "change_password": "cambiar contraseña" }, "author_roles": { "colorist": "coloristas", - "cover": "portada", + "cover": "cubierta", "editor": "editores", "inker": "entintadores", "letterer": "letristas", @@ -40,9 +40,9 @@ "download_current_page": "Descargar página actual", "end_of_book": "Has llegado al final del libro.", "from_series_metadata": "desde metadatos de serie", - "move_next": "Haz click o presiona \"Siguiente\" de nuevo para moverte al siguiente libro.", - "move_next_exit": "Haz click o presiona \"Siguiente\" de nuevo para salir del lector.", - "move_previous": "Haz click o presiona \"Anterior\" de nuevo para moverte al libro anterior.", + "move_next": "Haz clic o presiona «Siguiente» de nuevo para moverte al siguiente libro.", + "move_next_exit": "Haz clic o presiona «Siguiente» de nuevo para salir del lector.", + "move_previous": "Haz clic o presiona «Anterior» de nuevo para moverte al libro anterior.", "paged_reader_layout": { "double": "Doble página", "double_no_cover": "Doble página (sin portada)", @@ -58,7 +58,7 @@ "width": "Ajustar a ancho" }, "settings": { - "animate_page_transitions": "Animar transiciones de página", + "animate_page_transitions": "Animar las transiciones de página", "background_color": "Color de fondo", "background_colors": { "black": "Negro", @@ -70,7 +70,7 @@ "page_layout": "Disposición de página", "paged": "Opciones de Paginado", "reading_mode": "Modo de lectura", - "scale_type": "Escalado", + "scale_type": "Tipo de escalado", "side_padding": "Borde lateral", "side_padding_none": "Ninguno", "webtoon": "Opciones del Lector de Webtoon" @@ -88,7 +88,7 @@ "previous_page": "Página anterior", "reader_navigation": "Navegación en Lector", "right_to_left": "Derecha a Izquierda", - "settings": "Configuración", + "settings": "Ajustes", "show_hide_help": "Mostrar/ocultar ayuda", "show_hide_settings": "Mostrar/ocultar menú de configuración", "show_hide_thumbnails": "Mostrar/ocultar miniaturas", @@ -135,7 +135,7 @@ "create": "Crear", "delete": "Eliminar", "download": "Descargar", - "email": "Email", + "email": "Correo electrónico", "filter_no_matches": "El filtro activo no tiene coincidencias", "genre": "Género", "go_to_library": "Ir a la biblioteca", @@ -195,9 +195,9 @@ "add_user": { "button_cancel": "Cancelar", "button_confirm": "Agregar", - "dialog_title": "Agregar Usuario", - "field_email": "Email", - "field_email_error": "Debe ser una dirección de email válida", + "dialog_title": "Añadir usuario", + "field_email": "Correo electrónico", + "field_email_error": "Debe ser una dirección de correo electrónico válida", "field_password": "Contraseña", "field_role_administrator": "Administrador", "field_role_file_download": "Descarga de archivos", @@ -207,29 +207,29 @@ "delete_collection": { "button_cancel": "Cancelar", "button_confirm": "Eliminar", - "confirm_delete": "Sí, eliminar la colección \"{name}\"", + "confirm_delete": "Sí, eliminar la colección «{name}»", "dialog_title": "Eliminar Colección", "warning_html": "La colección {name} será removida de este servidor. Los archivos no serán eliminados. Esta acción es irreversible ¿Desea continuar?" }, "delete_library": { "button_cancel": "Cancelar", "button_confirm": "Eliminar", - "confirm_delete": "Sí, eliminar la biblioteca \"{name}\"", + "confirm_delete": "Sí, eliminar la biblioteca «{name}»", "title": "Eliminar Biblioteca", "warning_html": "La biblioteca {name} será removida de este servidor. Los archivos no serán eliminados. Esta acción es irreversible ¿Desea continuar?" }, "delete_readlist": { "button_cancel": "Cancelar", "button_confirm": "Eliminar", - "confirm_delete": "Sí, eliminar la lista de lectura \"{name}\"", - "dialog_title": "Eliminar Lista de Lectura", + "confirm_delete": "Sí, eliminar la lista de lectura «{name}»", + "dialog_title": "Eliminar Lista de lectura", "warning_html": "La lista de lectura {name} será removida de este servidor. Los archivos no serán eliminados. Esta acción es irreversible ¿Desea continuar?" }, "delete_user": { "button_cancel": "Cancelar", "button_confirm": "Eliminar", - "confirm_delete": "Sí, eliminar el usuario \"{name}\"", - "dialog_title": "Eliminar Usuario", + "confirm_delete": "Sí, eliminar el usuario «{name}»", + "dialog_title": "Eliminar usuario", "warning_html": "El usuario {name} será removido de este servidor. Esta acción es irreversible ¿Desea continuar?" }, "edit_books": { @@ -276,7 +276,7 @@ "field_import_epub_series": "Metadatos de serie", "field_import_local_artwork": "Portada local", "field_name": "Nombre", - "field_root_folder": "Directorio raíz", + "field_root_folder": "Carpeta raíz", "field_scanner_deep_scan": "Análisis en profundidad", "field_scanner_force_directory_modified_time": "Forzar hora de edición del directorio", "file_browser_dialog_button_confirm": "Elegir", @@ -370,7 +370,7 @@ "WEBTOON": "Webtoon" }, "series_status": { - "ABANDONED": "Abandonada", + "ABANDONED": "Abandonado", "ENDED": "Finalizada", "HIATUS": "En hiato", "ONGOING": "En curso" @@ -392,7 +392,7 @@ "ERR_1012": "No existe coincidencia para la serie", "ERR_1013": "No existe una única coincidencia para el número de libro dentro de la serie", "ERR_1014": "No existe coincidencia para el número de libro dentro de la serie", - "ERR_1015": "Error al deserializar la Lista de Lectura ComicRack" + "ERR_1015": "Error al deserializar la Lista de lectura ComicRack" }, "filter": { "age_rating": "edad mínima", @@ -422,7 +422,7 @@ "login": { "create_user_account": "Crear cuenta de usuario", "login": "Iniciar sesión", - "unclaimed_html": "Este servidor Komga no está activo aún, debes crear una cuenta de usuario para poder acceder a él.

Escoge un email y contraseña, y haz clic en Crear cuenta de usuario." + "unclaimed_html": "Este servidor Komga no está activo aún, debes crear una cuenta de usuario para poder acceder a él.

Escoge un correo eleactrónico y contraseña, y haz clic en Crear cuenta de usuario." }, "media_analysis": { "comment": "Comentario", @@ -468,7 +468,7 @@ "no_results": "La búsqueda no arrojó resultados", "search": "Buscar", "search_for_something_else": "Intenta buscar otra cosa", - "search_results_for": "Resultados de búsqueda para \"{name}\"" + "search_results_for": "Resultados de búsqueda para «{name}»" }, "searchbox": { "no_results": "No hay resultados", diff --git a/komga-webui/src/locales/fi.json b/komga-webui/src/locales/fi.json index 3e0de47a5..020b71669 100644 --- a/komga-webui/src/locales/fi.json +++ b/komga-webui/src/locales/fi.json @@ -40,9 +40,9 @@ "download_current_page": "Lataa tämänhetkinen sivu", "end_of_book": "Olet saavuttanut kirjan lopun.", "from_series_metadata": "sarjan metatiedoista", - "move_next": "Siirry seuraavaan kirjaan napsauttamalla tai painamalla \"Seuraava\" uudelleen.", - "move_next_exit": "Poistu lukijasta napsauttamalla tai painamalla \"Seuraava\" uudelleen.", - "move_previous": "Siirry edelliseen kirjaan napsauttamalla tai painamalla \"Edellinen\" uudelleen.", + "move_next": "Siirry seuraavaan kirjaan napsauttamalla tai painamalla ”Seuraava” uudelleen.", + "move_next_exit": "Poistu lukijasta napsauttamalla tai painamalla ”Seuraava” uudelleen.", + "move_previous": "Siirry edelliseen kirjaan napsauttamalla tai painamalla ”Edellinen” uudelleen.", "paged_reader_layout": { "double": "Kaksoissivut", "double_no_cover": "Kaksoissivut (ei kantta)", @@ -207,28 +207,28 @@ "delete_collection": { "button_cancel": "Peruuta", "button_confirm": "Poista", - "confirm_delete": "Kyllä, poista kokoelma \"{name}\"", + "confirm_delete": "Kyllä, poista kokoelma ”{name}”", "dialog_title": "Poista kokoelma", "warning_html": "Kokoelma {name} poistetaan. Tämä ei vaikuta mediatiedostoihisi. Tätä ei voi kumota. Jatka?" }, "delete_library": { "button_cancel": "Peruuta", "button_confirm": "Poista", - "confirm_delete": "Kyllä, poista kirjasto \"{name}\"", + "confirm_delete": "Kyllä, poista kirjasto ”{name}”", "title": "Poista kirjasto", "warning_html": "Kirjasto {name} poistetaan palvelimelta. Tämä ei vaikuta mediatiedostoihisi. Tätä ei voi kumota. Jatka?" }, "delete_readlist": { "button_cancel": "Peruuta", "button_confirm": "Poista", - "confirm_delete": "Kyllä, poista lukulista \"{name}\"", + "confirm_delete": "Kyllä, poista lukulista ”{name}”", "dialog_title": "Poista lukulista", "warning_html": "Lukulista {name} poistetaan palvelimelta. Tämä ei vaikuta mediatiedostoihisi. Tätä ei voi kumota. Jatka?" }, "delete_user": { "button_cancel": "Peruuta", "button_confirm": "Poista", - "confirm_delete": "Kyllä, poista käyttäjä \"{name}\"", + "confirm_delete": "Kyllä, poista käyttäjä ”{name}”", "dialog_title": "Poista käyttäjä", "warning_html": "Käyttäjä {name} poistetaan palvelimelta. Tätä ei voi kumota. Jatka?" }, @@ -468,7 +468,7 @@ "no_results": "Haku ei tuottanut yhtään tulosta", "search": "Etsi", "search_for_something_else": "Yritä etsiä jotain muuta", - "search_results_for": "Tulokset haulle \"{name}\"" + "search_results_for": "Tulokset haulle ”{name}”" }, "searchbox": { "no_results": "Ei tuloksia", diff --git a/komga-webui/src/locales/fr.json b/komga-webui/src/locales/fr.json index b71513e5c..9a22689c7 100644 --- a/komga-webui/src/locales/fr.json +++ b/komga-webui/src/locales/fr.json @@ -40,9 +40,9 @@ "download_current_page": "Télécharger la page courante", "end_of_book": "Vous avez atteint la fin du livre.", "from_series_metadata": "à partir des métadonnées de la série", - "move_next": "Cliquez ou appuyez à nouveau sur \"Suivant\" pour passer au livre suivant.", - "move_next_exit": "Cliquez ou appuyez à nouveau sur \"Suivant\" pour quitter la liseuse.", - "move_previous": "Cliquez ou appuyez à nouveau sur \"Précédent\" pour passer au livre précédent.", + "move_next": "Cliquez ou appuyez à nouveau sur « Suivant » pour passer au livre suivant.", + "move_next_exit": "Cliquez ou appuyez à nouveau sur « Suivant » pour quitter la liseuse.", + "move_previous": "Cliquez ou appuyez à nouveau sur « Précédent » pour passer au livre précédent.", "paged_reader_layout": { "double": "Double pages", "double_no_cover": "Double pages (sans couverture)", @@ -197,7 +197,7 @@ "button_confirm": "Ajouter", "dialog_title": "Ajouter un utilisateur", "field_email": "E-mail", - "field_email_error": "Doit être une adresse e-mail valide", + "field_email_error": "Doit être une adresse électronique valide", "field_password": "Mot de passe", "field_role_administrator": "Administrateur", "field_role_file_download": "Téléchargement de fichier", @@ -244,7 +244,7 @@ "field_number_sort": "Numéro pour le tri", "field_number_sort_hint": "Vous pouvez utiliser des nombres décimaux", "field_release_date": "Date de publication", - "field_release_date_error": "Doit être une date au format YYYY-MM-DD", + "field_release_date_error": "Doit être une date au format AAAA-MM-JJ", "field_summary": "Résumé", "field_tags": "Étiquettes", "field_title": "Titre", @@ -347,7 +347,7 @@ "server_stop": { "button_cancel": "Annuler", "button_confirm": "Arrêter", - "confirmation_message": "Êtes-vous sûr de vouloir arrêter Komga ?", + "confirmation_message": "Êtes-vous sûr de vouloir arrêter Komga ?", "dialog_title": "Arrêter le serveur" }, "shortcut_help": { diff --git a/komga-webui/src/locales/it.json b/komga-webui/src/locales/it.json index 16ab71ec5..ff950e0a6 100644 --- a/komga-webui/src/locales/it.json +++ b/komga-webui/src/locales/it.json @@ -11,10 +11,10 @@ "counter": "{0} file", "counterSize": "{0} files ({1} in totale)" }, - "noDataText": "Nessun elemento disponibile" + "noDataText": "Nessun dato disponibile" }, "account_settings": { - "account_settings": "Impostazioni Account", + "account_settings": "Impostazioni profilo", "change_password": "cambia password" }, "author_roles": { @@ -35,14 +35,14 @@ "beginning_of_book": "Sei all'inizio del libro.", "changing_reading_direction": "Cambia la direzione di lettura in", "cycling_page_layout": "Cambia Layout Pagina", - "cycling_scale": "Cambia Scala", + "cycling_scale": "Cambia scala", "cycling_side_padding": "Cambia Distanziamento Laterale", "download_current_page": "Scarica pagina corrente", "end_of_book": "Hai raggiunto la fine del libro.", "from_series_metadata": "dai metadati della serie", - "move_next": "Clicca o premi nuovamente \"Avanti\" per passare al libro successivo.", - "move_next_exit": "Clicca o premi nuovamente \"Avanti\" per uscire dal lettore.", - "move_previous": "Clicca o premi nuovamente \"Indietro\" per passare al libro precedente.", + "move_next": "Clicca o premi nuovamente «Avanti» per passare al libro successivo.", + "move_next_exit": "Clicca o premi nuovamente «Avanti» per uscire dal lettore.", + "move_previous": "Clicca o premi nuovamente «Indietro» per passare al libro precedente.", "paged_reader_layout": { "double": "Pagine doppie", "double_no_cover": "Pagine doppie (senza copertina)", @@ -64,8 +64,8 @@ "black": "Nero", "white": "Bianco" }, - "display": "Schermo", - "general": "Generali", + "display": "Visualizzazione", + "general": "Generale", "gestures": "Gesti", "page_layout": "Formato pagina", "paged": "Opzioni Impaginazione", @@ -88,7 +88,7 @@ "previous_page": "Pagina precedente", "reader_navigation": "Navigazione Lettore", "right_to_left": "Da destra a sinistra", - "settings": "impostazioni", + "settings": "Impostazioni", "show_hide_help": "Mostra/nascondi aiuto", "show_hide_settings": "Mostra/nascondi le impostazioni", "show_hide_thumbnails": "Mostra/nascondi miniature", @@ -127,15 +127,15 @@ }, "common": { "all_libraries": "Tutte le librerie", - "books": "Libri", - "books_n": "Nessun libro | 1 libro | {count} libri", + "books": "Fumetti", + "books_n": "Nessun fumetto | 1 fumetto | {count} fumetti", "cancel": "Annulla", "close": "Chiudi", "collections": "Raccolte", "create": "Crea", "delete": "Elimina", "download": "Scarica", - "email": "Email", + "email": "E-mail", "filter_no_matches": "Il filtro attivo non ha corrispondenze", "genre": "Genere", "go_to_library": "Vai alla libreria", @@ -159,7 +159,7 @@ "dashboard": { "keep_reading": "Continua a leggere", "on_deck": "Primo Piano", - "recently_added_books": "Libri aggiunti di recente", + "recently_added_books": "Fumetti aggiunti di recente", "recently_added_series": "Serie aggiunte di recente", "recently_updated_series": "Serie aggiornate di recente" }, @@ -167,11 +167,11 @@ "book_number": "Libro numero: {name}", "book_series": "Serie: {name}", "button_import": "Importa", - "comicrack_preambule_html": "Puoi importare liste di lettura ComicRack esistenti in formato .cbl.
Komga cercherà di far corrispondere la serie e il numero di libri forniti con le serie e i libri nelle tue librerie.", + "comicrack_preambule_html": "Puoi importare liste di lettura ComicRack esistenti in formato .cbl.
Komga cercherà di far corrispondere la serie e il numero di fumetti forniti con le serie e i fumetti nelle tue librerie.", "field_files_label": "Liste di lettura ComicRack (.cbl)", "import_read_lists": "Importa Liste Lettura", "imported_as": "Importato come {name}", - "results_preambule": "Il risultato dell'importazione è mostrato qui sotto. Puoi anche controllare i libri senza corrispondenza per ogni file fornito.", + "results_preambule": "Il risultato dell'importazione è mostrato qui sotto. Puoi anche controllare i fumetti senza corrispondenza per ogni file fornito.", "size_limit": "Deve essere meno di {size} MB", "tab_title": "Importa Dati" }, @@ -186,7 +186,7 @@ }, "add_to_readlist": { "button_create": "Crea", - "card_readlist_subtitle": "Nessun libro | 1 libro | {count} libri", + "card_readlist_subtitle": "Nessun fumetto | 1 fumetto | {count} fumetti", "dialog_title": "Aggiungi all'elenco di lettura", "field_search_create": "Cerca o crea una lista di lettura", "field_search_create_error": "Una lista di lettura con questo nome esiste già", @@ -195,63 +195,63 @@ "add_user": { "button_cancel": "Annulla", "button_confirm": "Aggiungi", - "dialog_title": "Aggiungi Utente", - "field_email": "Email", + "dialog_title": "Aggiungi un utente", + "field_email": "E-mail", "field_email_error": "Deve essere un indirizzo e-mail valido", "field_password": "Password", "field_role_administrator": "Amministratore", - "field_role_file_download": "Scarica File", + "field_role_file_download": "Scaricamento file", "field_role_page_streaming": "Lettura online", "label_roles": "Ruoli" }, "delete_collection": { "button_cancel": "Annulla", "button_confirm": "Elimina", - "confirm_delete": "Sì, elimina la raccolta \"{name}\"", + "confirm_delete": "Sì, elimina la raccolta «{name}»", "dialog_title": "Elimina Raccolta", "warning_html": "La raccolta {name} sarà rimossa da questo server. I tuoi file non saranno interessati. Questo non può essere annullato. Continuare?" }, "delete_library": { "button_cancel": "Annulla", "button_confirm": "Elimina", - "confirm_delete": "Sì, elimina la libreria \"{name}\"", + "confirm_delete": "Sì, elimina la libreria «{name}»", "title": "Elimina Libreria", "warning_html": "La libreria {name} sarà rimossa da questo server. I tuoi file multimediali non saranno interessati. Questo non può essere annullato. Continuare?" }, "delete_readlist": { "button_cancel": "Annulla", "button_confirm": "Elimina", - "confirm_delete": "Sì, cancella la lista di lettura \"{name}\"", + "confirm_delete": "Sì, cancella la lista di lettura «{name}»", "dialog_title": "Elimina Elenco Lettura", "warning_html": "La lista di lettura {name} sarà rimossa da questo server. I tuoi file multimediali non saranno interessati. Questo non può essere annullato. Continuare?" }, "delete_user": { "button_cancel": "Annulla", "button_confirm": "Elimina", - "confirm_delete": "Sì, elimina l'utente \"{name}\"", + "confirm_delete": "Sì, elimina l'utente «{name}»", "dialog_title": "Elimina Utente", "warning_html": "L'utente {name} sarà cancellato da questo server. Questo non può essere annullato. Continuare?" }, "edit_books": { - "authors_notice_multiple_edit": "Stai modificando gli autori per più libri. Questo sovrascriverà gli autori esistenti di ogni libro.", + "authors_notice_multiple_edit": "Stai modificando gli autori per più fumetti. Questo sovrascriverà gli autori esistenti di ogni fumetto.", "button_cancel": "Annulla", "button_confirm": "Salva modifiche", - "dialog_title_multiple": "Modifica {count} libro | Modifica {count} libri", + "dialog_title_multiple": "Modifica {count} fumetto | Modifica {count} fumetti", "dialog_title_single": "Modifica {book}", "field_isbn": "ISBN", "field_isbn_error": "Deve essere un ISBN 13 valido", "field_number": "Numero", "field_number_sort": "Numero di ordinamento", "field_number_sort_hint": "Puoi usare numeri decimali", - "field_release_date": "Data di Uscita", + "field_release_date": "Data di uscita", "field_release_date_error": "Deve essere una data valida nel formato AAAA-MM-GG", "field_summary": "Riassunto", - "field_tags": "Tag", + "field_tags": "Etichette", "field_title": "Titolo", "tab_authors": "Autori", "tab_general": "Generale", - "tab_tags": "Tag", - "tags_notice_multiple_edit": "Stai modificando i tag per più libri. Questo sovrascriverà i tag esistenti di ogni libro." + "tab_tags": "Etichette", + "tags_notice_multiple_edit": "Stai modificando le etichette per più fumetti. Questo sovrascriverà le etichette esistenti di ogni fumetto." }, "edit_collection": { "button_cancel": "Annulla", @@ -291,13 +291,13 @@ }, "edit_readlist": { "button_cancel": "Annulla", - "button_confirm": "Salva modifice", + "button_confirm": "Salva modifiche", "dialog_title": "Modifica elenco di lettura", "field_name": "Nome" }, "edit_series": { "button_cancel": "Annulla", - "button_confirm": "Salva modifice", + "button_confirm": "Salva modifiche", "dialog_title_multiple": "Modifica {count} serie | Modifica {count} serie", "dialog_title_single": "Modifica {series}", "field_age_rating": "Fascia d'età", @@ -309,12 +309,12 @@ "field_sort_title": "Titolo di Ordinamento", "field_status": "Stato", "field_summary": "Riassunto", - "field_tags": "Tag", + "field_tags": "Etichette", "field_title": "Titolo", "mixed": "MISTO", "tab_general": "Generale", - "tab_tags": "Tag", - "tags_notice_multiple_edit": "Stai modificando i tag per più serie. Questo sovrascriverà i tag esistenti di ogni serie." + "tab_tags": "Etichette", + "tags_notice_multiple_edit": "Stai modificando le etichette per più serie. Questo sovrascriverà le etichette esistenti di ogni serie." }, "edit_user": { "button_cancel": "Annulla", @@ -332,8 +332,8 @@ "file_browser": { "button_cancel": "Annulla", "button_confirm_default": "Scegli", - "dialog_title_default": "Browser di File", - "parent_directory": "Cartella Superiore" + "dialog_title_default": "Browser di file", + "parent_directory": "Genitore" }, "password_change": { "button_cancel": "Annulla", @@ -347,7 +347,7 @@ "server_stop": { "button_cancel": "Annulla", "button_confirm": "Spegni", - "confirmation_message": "Sei sicuro di voler spegnere Komga?", + "confirmation_message": "Sei sicuro/a di voler spegnere Komga?", "dialog_title": "Spegni il server" }, "shortcut_help": { @@ -371,7 +371,7 @@ }, "series_status": { "ABANDONED": "Abbandonato", - "ENDED": "Conclusa", + "ENDED": "Concluso", "HIATUS": "Hiatus", "ONGOING": "In corso" } @@ -401,10 +401,10 @@ "language": "lingua", "library": "libreria", "publisher": "editore", - "release_date": "data di rilascio", + "release_date": "data di pubblicazione", "status": "stato", - "tag": "tag", - "unread": "Non lette" + "tag": "etichetta", + "unread": "Non letti" }, "filter_drawer": { "filter": "filtro", @@ -422,7 +422,7 @@ "login": { "create_user_account": "Crea account utente", "login": "Entra", - "unclaimed_html": "Questo server Komga non è ancora attivo, devi creare un account utente per potervi accedere.

Scegli una email e una password e clicca su Crea account utente." + "unclaimed_html": "Questo server Komga non è ancora attivo, devi creare un account utente per potervi accedere.

Scegli un'e-mail e una password e clicca su Crea account utente." }, "media_analysis": { "comment": "Commento", @@ -468,7 +468,7 @@ "no_results": "La ricerca non ha dato risultati", "search": "Cerca", "search_for_something_else": "Prova a cercare qualcos'altro", - "search_results_for": "Risultati della ricerca per \"{name}\"" + "search_results_for": "Risultati della ricerca per «{name}»" }, "searchbox": { "no_results": "Nessun risultato", @@ -485,7 +485,7 @@ "server_settings": "Impostazioni Server" }, "settings_user": { - "edit_shared_libraries": "Modifica le librerie condivise", + "edit_shared_libraries": "Modifica librerie condivise", "edit_user": "Modifica utente", "role_administrator": "Amministratore", "role_user": "Utente" @@ -508,7 +508,7 @@ }, "user_roles": { "ADMIN": "Amministratore", - "FILE_DOWNLOAD": "Scarica File", + "FILE_DOWNLOAD": "Scaricamento file", "PAGE_STREAMING": "Leggi online", "USER": "Utente" }, @@ -518,6 +518,6 @@ "welcome": { "add_library": "Aggiungi libreria", "no_libraries_yet": "Non sono ancora state aggiunte librerie!", - "welcome_message": "Benvenuto su Komga" + "welcome_message": "Benvenuto/a su Komga" } } diff --git a/komga-webui/src/locales/pl.json b/komga-webui/src/locales/pl.json new file mode 100644 index 000000000..b68d226b4 --- /dev/null +++ b/komga-webui/src/locales/pl.json @@ -0,0 +1,523 @@ +{ + "$vuetify": { + "dataFooter": { + "pageText": "{0}-{1} z {2}" + }, + "dataTable": { + "itemsPerPageText": "Rekordów na stronę:", + "sortBy": "Sortowanie po" + }, + "fileInput": { + "counter": "{0} plików", + "counterSize": "{0} plików ({1} wszystkich)" + }, + "noDataText": "Brak dostępnych danych" + }, + "account_settings": { + "account_settings": "Ustawienia konta", + "change_password": "zmiana hasła" + }, + "author_roles": { + "colorist": "koloryści", + "cover": "okładka", + "editor": "edytorzy", + "inker": "inkerzy", + "letterer": "liternicy", + "penciller": "rysownicy", + "writer": "scenarzyści" + }, + "book_card": { + "error": "Błąd", + "unknown": "Do analizy", + "unsupported": "Nieobsługiwany" + }, + "bookreader": { + "beginning_of_book": "Jesteś na początku książki.", + "changing_reading_direction": "Zmiana kierunku czytania", + "cycling_page_layout": "Zmiana układu strony", + "cycling_scale": "Zmiana powiększenia", + "cycling_side_padding": "Zmiana wypełnienia bocznego", + "download_current_page": "Pobierz aktualną stronę", + "end_of_book": "Osiągnięto koniec książki.", + "from_series_metadata": "z metadanych serii", + "move_next": "Kliknij lub naciśnij \"Dalej\" ponownie aby przejść do następnej książki.", + "move_next_exit": "Kliknij lub naciśnij \"Dalej\" ponownie aby zamknąć czytnik.", + "move_previous": "Kliknij lub naciśnij \"Wstecz\" ponownie aby przejść do poprzedniej książki.", + "paged_reader_layout": { + "double": "Podwójne strony", + "double_no_cover": "Podwójne strony (bez okładki)", + "single": "Pojedyncza strona" + }, + "reader_settings": "Ustawienia czytnika", + "scale_type": { + "continuous_original": "Bez zmian", + "continuous_width": "Dopasuj do szerokości", + "height": "Dopasuj do wysokości", + "original": "Bez zmian", + "screen": "Ekran", + "width": "Dopasuj do szerokości" + }, + "settings": { + "animate_page_transitions": "Animowane przejścia stron", + "background_color": "Kolor tła", + "background_colors": { + "black": "czarny", + "white": "biały" + }, + "display": "Wyświetlanie", + "general": "Ogólne", + "gestures": "Gesty", + "page_layout": "Układ strony", + "paged": "Konfiguracja stron w czytniku", + "reading_mode": "Tryb czytania", + "scale_type": "Tryb skalowania", + "side_padding": "Wypełnienie boczne", + "side_padding_none": "Brak", + "webtoon": "Konfiguracja czytnika Webtoon" + }, + "shortcuts": { + "close": "Zamknij", + "cycle_page_layout": "Zmień układ strony", + "cycle_scale": "Zmień skalowanie", + "cycle_side_padding": "Zmień wypełnienie boczne", + "first_page": "Pierwsza strona", + "last_page": "Ostatnia strona", + "left_to_right": "Od lewej do prawej", + "menus": "Menu", + "next_page": "Następna strona", + "previous_page": "Poprzednia strona", + "reader_navigation": "Nawigacja w czytniku", + "right_to_left": "Od prawej do lewej", + "settings": "Ustawienia", + "show_hide_help": "Pokaż/ukryj pomoc", + "show_hide_settings": "Pokaż/ukryj menu ustawień", + "show_hide_thumbnails": "Pokaż/ukryj przeglądarkę miniaturek", + "show_hide_toolbars": "Pokaż/ukryj paski narzędzi", + "vertical": "Pionowy", + "webtoon": "Web-komiks" + } + }, + "browse_book": { + "comment": "KOMENTARZ", + "download_file": "Pobierz plik", + "file": "PLIK", + "format": "FORMAT", + "isbn": "ISBN", + "navigation_within_readlist": "Nawigacja w ramach listy: {name}", + "read_book": "Czytaj", + "size": "ROZMIAR" + }, + "browse_collection": { + "edit_collection": "Edycja kolekcji", + "edit_elements": "Edycja elementów", + "manual_ordering": "sortowanie manualne" + }, + "browse_readlist": { + "edit_elements": "Edycja elementów", + "edit_readlist": "Edytuj listę" + }, + "browse_series": { + "earliest_year_from_release_dates": "To jest najwcześniejszy rok z dat publikacji wszystkich książek w serii", + "series_no_summary": "Ta seria nie ma podsumowania, dlatego uzupełniliśmy je dla Ciebie!", + "summary_from_book": "Podsumowanie z książki {number}:" + }, + "collections_expansion_panel": { + "manage_collection": "Zarządzaj kolekcją", + "title": "kolekcja {name}" + }, + "common": { + "all_libraries": "Wszystkie biblioteki", + "books": "Książki", + "books_n": "Brak książek | 1 książka | {count} książki", + "cancel": "Anuluj", + "close": "Zamknij", + "collections": "Kolekcje", + "create": "Utwórz", + "delete": "Usuń", + "download": "Pobierz", + "email": "Adres e-mail", + "filter_no_matches": "Aktywny filtr nie zwrócił żadnych wyników", + "genre": "Gatunek", + "go_to_library": "Przejdź do biblioteki", + "locale_name": "polski", + "locale_rtl": "false", + "n_selected": "wybrano {count}", + "nothing_to_show": "Nic do wyświetlenia", + "pages": "strony", + "pages_n": "Brak stron | 1 strona | {count} stron", + "password": "Hasło", + "publisher": "Wydawca", + "read": "Czytaj", + "readlists": "Listy", + "required": "Wymagane", + "roles": "Role", + "series": "Serie", + "tags": "Tagi", + "use_filter_panel_to_change_filter": "Użyj panelu filtrów aby zmienić aktywny filtr", + "year": "rok" + }, + "dashboard": { + "keep_reading": "Kontynuuj czytanie", + "on_deck": "Na stosie", + "recently_added_books": "Ostatnio dodane książki", + "recently_added_series": "Ostatnio dodane cykle", + "recently_updated_series": "Ostatnio aktualizowane cykle" + }, + "data_import": { + "book_number": "Numer książki: {name}", + "book_series": "Seria: {name}", + "button_import": "Importuj", + "comicrack_preambule_html": "Importowanie istniejących list z programu ComicRack w formacie .cbl.
Komga spróbuje dopasować cykle i tomy do cykli i tomów znajdujących się w bibliotekach.", + "field_files_label": "Listy programu ComicRack", + "import_read_lists": "Zaimportuj listy", + "imported_as": "Zaimportowano jako {name}", + "results_preambule": "Wynik importu wyświetlono poniżej. Wyświetlono także niedopasowane książki dla każdego przetworzonego pliku.", + "size_limit": "Plik powinien być mniejszy niż {size} MB", + "tab_title": "Import danych" + }, + "dialog": { + "add_to_collection": { + "button_create": "Utwórz", + "card_collection_subtitle": "Brak serii | 1 seria | {count} serie", + "dialog_title": "Dodaj do kolekcji", + "field_search_create": "Szukaj lub utwórz kolekcję", + "field_search_create_error": "Kolekcja z taką nazwą już istnieje", + "label_no_matching_collection": "Brak pasujących kolekcji" + }, + "add_to_readlist": { + "button_create": "Utwórz", + "card_readlist_subtitle": "Brak tomów | 1 tom | {count} tomów", + "dialog_title": "Dodaj do listy", + "field_search_create": "Szukaj lub utwórz listę", + "field_search_create_error": "Lista z taką nazwą już istnieje", + "label_no_matching_readlist": "Brak pasujących list" + }, + "add_user": { + "button_cancel": "Anuluj", + "button_confirm": "Dodaj", + "dialog_title": "Dodaj użytkownika", + "field_email": "E-mail", + "field_email_error": "Adres e-mail musi być poprawny", + "field_password": "Hasło", + "field_role_administrator": "Administrator", + "field_role_file_download": "Pobieranie pliku", + "field_role_page_streaming": "Strumieniowanie stron", + "label_roles": "Role" + }, + "delete_collection": { + "button_cancel": "Anuluj", + "button_confirm": "Usuń", + "confirm_delete": "Tak, usuń kolekcję „{name}”", + "dialog_title": "Usuń kolekcję", + "warning_html": "Kolekcja {name} zostanie usunięta z tego serwera. Nie będzie to miało wpływu na Twoje pliki. Ta czynność nie może być cofnięta. Kontynuować?" + }, + "delete_library": { + "button_cancel": "Anuluj", + "button_confirm": "Usuń", + "confirm_delete": "Tak, usuń bibliotekę \"{name}\"", + "title": "Usuń bibliotekę", + "warning_html": "Biblioteka {name} zostanie usunięta z tego serwera. Nie będzie to miało wpływu na Twoje pliki. Ta czynność nie może być cofnięta. Kontynuować?" + }, + "delete_readlist": { + "button_cancel": "Anuluj", + "button_confirm": "Usuń", + "confirm_delete": "Tak, usuń listę \"{name}\"", + "dialog_title": "Usuwanie listy", + "warning_html": "Lista {name} zostanie usunięta z tego serwera. Nie będzie to miało wpływu na Twoje pliki. Ta czynność nie może być cofnięta. Kontynuować?" + }, + "delete_user": { + "button_cancel": "Anuluj", + "button_confirm": "Usuń", + "confirm_delete": "Tak, usuń użytkownika \"{name}\"", + "dialog_title": "Usuń użytkownika", + "warning_html": "Użytkownik {name} zostanie usunięty z tego serwera. Ta czynność nie może zostać cofnięta. Kontynuować?" + }, + "edit_books": { + "authors_notice_multiple_edit": "Edytujesz autorów dla wielu książek. Spowoduje to zastąpienie istniejących autorów każdej książki.", + "button_cancel": "Anuluj", + "button_confirm": "Zapisz zmiany", + "dialog_title_multiple": "Edycja książki | Edycja {count} książek", + "dialog_title_single": "Edycja {book}", + "field_isbn": "ISBN", + "field_isbn_error": "Musi być prawidłowym numerem ISBN 13", + "field_number": "Numer", + "field_number_sort": "Numer sortowania", + "field_number_sort_hint": "Można użyć liczb dziesiętnych", + "field_release_date": "Data wydania", + "field_release_date_error": "Musi być prawidłową datą w formacie RRRR-MM-DD", + "field_summary": "Streszczenie", + "field_tags": "Tagi", + "field_title": "Tytuł", + "tab_authors": "Autorzy", + "tab_general": "Ogólne", + "tab_tags": "Tagi", + "tags_notice_multiple_edit": "Edytujesz tagi dla wielu książek. Spowoduje to zastąpienie istniejących tagów każdej książki." + }, + "edit_collection": { + "button_cancel": "Anuluj", + "button_confirm": "Zapisz zmiany", + "dialog_title": "Edycja kolekcji", + "field_manual_ordering": "Ręczne sortowanie", + "label_ordering": "Domyślnie cykle w kolekcji będą uporządkowane według nazwy. Można włączyć ręczne porządkowanie, aby zdefiniować własną kolejność." + }, + "edit_library": { + "button_browse": "Przeglądaj", + "button_cancel": "Anuluj", + "button_confirm_add": "Dodaj", + "button_confirm_edit": "Edytuj", + "dialog_title_add": "Dodawanie biblioteki", + "dialot_title_edit": "Edycja biblioteki", + "field_import_barcode_isbn": "Kod kreskowy ISBN", + "field_import_comicinfo_book": "Metadane książki", + "field_import_comicinfo_collections": "Kolekcje", + "field_import_comicinfo_readlists": "Listy", + "field_import_comicinfo_series": "Metadane cykli", + "field_import_epub_book": "Metadane książki", + "field_import_epub_series": "Metadane cykli", + "field_import_local_artwork": "Grafika lokalna", + "field_name": "Nazwa", + "field_root_folder": "Folder główny", + "field_scanner_deep_scan": "Głębokie skanowanie", + "field_scanner_force_directory_modified_time": "Wymuś czas modyfikacji katalogu", + "file_browser_dialog_button_confirm": "Wybierz", + "file_browser_dialog_title": "Folder główny biblioteki", + "label_import_barcode_isbn": "Importowanie ISBN z kodu kreskowego", + "label_import_comicinfo": "Importowanie metadanych dla CBR/CBZ zawierających plik ComicInfo.xml", + "label_import_epub": "Importowanie metadanych z plików EPUB", + "label_import_local": "Importowanie lokalnych zasobów", + "label_scanner": "Skaner", + "tab_general": "Ogólne", + "tab_options": "Opcje" + }, + "edit_readlist": { + "button_cancel": "Anuluj", + "button_confirm": "Zapisz zmiany", + "dialog_title": "Edycja listy", + "field_name": "Nazwa" + }, + "edit_series": { + "button_cancel": "Anuluj", + "button_confirm": "Zapisz zmiany", + "dialog_title_multiple": "Edycja cyklu | Edycja {count} cykli", + "dialog_title_single": "Edycja {series}", + "field_age_rating": "Klasyfikacja wiekowa", + "field_age_rating_error": "Klasyfikacja wiekowa musi wynosić 0 lub więcej", + "field_genres": "Gatunki", + "field_language": "Język", + "field_publisher": "Wydawca", + "field_reading_direction": "Kierunek czytania", + "field_sort_title": "Tytuł sortowania", + "field_status": "Status", + "field_summary": "Streszczenie", + "field_tags": "Tagi", + "field_title": "Tytuł", + "mixed": "MIESZANY", + "tab_general": "Ogólne", + "tab_tags": "Tagi", + "tags_notice_multiple_edit": "Edytujesz tagi dla wielu cykli. Spowoduje to zastąpienie istniejących tagów w każdym cyklu." + }, + "edit_user": { + "button_cancel": "Anuluj", + "button_confirm": "Zapisz zmiany", + "dialog_title": "Edycja użytkownika", + "label_roles_for": "Role dla {name}" + }, + "edit_user_shared_libraries": { + "button_cancel": "Anuluj", + "button_confirm": "Zapisz zmiany", + "dialog_title": "Edycja bibliotek udostępnionych", + "field_all_libraries": "Wszystkie biblioteki", + "label_shared_with": "Udostępnione {name}" + }, + "file_browser": { + "button_cancel": "Anuluj", + "button_confirm_default": "Wybierz", + "dialog_title_default": "Przeglądarka plików", + "parent_directory": "Nadrzędny" + }, + "password_change": { + "button_cancel": "Anuluj", + "button_confirm": "Zmień hasło", + "dialog_title": "Zmiana hasła", + "field_new_password": "Nowe hasło", + "field_new_password_error": "Wymagane jest nowe hasło.", + "field_repeat_password": "Powtórzenie nowego hasła", + "field_repeat_password_error": "Hasła muszą być identyczne." + }, + "server_stop": { + "button_cancel": "Anuluj", + "button_confirm": "Wyłącz", + "confirmation_message": "Jesteś pewien, że chcesz wyłączyć Komgę?", + "dialog_title": "Wyłączenie serwera" + }, + "shortcut_help": { + "label_description": "Opis", + "label_key": "Klucz" + } + }, + "enums": { + "media_status": { + "ERROR": "Błąd", + "OUTDATED": "Nieaktualny", + "READY": "Gotowy", + "UNKNOWN": "Nieznany", + "UNSUPPORTED": "Nieobsługiwany" + }, + "reading_direction": { + "LEFT_TO_RIGHT": "Od lewej do prawej", + "RIGHT_TO_LEFT": "Od prawej do lewej", + "VERTICAL": "Pionowy", + "WEBTOON": "Web-komiks" + }, + "series_status": { + "ABANDONED": "Porzucony", + "ENDED": "Zakończony", + "HIATUS": "Zawieszony", + "ONGOING": "Trwający" + } + }, + "error_codes": { + "ERR_1000": "Nie można uzyskać dostępu do pliku podczas analizy", + "ERR_1001": "Typ mediów nie jest obsługiwany", + "ERR_1002": "Zaszyfrowane archiwa RAR nie są obsługiwane", + "ERR_1003": "Archiwa Solid RAR nie są obsługiwane", + "ERR_1004": "Archiwa RAR o wielu woluminach nie są obsługiwane", + "ERR_1005": "Nieznany błąd podczas analizowania książki", + "ERR_1006": "Książka nie zawiera żadnej strony", + "ERR_1007": "Niektóre wpisy nie mogły zostać przeanalizowane", + "ERR_1008": "Nieznany błąd podczas pobierania pozycji książki", + "ERR_1009": "Lista o tej nazwie już istnieje", + "ERR_1010": "Żadne książki nie zostały dopasowane w żądaniu listy", + "ERR_1011": "Brak unikalnego dopasowania dla cyklu", + "ERR_1012": "Brak dopasowania dla cykli", + "ERR_1013": "Brak unikalnego dopasowania dla numeru tomu w cyklu", + "ERR_1014": "Brak dopasowania dla numeru tomu w cyklu", + "ERR_1015": "Błąd podczas deserializacji listy ComicRack" + }, + "filter": { + "age_rating": "Klasyfikacja wiekowa", + "age_rating_none": "Brak", + "genre": "gatunek", + "language": "język", + "library": "biblioteka", + "publisher": "wydawca", + "release_date": "data wydania", + "status": "status", + "tag": "tag", + "unread": "Nieprzeczytane" + }, + "filter_drawer": { + "filter": "filtr", + "sort": "sortowanie" + }, + "home": { + "theme": "Motyw", + "translation": "Tłumaczenie" + }, + "library_navigation": { + "browse": "Przeglądaj", + "collections": "Kolekcje", + "readlists": "Listy" + }, + "login": { + "create_user_account": "Tworzenie konta użytkownika", + "login": "Zaloguj", + "unclaimed_html": "Ten serwer Komga nie jest jeszcze aktywny, musisz utworzyć konto użytkownika, by móc uzyskać do niego dostęp.

Wybierz e-mail oraz hasło i kliknij na Utwórz konto użytkownika." + }, + "media_analysis": { + "comment": "Komentarz", + "media_analysis": "Analiza mediów", + "media_type": "Typ mediów", + "name": "Nazwa", + "size": "Rozmiar", + "status": "Status", + "url": "Adres URL" + }, + "menu": { + "add_to_collection": "Dodaj do kolekcji", + "add_to_readlist": "Dodaj do listy", + "analyze": "Analizuj", + "delete": "Usuń", + "download_series": "Pobierz cykl", + "edit": "Edytuj", + "edit_metadata": "Edytuj metadane", + "mark_read": "Oznacz jako przeczytane", + "mark_unread": "Oznacz jako nieprzeczytane", + "refresh_metadata": "Odśwież metadane", + "scan_library_files": "Skanuj pliki bibliotek" + }, + "navigation": { + "home": "Ekran główny", + "libraries": "Biblioteki", + "logout": "Wyloguj się" + }, + "page_not_found": { + "go_back_to_home_page": "Wróć do ekranu głównego", + "page_does_not_exist": "Strona, której szukasz, nie istnieje.", + "page_not_found": "Strona nie znaleziona" + }, + "read_more": { + "less": "Mniej", + "more": "Więcej" + }, + "readlists_expansion_panel": { + "manage_readlist": "Zarządzanie listami", + "title": "lista {name}" + }, + "search": { + "no_results": "Wyszukiwanie nie zwróciło żadnych wyników", + "search": "Wyszukaj", + "search_for_something_else": "Spróbuj wyszukać coś innego", + "search_results_for": "Wyniki wyszukiwania dla \"{name}\"" + }, + "searchbox": { + "no_results": "Brak Wyników", + "search_all": "Przeszukaj wszystkie…" + }, + "server": { + "server_management": { + "button_shutdown": "Wyłącz", + "section_title": "Zarządzanie serwerem" + }, + "tab_title": "Serwer" + }, + "server_settings": { + "server_settings": "Ustawienia serwera" + }, + "settings_user": { + "edit_shared_libraries": "Edycja bibliotek udostępnionych", + "edit_user": "Edycja użytkownika", + "role_administrator": "Administrator", + "role_user": "Użytkownik" + }, + "sort": { + "books_count": "Liczba książek", + "date_added": "Data dodania", + "date_updated": "Data aktualizacji", + "file_name": "Nazwa pliku", + "file_size": "Rozmiar pliku", + "folder_name": "Nazwa katalogu", + "name": "Nazwa", + "number": "Numer", + "release_date": "Data wydania" + }, + "theme": { + "dark": "Ciemny", + "light": "Jasny", + "system": "Systemowy" + }, + "user_roles": { + "ADMIN": "Administrator", + "FILE_DOWNLOAD": "Pobieranie plików", + "PAGE_STREAMING": "Strumieniowanie stron", + "USER": "Użytkownik" + }, + "users": { + "users": "Użytkownicy" + }, + "welcome": { + "add_library": "Dodaj bibliotekę", + "no_libraries_yet": "Nie dodano jeszcze żadnych bibliotek!", + "welcome_message": "Witamy w Komga" + } +} diff --git a/komga-webui/src/locales/sv.json b/komga-webui/src/locales/sv.json index 9a8d1bb0d..2375b5226 100644 --- a/komga-webui/src/locales/sv.json +++ b/komga-webui/src/locales/sv.json @@ -28,7 +28,7 @@ }, "book_card": { "error": "Fel", - "unknown": "Ska analyserad", + "unknown": "Ska analyseras", "unsupported": "Stödjs inte" }, "bookreader": { @@ -37,16 +37,16 @@ "cycling_page_layout": "Växla Sidlayout", "cycling_scale": "Växla Skala", "cycling_side_padding": "Växla Spaltfyllnad", - "download_current_page": "Ladda ner aktuell sida", + "download_current_page": "Ladda ner nuvarande sida", "end_of_book": "Du har kommit till slutet av boken.", "from_series_metadata": "från seriens metadata", - "move_next": "Tryck \"Nästa\" igen för att gå till nästa bok.", - "move_next_exit": "Tryck \"Nästa\" igen för att stänga läsaren.", - "move_previous": "Tryck \"Föregående\" igen för att gå till föregående bok.", + "move_next": "Tryck ”Nästa” igen för att gå till nästa bok.", + "move_next_exit": "Tryck ”Nästa” igen för att stänga läsaren.", + "move_previous": "Tryck ”Föregående” igen för att gå till föregående bok.", "paged_reader_layout": { "double": "Dubbla sidor", "double_no_cover": "Dubbla sidor (inget omslag)", - "single": "En sida" + "single": "Enkelsidig" }, "reader_settings": "Läsinställningar", "scale_type": { @@ -207,28 +207,28 @@ "delete_collection": { "button_cancel": "Avbryt", "button_confirm": "Radera", - "confirm_delete": "Ja, radera samlingen \"{name}\"", + "confirm_delete": "Ja, radera samlingen ”{name}”", "dialog_title": "Radera samling", "warning_html": "Samlingen {name} tas bort från den här servern. Dina mediefiler påverkas inte. Detta kan inte ångras. Fortsätta?" }, "delete_library": { "button_cancel": "Avbryt", "button_confirm": "Radera", - "confirm_delete": "Ja, redera biblioteket \"{name}\"", + "confirm_delete": "Ja, redera biblioteket ”{name}”", "title": "Redera bibliotek", "warning_html": "Biblioteket {name} kommer att tas bort från den här servern. Dina mediefiler påverkas inte. Detta kan inte ångras. Fortsätta?" }, "delete_readlist": { "button_cancel": "Avbryt", "button_confirm": "Radera", - "confirm_delete": "Ja, radera läslistan \"{name}\"", + "confirm_delete": "Ja, radera läslistan ”{name}”", "dialog_title": "Radera läslista", "warning_html": "Läslistan {name} tas bort från den här servern. Dina mediefiler påverkas inte. Detta kan inte ångras. Fortsätta?" }, "delete_user": { "button_cancel": "Avbryt", "button_confirm": "Radera", - "confirm_delete": "Ja, radera användare \"{name}\"", + "confirm_delete": "Ja, radera användare ”{name}”", "dialog_title": "Radera användare", "warning_html": "Användaren {name} kommer att tas bort från den här servern. Detta kan inte ångras. Fortsätta?" }, @@ -380,8 +380,8 @@ "ERR_1000": "Filen kunde inte nås under analysen", "ERR_1001": "Mediatyp är inte supporterad", "ERR_1002": "Krypterade RAR-arkiv är inte supporterade", - "ERR_1003": "\"Solid RAR\"-arkiv är inte supporterade", - "ERR_1004": "\"Multi Volume RAR\"-arkiv är inte supporterade", + "ERR_1003": "Solid RAR-arkiv är inte supporterade", + "ERR_1004": "Multi Volume RAR-arkiv är inte supporterade", "ERR_1005": "Okänt fel vid analys av bok", "ERR_1006": "Boken innehåller inte några sidor", "ERR_1007": "Vissa poster kunde inte analyseras", @@ -468,7 +468,7 @@ "no_results": "Sökningen gav inga resultat", "search": "Sök", "search_for_something_else": "Försök söka efter något annat", - "search_results_for": "Sökresultat för \"{name}\"" + "search_results_for": "Sökresultat för ”{name}”" }, "searchbox": { "no_results": "Inga resultat", From 25d6272e0fe73cd0cee3e263fd82cafe7a63cac9 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 19 Apr 2021 17:12:34 +0800 Subject: [PATCH 06/39] refactor: book with media --- .../komga/domain/model/BookWithMedia.kt | 6 ++ .../komga/domain/service/BookAnalyzer.kt | 65 +++++++++---------- .../komga/domain/service/BookLifecycle.kt | 5 +- .../komga/domain/service/MetadataLifecycle.kt | 5 +- .../metadata/BookMetadataProvider.kt | 5 +- .../metadata/SeriesMetadataProvider.kt | 5 +- .../metadata/barcode/IsbnBarcodeProvider.kt | 7 +- .../metadata/comicrack/ComicInfoProvider.kt | 15 ++--- .../metadata/epub/EpubMetadataProvider.kt | 15 ++--- .../barcode/IsbnBarcodeProviderTest.kt | 7 +- .../comicrack/ComicInfoProviderTest.kt | 25 +++---- 11 files changed, 79 insertions(+), 81 deletions(-) create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/BookWithMedia.kt diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookWithMedia.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookWithMedia.kt new file mode 100644 index 000000000..e4185d029 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookWithMedia.kt @@ -0,0 +1,6 @@ +package org.gotson.komga.domain.model + +data class BookWithMedia( + val book: Book, + val media: Media, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt index bd7a0bd80..c611fed63 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt @@ -3,11 +3,11 @@ package org.gotson.komga.domain.service import mu.KotlinLogging import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookPage +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.model.MediaUnsupportedException import org.gotson.komga.domain.model.ThumbnailBook -import org.gotson.komga.domain.persistence.MediaRepository import org.gotson.komga.infrastructure.image.ImageConverter import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.mediacontainer.MediaContainerExtractor @@ -19,8 +19,7 @@ private val logger = KotlinLogging.logger {} class BookAnalyzer( private val contentDetector: ContentDetector, extractors: List, - private val imageConverter: ImageConverter, - private val mediaRepository: MediaRepository + private val imageConverter: ImageConverter ) { val supportedMediaTypes = extractors @@ -47,21 +46,21 @@ class BookAnalyzer( return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1008") } - val (pages, others) = entries - .partition { entry -> - entry.mediaType?.let { contentDetector.isImage(it) } ?: false - }.let { (images, others) -> - Pair( - images.map { BookPage(it.name, it.mediaType!!, it.dimension) }, - others - ) - } + val (pages, others) = entries + .partition { entry -> + entry.mediaType?.let { contentDetector.isImage(it) } ?: false + }.let { (images, others) -> + Pair( + images.map { BookPage(it.name, it.mediaType!!, it.dimension) }, + others + ) + } - val entriesErrorSummary = others - .filter { it.mediaType.isNullOrBlank() } - .map { it.name } - .ifEmpty { null } - ?.joinToString(prefix = "ERR_1007 [", postfix = "]") { it } + val entriesErrorSummary = others + .filter { it.mediaType.isNullOrBlank() } + .map { it.name } + .ifEmpty { null } + ?.joinToString(prefix = "ERR_1007 [", postfix = "]") { it } if (pages.isEmpty()) { logger.warn { "Book $book does not contain any pages" } @@ -69,24 +68,22 @@ class BookAnalyzer( } logger.info { "Book has ${pages.size} pages" } - val files = others.map { it.name } + val files = others.map { it.name } return Media(mediaType = mediaType, status = Media.Status.READY, pages = pages, files = files, comment = entriesErrorSummary) } @Throws(MediaNotReadyException::class) - fun generateThumbnail(book: Book): ThumbnailBook { + fun generateThumbnail(book: BookWithMedia): ThumbnailBook { logger.info { "Generate thumbnail for book: $book" } - val media = mediaRepository.findById(book.id) - - if (media.status != Media.Status.READY) { + if (book.media.status != Media.Status.READY) { logger.warn { "Book media is not ready, cannot generate thumbnail. Book: $book" } throw MediaNotReadyException() } val thumbnail = try { - supportedMediaTypes.getValue(media.mediaType!!).getEntryStream(book.path(), media.pages.first().fileName).let { cover -> + supportedMediaTypes.getValue(book.media.mediaType!!).getEntryStream(book.book.path(), book.media.pages.first().fileName).let { cover -> imageConverter.resizeImage(cover, thumbnailFormat, thumbnailSize) } } catch (ex: Exception) { @@ -97,7 +94,7 @@ class BookAnalyzer( return ThumbnailBook( thumbnail = thumbnail, type = ThumbnailBook.Type.GENERATED, - bookId = book.id + bookId = book.book.id ) } @@ -105,37 +102,33 @@ class BookAnalyzer( MediaNotReadyException::class, IndexOutOfBoundsException::class ) - fun getPageContent(book: Book, number: Int): ByteArray { + fun getPageContent(book: BookWithMedia, number: Int): ByteArray { logger.info { "Get page #$number for book: $book" } - val media = mediaRepository.findById(book.id) - - if (media.status != Media.Status.READY) { + if (book.media.status != Media.Status.READY) { logger.warn { "Book media is not ready, cannot get pages" } throw MediaNotReadyException() } - if (number > media.pages.size || number <= 0) { - logger.error { "Page number #$number is out of bounds. Book has ${media.pages.size} pages" } + if (number > book.media.pages.size || number <= 0) { + logger.error { "Page number #$number is out of bounds. Book has ${book.media.pages.size} pages" } throw IndexOutOfBoundsException("Page $number does not exist") } - return supportedMediaTypes.getValue(media.mediaType!!).getEntryStream(book.path(), media.pages[number - 1].fileName) + return supportedMediaTypes.getValue(book.media.mediaType!!).getEntryStream(book.book.path(), book.media.pages[number - 1].fileName) } @Throws( MediaNotReadyException::class ) - fun getFileContent(book: Book, fileName: String): ByteArray { + fun getFileContent(book: BookWithMedia, fileName: String): ByteArray { logger.info { "Get file $fileName for book: $book" } - val media = mediaRepository.findById(book.id) - - if (media.status != Media.Status.READY) { + if (book.media.status != Media.Status.READY) { logger.warn { "Book media is not ready, cannot get files" } throw MediaNotReadyException() } - return supportedMediaTypes.getValue(media.mediaType!!).getEntryStream(book.path(), fileName) + return supportedMediaTypes.getValue(book.media.mediaType!!).getEntryStream(book.book.path(), fileName) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt index ca7e0216a..f095b8b9b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt @@ -3,6 +3,7 @@ package org.gotson.komga.domain.service import mu.KotlinLogging import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookPageContent +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.ImageConversionException import org.gotson.komga.domain.model.KomgaUser import org.gotson.komga.domain.model.Media @@ -63,7 +64,7 @@ class BookLifecycle( fun generateThumbnailAndPersist(book: Book) { logger.info { "Generate thumbnail and persist for book: $book" } try { - addThumbnailForBook(bookAnalyzer.generateThumbnail(book)) + addThumbnailForBook(bookAnalyzer.generateThumbnail(BookWithMedia(book, mediaRepository.findById(book.id)))) } catch (ex: Exception) { logger.error(ex) { "Error while creating thumbnail" } } @@ -155,7 +156,7 @@ class BookLifecycle( ) fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null, resizeTo: Int? = null): BookPageContent { val media = mediaRepository.findById(book.id) - val pageContent = bookAnalyzer.getPageContent(book, number) + val pageContent = bookAnalyzer.getPageContent(BookWithMedia(book, mediaRepository.findById(book.id)), number) val pageMediaType = media.pages[number - 1].mediaType if (resizeTo != null) { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt index ae2590e2d..3eea94d0b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt @@ -4,6 +4,7 @@ import mu.KotlinLogging import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadataPatch import org.gotson.komga.domain.model.BookMetadataPatchCapability +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.Series import org.gotson.komga.domain.model.SeriesCollection @@ -65,7 +66,7 @@ class MetadataLifecycle( logger.info { "Library is not set to import book metadata from Barcode ISBN, skipping" } else -> { logger.debug { "Provider: $provider" } - val patch = provider.getBookMetadataFromBook(book, media) + val patch = provider.getBookMetadataFromBook(BookWithMedia(book, media)) if ( (provider is ComicInfoProvider && library.importComicInfoBook) || @@ -156,7 +157,7 @@ class MetadataLifecycle( else -> { logger.debug { "Provider: $provider" } val patches = bookRepository.findBySeriesId(series.id) - .mapNotNull { provider.getSeriesMetadataFromBook(it, mediaRepository.findById(it.id)) } + .mapNotNull { provider.getSeriesMetadataFromBook(BookWithMedia(it, mediaRepository.findById(it.id))) } if ( (provider is ComicInfoProvider && library.importComicInfoSeries) || diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/BookMetadataProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/BookMetadataProvider.kt index 085e9c112..723281db0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/BookMetadataProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/BookMetadataProvider.kt @@ -1,11 +1,10 @@ package org.gotson.komga.infrastructure.metadata -import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadataPatch import org.gotson.komga.domain.model.BookMetadataPatchCapability -import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.BookWithMedia interface BookMetadataProvider { fun getCapabilities(): List - fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? + fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch? } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataProvider.kt index 4abbda685..446a7be20 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataProvider.kt @@ -1,9 +1,8 @@ package org.gotson.komga.infrastructure.metadata -import org.gotson.komga.domain.model.Book -import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.SeriesMetadataPatch interface SeriesMetadataProvider { - fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? + fun getSeriesMetadataFromBook(book: BookWithMedia): SeriesMetadataPatch? } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProvider.kt index bb92076c8..5d00058f8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProvider.kt @@ -8,10 +8,9 @@ import com.google.zxing.RGBLuminanceSource import com.google.zxing.common.HybridBinarizer import mu.KotlinLogging import org.apache.commons.validator.routines.ISBNValidator -import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadataPatch import org.gotson.komga.domain.model.BookMetadataPatchCapability -import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.infrastructure.metadata.BookMetadataProvider import org.springframework.stereotype.Service @@ -37,8 +36,8 @@ class IsbnBarcodeProvider( override fun getCapabilities(): List = listOf(BookMetadataPatchCapability.ISBN) - override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? { - val pagesToTry = (1..media.pages.size).toList().let { + override fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch? { + val pagesToTry = (1..book.media.pages.size).toList().let { (it.takeLast(PAGES_LAST).reversed() + it.take(PAGES_FIRST)).distinct() } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt index 882eb48cf..7ceb2628c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt @@ -3,10 +3,9 @@ package org.gotson.komga.infrastructure.metadata.comicrack import com.fasterxml.jackson.dataformat.xml.XmlMapper import mu.KotlinLogging import org.gotson.komga.domain.model.Author -import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadataPatch import org.gotson.komga.domain.model.BookMetadataPatchCapability -import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.SeriesMetadataPatch import org.gotson.komga.domain.service.BookAnalyzer @@ -40,8 +39,8 @@ class ComicInfoProvider( BookMetadataPatchCapability.READ_LISTS, ) - override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? { - getComicInfo(book, media)?.let { comicInfo -> + override fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch? { + getComicInfo(book)?.let { comicInfo -> val releaseDate = comicInfo.year?.let { LocalDate.of(comicInfo.year!!, comicInfo.month ?: 1, comicInfo.day ?: 1) } @@ -83,8 +82,8 @@ class ComicInfoProvider( return null } - override fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? { - getComicInfo(book, media)?.let { comicInfo -> + override fun getSeriesMetadataFromBook(book: BookWithMedia): SeriesMetadataPatch? { + getComicInfo(book)?.let { comicInfo -> val readingDirection = when (comicInfo.manga) { Manga.NO -> SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT Manga.YES_AND_RIGHT_TO_LEFT -> SeriesMetadata.ReadingDirection.RIGHT_TO_LEFT @@ -110,9 +109,9 @@ class ComicInfoProvider( return null } - private fun getComicInfo(book: Book, media: Media): ComicInfo? { + private fun getComicInfo(book: BookWithMedia): ComicInfo? { try { - if (media.files.none { it == COMIC_INFO }) { + if (book.media.files.none { it == COMIC_INFO }) { logger.debug { "Book does not contain any $COMIC_INFO file: $book" } return null } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt index cf81dd997..fd647c45a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt @@ -2,10 +2,9 @@ package org.gotson.komga.infrastructure.metadata.epub import org.apache.commons.validator.routines.ISBNValidator import org.gotson.komga.domain.model.Author -import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadataPatch import org.gotson.komga.domain.model.BookMetadataPatchCapability -import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.SeriesMetadataPatch import org.gotson.komga.infrastructure.mediacontainer.EpubExtractor @@ -41,9 +40,9 @@ class EpubMetadataProvider( BookMetadataPatchCapability.ISBN, ) - override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? { - if (media.mediaType != "application/epub+zip") return null - epubExtractor.getPackageFile(book.path())?.let { packageFile -> + override fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch? { + if (book.media.mediaType != "application/epub+zip") return null + epubExtractor.getPackageFile(book.book.path())?.let { packageFile -> val opf = Jsoup.parse(packageFile) val title = opf.selectFirst("metadata > dc|title")?.text()?.ifBlank { null } @@ -80,9 +79,9 @@ class EpubMetadataProvider( return null } - override fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? { - if (media.mediaType != "application/epub+zip") return null - epubExtractor.getPackageFile(book.path())?.let { packageFile -> + override fun getSeriesMetadataFromBook(book: BookWithMedia): SeriesMetadataPatch? { + if (book.media.mediaType != "application/epub+zip") return null + epubExtractor.getPackageFile(book.book.path())?.let { packageFile -> val opf = Jsoup.parse(packageFile) val series = opf.selectFirst("metadata > meta[property=belongs-to-collection]")?.text()?.ifBlank { null } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProviderTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProviderTest.kt index d2c416506..7d25e4f66 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProviderTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/barcode/IsbnBarcodeProviderTest.kt @@ -5,6 +5,7 @@ import io.mockk.mockk import org.apache.commons.validator.routines.ISBNValidator import org.assertj.core.api.Assertions.assertThat import org.gotson.komga.domain.model.BookPage +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.makeBook import org.gotson.komga.domain.service.BookAnalyzer @@ -25,7 +26,7 @@ class IsbnBarcodeProviderTest { val media = Media(pages = listOf(BookPage("page", "image/jpeg"))) // when - val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media) + val patch = isbnBarcodeProvider.getBookMetadataFromBook(BookWithMedia(book, media)) // then assertThat(patch?.isbn).isEqualTo("9782811632397") @@ -40,7 +41,7 @@ class IsbnBarcodeProviderTest { val media = Media(pages = listOf(BookPage("page", "image/jpeg"))) // when - val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media) + val patch = isbnBarcodeProvider.getBookMetadataFromBook(BookWithMedia(book, media)) // then assertThat(patch).isNull() @@ -56,7 +57,7 @@ class IsbnBarcodeProviderTest { val media = Media(pages = listOf(BookPage("page", "image/jpeg"))) // when - val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media) + val patch = isbnBarcodeProvider.getBookMetadataFromBook(BookWithMedia(book, media)) // then assertThat(patch).isNull() diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt index 7fa7c742f..0c09ddfd9 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper import io.mockk.every import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat +import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.makeBook @@ -53,7 +54,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getBookMetadataFromBook(book, media) + val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media)) with(patch!!) { assertThat(title).isEqualTo("title") @@ -86,7 +87,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getBookMetadataFromBook(book, media) + val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media)) with(patch!!) { assertThat(title).isNull() @@ -106,7 +107,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getBookMetadataFromBook(book, media) + val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media)) with(patch!!) { assertThat(releaseDate).isNull() @@ -121,7 +122,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getBookMetadataFromBook(book, media) + val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media)) with(patch!!) { assertThat(releaseDate).isEqualTo(LocalDate.of(2020, 1, 1)) @@ -142,7 +143,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getBookMetadataFromBook(book, media) + val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media)) with(patch!!) { assertThat(authors).hasSize(7) @@ -165,7 +166,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getBookMetadataFromBook(book, media) + val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media)) with(patch!!) { assertThat(authors).hasSize(14) @@ -179,7 +180,7 @@ class ComicInfoProviderTest { val book = makeBook("book") val media = Media(Media.Status.READY) - val patch = comicInfoProvider.getBookMetadataFromBook(book, media) + val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media)) assertThat(patch).isNull() } @@ -201,7 +202,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!! with(patch) { assertThat(title).isEqualTo("series") @@ -226,7 +227,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!! with(patch) { assertThat(title).isEqualTo("series (2020)") @@ -242,7 +243,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!! with(patch) { assertThat(title).isEqualTo("series") @@ -257,7 +258,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!! with(patch) { assertThat(language).isNull() @@ -277,7 +278,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!! with(patch) { assertThat(title).isNull() From 34f77a83fc91ed5ccb66ec50a07206ed3baf06a1 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 19 Apr 2021 17:14:10 +0800 Subject: [PATCH 07/39] refactor: move exception handling inside BookAnalyzer.kt --- .../komga/domain/service/BookAnalyzer.kt | 46 +++++++++++-------- .../komga/domain/service/BookLifecycle.kt | 11 +---- .../komga/domain/service/BookLifecycleTest.kt | 6 +-- .../service/LibraryContentLifecycleTest.kt | 4 +- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt index c611fed63..046f43184 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt @@ -12,6 +12,7 @@ import org.gotson.komga.infrastructure.image.ImageConverter import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.mediacontainer.MediaContainerExtractor import org.springframework.stereotype.Service +import java.nio.file.AccessDeniedException private val logger = KotlinLogging.logger {} @@ -31,20 +32,20 @@ class BookAnalyzer( fun analyze(book: Book): Media { logger.info { "Trying to analyze book: $book" } + try { + val mediaType = contentDetector.detectMediaType(book.path()) + logger.info { "Detected media type: $mediaType" } + if (!supportedMediaTypes.containsKey(mediaType)) + return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = "ERR_1001", bookId = book.id) - val mediaType = contentDetector.detectMediaType(book.path()) - logger.info { "Detected media type: $mediaType" } - if (!supportedMediaTypes.containsKey(mediaType)) - return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = "ERR_1001") - - val entries = try { - supportedMediaTypes.getValue(mediaType).getEntries(book.path()) - } catch (ex: MediaUnsupportedException) { - return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = ex.code) - } catch (ex: Exception) { - logger.error(ex) { "Error while analyzing book: $book" } - return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1008") - } + val entries = try { + supportedMediaTypes.getValue(mediaType).getEntries(book.path()) + } catch (ex: MediaUnsupportedException) { + return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = ex.code, bookId = book.id) + } catch (ex: Exception) { + logger.error(ex) { "Error while analyzing book: $book" } + return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1008", bookId = book.id) + } val (pages, others) = entries .partition { entry -> @@ -62,15 +63,22 @@ class BookAnalyzer( .ifEmpty { null } ?.joinToString(prefix = "ERR_1007 [", postfix = "]") { it } - if (pages.isEmpty()) { - logger.warn { "Book $book does not contain any pages" } - return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1006") - } - logger.info { "Book has ${pages.size} pages" } + if (pages.isEmpty()) { + logger.warn { "Book $book does not contain any pages" } + return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1006", bookId = book.id) + } + logger.info { "Book has ${pages.size} pages" } val files = others.map { it.name } - return Media(mediaType = mediaType, status = Media.Status.READY, pages = pages, files = files, comment = entriesErrorSummary) + return Media(mediaType = mediaType, status = Media.Status.READY, pages = pages, files = files, comment = entriesErrorSummary, bookId = book.id) + } catch (ade: AccessDeniedException) { + logger.error(ade) { "Error while analyzing book: $book" } + return Media(status = Media.Status.ERROR, comment = "ERR_1000", bookId = book.id) + } catch (ex: Exception) { + logger.error(ex) { "Error while analyzing book: $book" } + return Media(status = Media.Status.ERROR, comment = "ERR_1005", bookId = book.id) + } } @Throws(MediaNotReadyException::class) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt index f095b8b9b..74a90cc2c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt @@ -20,7 +20,6 @@ import org.gotson.komga.infrastructure.image.ImageConverter import org.gotson.komga.infrastructure.image.ImageType import org.springframework.stereotype.Service import java.io.File -import java.nio.file.AccessDeniedException import java.nio.file.Files import java.nio.file.Paths @@ -40,15 +39,7 @@ class BookLifecycle( fun analyzeAndPersist(book: Book): Boolean { logger.info { "Analyze and persist book: $book" } - val media = try { - bookAnalyzer.analyze(book) - } catch (ade: AccessDeniedException) { - logger.error(ade) { "Error while analyzing book: $book" } - Media(status = Media.Status.ERROR, comment = "ERR_1000") - } catch (ex: Exception) { - logger.error(ex) { "Error while analyzing book: $book" } - Media(status = Media.Status.ERROR, comment = "ERR_1005") - }.copy(bookId = book.id) + val media = bookAnalyzer.analyze(book) // if the number of pages has changed, delete all read progress for that book mediaRepository.findById(book.id).let { previous -> diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt index 531f8bf02..89c457dbb 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookLifecycleTest.kt @@ -35,7 +35,7 @@ class BookLifecycleTest( @Autowired private val seriesLifecycle: SeriesLifecycle, @Autowired private val readProgressRepository: ReadProgressRepository, @Autowired private val mediaRepository: MediaRepository, - @Autowired private val userRepository: KomgaUserRepository + @Autowired private val userRepository: KomgaUserRepository, ) { @MockkBean @@ -90,7 +90,7 @@ class BookLifecycleTest( assertThat(readProgressRepository.findAll()).hasSize(2) // when - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg"))) + every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book.id) bookLifecycle.analyzeAndPersist(book) // then @@ -123,7 +123,7 @@ class BookLifecycleTest( assertThat(readProgressRepository.findAll()).hasSize(2) // when - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = (1..10).map { BookPage("$it", "image/jpeg") }) + every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = (1..10).map { BookPage("$it", "image/jpeg") }, bookId = book.id) bookLifecycle.analyzeAndPersist(book) // then diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt index 19acb7628..c8379a930 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryContentLifecycleTest.kt @@ -200,7 +200,7 @@ class LibraryContentLifecycleTest( ) libraryContentLifecycle.scanRootFolder(library) - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg"))) + every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id) bookRepository.findAll().map { bookLifecycle.analyzeAndPersist(it) } // when @@ -236,7 +236,7 @@ class LibraryContentLifecycleTest( ) libraryContentLifecycle.scanRootFolder(library) - every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg"))) + every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id) bookRepository.findAll().map { bookLifecycle.analyzeAndPersist(it) } // when From 02b08932babd27b5b309b3038279885ac65d0821 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 19 Apr 2021 17:17:37 +0800 Subject: [PATCH 08/39] feat(api): support for transient books Transient books are books that are outside of a Komga library, and not persisted --- ERRORCODES.md | 2 + komga/build.gradle.kts | 8 +- .../persistence/TransientBookRepository.kt | 9 ++ .../komga/domain/service/FileSystemScanner.kt | 25 +++- .../domain/service/TransientBookLifecycle.kt | 45 +++++++ .../cache/TransientBookCache.kt | 27 ++++ .../gotson/komga/infrastructure/web/Utils.kt | 11 ++ .../komga/interfaces/rest/BookController.kt | 11 +- .../rest/TransientBooksController.kt | 121 ++++++++++++++++++ 9 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt diff --git a/ERRORCODES.md b/ERRORCODES.md index a8906cccb..f1eb98ca5 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -1,3 +1,4 @@ + # Error codes Code | Description @@ -18,3 +19,4 @@ ERR_1012 | No match for series ERR_1013 | No unique match for book number within series ERR_1014 | No match for book number within series ERR_1015 | Error while deserializing ComicRack ReadingList +ERR_1016 | Directory not accessible or not a directory diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index c0c6a98d0..925c7df2f 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -83,6 +83,8 @@ dependencies { implementation("com.github.f4b6a3:tsid-creator:3.0.1") + implementation("com.github.ben-manes.caffeine:caffeine:2.9.0") + // While waiting for https://github.com/xerial/sqlite-jdbc/pull/491 and https://github.com/xerial/sqlite-jdbc/pull/494 // runtimeOnly("org.xerial:sqlite-jdbc:3.32.3.2") // jooqGenerator("org.xerial:sqlite-jdbc:3.32.3.2") @@ -107,7 +109,11 @@ tasks { withType { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs = listOf("-Xjsr305=strict", "-Xopt-in=kotlin.time.ExperimentalTime") + freeCompilerArgs = listOf( + "-Xjsr305=strict", + "-Xopt-in=kotlin.time.ExperimentalTime", + "-Xopt-in=kotlin.io.path.ExperimentalPathApi" + ) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt new file mode 100644 index 000000000..92a376a8f --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt @@ -0,0 +1,9 @@ +package org.gotson.komga.domain.persistence + +import org.gotson.komga.domain.model.BookWithMedia + +interface TransientBookRepository { + fun findById(transientBookId: String): BookWithMedia? + fun save(transientBook: BookWithMedia) + fun saveAll(transientBooks: Collection) +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/FileSystemScanner.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/FileSystemScanner.kt index 26ce70eb6..77f2c2ffb 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/FileSystemScanner.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/FileSystemScanner.kt @@ -17,6 +17,8 @@ import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.FileTime import java.time.LocalDateTime import java.time.ZoneId +import kotlin.io.path.exists +import kotlin.io.path.readAttributes import kotlin.time.measureTime private val logger = KotlinLogging.logger {} @@ -35,7 +37,7 @@ class FileSystemScanner( logger.info { "Force directory modified time: $forceDirectoryModifiedTime" } if (!(Files.isDirectory(root) && Files.isReadable(root))) - throw DirectoryNotFoundException("Library root is not accessible: $root") + throw DirectoryNotFoundException("Folder is not accessible: $root", "ERR_1016") val scannedSeries = mutableMapOf>() @@ -69,12 +71,7 @@ class FileSystemScanner( supportedExtensions.contains(FilenameUtils.getExtension(file.fileName.toString()).toLowerCase()) && !file.fileName.toString().startsWith(".") ) { - val book = Book( - name = FilenameUtils.getBaseName(file.fileName.toString()), - url = file.toUri().toURL(), - fileLastModified = attrs.getUpdatedTime(), - fileSize = attrs.size() - ) + val book = pathToBook(file, attrs) file.parent.let { key -> if (pathToBooks.containsKey(key)) pathToBooks[key]!!.add(book) else pathToBooks[key] = mutableListOf(book) @@ -118,6 +115,20 @@ class FileSystemScanner( return scannedSeries } + + fun scanFile(path: Path): Book? { + if (!path.exists()) return null + + return pathToBook(path, path.readAttributes()) + } + + private fun pathToBook(path: Path, attrs: BasicFileAttributes): Book = + Book( + name = FilenameUtils.getBaseName(path.fileName.toString()), + url = path.toUri().toURL(), + fileLastModified = attrs.getUpdatedTime(), + fileSize = attrs.size() + ) } fun BasicFileAttributes.getUpdatedTime(): LocalDateTime = diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt new file mode 100644 index 000000000..349a7fe7a --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt @@ -0,0 +1,45 @@ +package org.gotson.komga.domain.service + +import org.gotson.komga.domain.model.BookPageContent +import org.gotson.komga.domain.model.BookWithMedia +import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.MediaNotReadyException +import org.gotson.komga.domain.persistence.TransientBookRepository +import org.springframework.stereotype.Service +import java.nio.file.Paths + +@Service +class TransientBookLifecycle( + private val transientBookRepository: TransientBookRepository, + private val bookAnalyzer: BookAnalyzer, + private val fileSystemScanner: FileSystemScanner, +) { + + fun scanAndPersist(filePath: String): List { + val books = fileSystemScanner.scanRootFolder(Paths.get(filePath)).values.flatten().map { BookWithMedia(it, Media()) } + + transientBookRepository.saveAll(books) + + return books + } + + fun analyzeAndPersist(transientBook: BookWithMedia): BookWithMedia { + val media = bookAnalyzer.analyze(transientBook.book) + + val updated = transientBook.copy(media = media) + transientBookRepository.save(updated) + + return updated + } + + @Throws( + MediaNotReadyException::class, + IndexOutOfBoundsException::class + ) + fun getBookPage(transientBook: BookWithMedia, number: Int): BookPageContent { + val pageContent = bookAnalyzer.getPageContent(transientBook, number) + val pageMediaType = transientBook.media.pages[number - 1].mediaType + + return BookPageContent(number, pageContent, pageMediaType) + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt new file mode 100644 index 000000000..9eac7da77 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt @@ -0,0 +1,27 @@ +package org.gotson.komga.infrastructure.cache + +import com.github.benmanes.caffeine.cache.Caffeine +import mu.KotlinLogging +import org.gotson.komga.domain.model.BookWithMedia +import org.gotson.komga.domain.persistence.TransientBookRepository +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +private val logger = KotlinLogging.logger {} + +@Service +class TransientBookCache : TransientBookRepository { + private val cache = Caffeine.newBuilder() + .expireAfterAccess(1, TimeUnit.HOURS) + .build() + + override fun findById(transientBookId: String): BookWithMedia? = cache.getIfPresent(transientBookId) + + override fun save(transientBook: BookWithMedia) { + cache.put(transientBook.book.id, transientBook) + } + + override fun saveAll(transientBooks: Collection) { + cache.putAll(transientBooks.associateBy { it.book.id }) + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/web/Utils.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/web/Utils.kt index dbe271e54..73e45ec08 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/web/Utils.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/web/Utils.kt @@ -1,6 +1,7 @@ package org.gotson.komga.infrastructure.web import org.springframework.http.CacheControl +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import java.net.URL import java.nio.file.Paths @@ -20,3 +21,13 @@ val cachePrivate = CacheControl .noTransform() .cachePrivate() .mustRevalidate() + +fun getMediaTypeOrDefault(mediaTypeString: String?): MediaType { + mediaTypeString?.let { + try { + return MediaType.parseMediaType(mediaTypeString) + } catch (ex: Exception) { + } + } + return MediaType.APPLICATION_OCTET_STREAM +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt index 6e119fb2e..bd3de0ecd 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt @@ -28,6 +28,7 @@ import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam +import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault import org.gotson.komga.infrastructure.web.setCachePrivate import org.gotson.komga.interfaces.rest.dto.BookDto import org.gotson.komga.interfaces.rest.dto.BookMetadataUpdateDto @@ -503,14 +504,4 @@ class BookController( private fun getBookLastModified(media: Media) = media.lastModifiedDate.toInstant(ZoneOffset.UTC).toEpochMilli() - - private fun getMediaTypeOrDefault(mediaTypeString: String?): MediaType { - mediaTypeString?.let { - try { - return MediaType.parseMediaType(mediaTypeString) - } catch (ex: Exception) { - } - } - return MediaType.APPLICATION_OCTET_STREAM - } } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt new file mode 100644 index 000000000..312593eb7 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt @@ -0,0 +1,121 @@ +package org.gotson.komga.interfaces.rest + +import com.jakewharton.byteunits.BinaryByteUnit +import mu.KotlinLogging +import org.gotson.komga.domain.model.BookWithMedia +import org.gotson.komga.domain.model.DirectoryNotFoundException +import org.gotson.komga.domain.model.MediaNotReadyException +import org.gotson.komga.domain.model.ROLE_ADMIN +import org.gotson.komga.domain.persistence.TransientBookRepository +import org.gotson.komga.domain.service.TransientBookLifecycle +import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault +import org.gotson.komga.infrastructure.web.toFilePath +import org.gotson.komga.interfaces.rest.dto.PageDto +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException +import java.nio.file.NoSuchFileException +import java.time.LocalDateTime + +private val logger = KotlinLogging.logger {} + +@RestController +@RequestMapping("api/v1/transient-books", produces = [MediaType.APPLICATION_JSON_VALUE]) +@PreAuthorize("hasRole('$ROLE_ADMIN')") +class TransientBooksController( + private val transientBookLifecycle: TransientBookLifecycle, + private val transientBookRepository: TransientBookRepository, +) { + + @PostMapping + fun scanForTransientBooks( + @RequestBody request: ScanRequestDto + ): List = + try { + transientBookLifecycle.scanAndPersist(request.path) + .sortedBy { it.book.path() } + .map { it.toDto() } + } catch (e: DirectoryNotFoundException) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.code) + } + + @PostMapping("{id}/analyze") + fun analyze( + @PathVariable id: String, + ): TransientBookDto = transientBookRepository.findById(id)?.let { + transientBookLifecycle.analyzeAndPersist(it).toDto() + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + @GetMapping( + value = ["{id}/pages/{pageNumber}"], + produces = [MediaType.ALL_VALUE] + ) + fun getSourcePage( + @PathVariable id: String, + @PathVariable pageNumber: Int, + ): ResponseEntity = + transientBookRepository.findById(id)?.let { + try { + val pageContent = transientBookLifecycle.getBookPage(it, pageNumber) + + ResponseEntity.ok() + .contentType(getMediaTypeOrDefault(pageContent.mediaType)) + .body(pageContent.content) + } catch (ex: IndexOutOfBoundsException) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist") + } catch (ex: MediaNotReadyException) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed") + } catch (ex: NoSuchFileException) { + logger.warn(ex) { "File not found}" } + throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved") + } + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) +} + +private fun BookWithMedia.toDto() = + TransientBookDto( + id = book.id, + name = book.name, + url = book.url.toFilePath(), + fileLastModified = book.fileLastModified, + sizeBytes = book.fileSize, + status = media.status.toString(), + mediaType = media.mediaType ?: "", + pages = media.pages.mapIndexed { index, bookPage -> + PageDto( + number = index + 1, + fileName = bookPage.fileName, + mediaType = bookPage.mediaType, + width = bookPage.dimension?.width, + height = bookPage.dimension?.height, + ) + }, + files = media.files, + comment = media.comment ?: "", + ) + +data class ScanRequestDto( + val path: String, +) + +data class TransientBookDto( + val id: String, + val name: String, + val url: String, + val fileLastModified: LocalDateTime, + val sizeBytes: Long, + val size: String = BinaryByteUnit.format(sizeBytes), + val status: String, + val mediaType: String, + val pages: List, + val files: List, + val comment: String, +) From d41dcefd3efd4f9844d5b3b1d336a246c320a1ec Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 19 Apr 2021 17:19:03 +0800 Subject: [PATCH 09/39] feat(api): import books Books can be imported directly into an existing Series --- .idea/codeStyles/Project.xml | 18 +- .../gotson/komga/application/tasks/Task.kt | 5 + .../komga/application/tasks/TaskHandler.kt | 10 +- .../komga/application/tasks/TaskReceiver.kt | 5 + .../org/gotson/komga/domain/model/CopyMode.kt | 7 + .../persistence/ReadProgressRepository.kt | 1 + .../komga/domain/service/BookImporter.kt | 138 +++++++ .../infrastructure/jooq/ReadProgressDao.kt | 6 + .../komga/interfaces/rest/BookController.kt | 22 ++ .../interfaces/rest/dto/BookImportBatchDto.kt | 15 + .../org/gotson/komga/domain/model/Utils.kt | 8 +- .../komga/domain/service/BookImporterTest.kt | 366 ++++++++++++++++++ 12 files changed, 579 insertions(+), 22 deletions(-) create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/CopyMode.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookImportBatchDto.kt create mode 100644 komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 2acbb5bd6..ea881c68b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -2,31 +2,15 @@ - - \ No newline at end of file diff --git a/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt b/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt index e9e0e1059..6885b0b4b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt @@ -1,6 +1,7 @@ package org.gotson.komga.application.tasks import org.gotson.komga.domain.model.BookMetadataPatchCapability +import org.gotson.komga.domain.model.CopyMode import java.io.Serializable sealed class Task : Serializable { @@ -29,4 +30,8 @@ sealed class Task : Serializable { data class AggregateSeriesMetadata(val seriesId: String) : Task() { override fun uniqueId() = "AGGREGATE_SERIES_METADATA_$seriesId" } + + data class ImportBook(val sourceFile: String, val seriesId: String, val copyMode: CopyMode, val destinationName: String?, val upgradeBookId: String?) : Task() { + override fun uniqueId(): String = "IMPORT_BOOK_${seriesId}_$sourceFile" + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt index 016c63734..bff748aab 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt @@ -4,6 +4,7 @@ import mu.KotlinLogging import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.SeriesRepository +import org.gotson.komga.domain.service.BookImporter import org.gotson.komga.domain.service.BookLifecycle import org.gotson.komga.domain.service.LibraryContentLifecycle import org.gotson.komga.domain.service.MetadataLifecycle @@ -11,6 +12,7 @@ import org.gotson.komga.infrastructure.jms.QUEUE_TASKS import org.gotson.komga.infrastructure.jms.QUEUE_TASKS_SELECTOR import org.springframework.jms.annotation.JmsListener import org.springframework.stereotype.Service +import java.nio.file.Paths import kotlin.time.measureTime private val logger = KotlinLogging.logger {} @@ -23,7 +25,8 @@ class TaskHandler( private val seriesRepository: SeriesRepository, private val libraryContentLifecycle: LibraryContentLifecycle, private val bookLifecycle: BookLifecycle, - private val metadataLifecycle: MetadataLifecycle + private val metadataLifecycle: MetadataLifecycle, + private val bookImporter: BookImporter, ) { @JmsListener(destination = QUEUE_TASKS, selector = QUEUE_TASKS_SELECTOR) @@ -68,6 +71,11 @@ class TaskHandler( seriesRepository.findByIdOrNull(task.seriesId)?.let { metadataLifecycle.aggregateMetadata(it) } ?: logger.warn { "Cannot execute task $task: Series does not exist" } + + is Task.ImportBook -> + seriesRepository.findByIdOrNull(task.seriesId)?.let { series -> + bookImporter.importBook(Paths.get(task.sourceFile), series, task.copyMode, task.destinationName, task.upgradeBookId) + } ?: logger.warn { "Cannot execute task $task: Series does not exist" } } }.also { logger.info { "Task $task executed in $it" } diff --git a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt index 0b5d6d2f9..5928fdae4 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt @@ -4,6 +4,7 @@ import mu.KotlinLogging import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookMetadataPatchCapability import org.gotson.komga.domain.model.BookSearch +import org.gotson.komga.domain.model.CopyMode import org.gotson.komga.domain.model.Library import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.persistence.BookRepository @@ -77,6 +78,10 @@ class TaskReceiver( submitTask(Task.AggregateSeriesMetadata(seriesId)) } + fun importBook(sourceFile: String, seriesId: String, copyMode: CopyMode, destinationName: String?, upgradeBookId: String?) { + submitTask(Task.ImportBook(sourceFile, seriesId, copyMode, destinationName, upgradeBookId)) + } + private fun submitTask(task: Task) { logger.info { "Sending task: $task" } jmsTemplate.convertAndSend(QUEUE_TASKS, task) { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/CopyMode.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/CopyMode.kt new file mode 100644 index 000000000..f885b9de8 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/CopyMode.kt @@ -0,0 +1,7 @@ +package org.gotson.komga.domain.model + +enum class CopyMode { + MOVE, + COPY, + HARDLINK, +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt index fb7b6cb2e..69fed2ccd 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadProgressRepository.kt @@ -6,6 +6,7 @@ interface ReadProgressRepository { fun findAll(): Collection fun findByBookIdAndUserId(bookId: String, userId: String): ReadProgress? fun findByUserId(userId: String): Collection + fun findByBookId(bookId: String): Collection fun save(readProgress: ReadProgress) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt new file mode 100644 index 000000000..b2b0660a8 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookImporter.kt @@ -0,0 +1,138 @@ +package org.gotson.komga.domain.service + +import mu.KotlinLogging +import org.gotson.komga.application.tasks.TaskReceiver +import org.gotson.komga.domain.model.CopyMode +import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.Series +import org.gotson.komga.domain.persistence.BookRepository +import org.gotson.komga.domain.persistence.MediaRepository +import org.gotson.komga.domain.persistence.ReadListRepository +import org.gotson.komga.domain.persistence.ReadProgressRepository +import org.gotson.komga.infrastructure.language.toIndexedMap +import org.springframework.stereotype.Service +import java.io.FileNotFoundException +import java.nio.file.FileAlreadyExistsException +import java.nio.file.Files +import java.nio.file.NoSuchFileException +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.copyTo +import kotlin.io.path.deleteExisting +import kotlin.io.path.deleteIfExists +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.moveTo +import kotlin.io.path.notExists + +private val logger = KotlinLogging.logger {} + +@Service +class BookImporter( + private val bookLifecycle: BookLifecycle, + private val fileSystemScanner: FileSystemScanner, + private val seriesLifecycle: SeriesLifecycle, + private val bookRepository: BookRepository, + private val mediaRepository: MediaRepository, + private val readProgressRepository: ReadProgressRepository, + private val readListRepository: ReadListRepository, + private val taskReceiver: TaskReceiver, +) { + + fun importBook(sourceFile: Path, series: Series, copyMode: CopyMode, destinationName: String? = null, upgradeBookId: String? = null) { + if (sourceFile.notExists()) throw FileNotFoundException("File not found: $sourceFile") + + val destFile = series.path().resolve( + if (destinationName != null) Paths.get("$destinationName.${sourceFile.extension}").fileName.toString() + else sourceFile.fileName.toString() + ) + + val upgradedBookId = + if (upgradeBookId != null) { + bookRepository.findByIdOrNull(upgradeBookId)?.let { + if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series") + it.id + } + } else null + val upgradedBookPath = + if (upgradedBookId != null) + bookRepository.findByIdOrNull(upgradedBookId)?.path() + else null + + var deletedUpgradedFile = false + when { + upgradedBookPath != null && destFile == upgradedBookPath -> { + logger.info { "Deleting existing file: $upgradedBookPath" } + try { + upgradedBookPath.deleteExisting() + deletedUpgradedFile = true + } catch (e: NoSuchFileException) { + logger.warn { "Could not delete upgraded book: $upgradedBookPath" } + } + } + destFile.exists() -> throw FileAlreadyExistsException("Destination file already exists: $destFile") + } + + when (copyMode) { + CopyMode.MOVE -> { + logger.info { "Moving file $sourceFile to $destFile" } + sourceFile.moveTo(destFile) + } + CopyMode.COPY -> { + logger.info { "Copying file $sourceFile to $destFile" } + sourceFile.copyTo(destFile) + } + CopyMode.HARDLINK -> try { + logger.info { "Hardlink file $sourceFile to $destFile" } + Files.createLink(destFile, sourceFile) + } catch (e: UnsupportedOperationException) { + logger.warn { "Filesystem does not support hardlinks, copying instead" } + sourceFile.copyTo(destFile) + } + } + + val importedBook = fileSystemScanner.scanFile(destFile) + ?.copy(libraryId = series.libraryId) + ?: throw IllegalStateException("Newly imported book could not be scanned: $destFile") + + seriesLifecycle.addBooks(series, listOf(importedBook)) + + if (upgradedBookId != null) { + // copy media and mark it as outdated + mediaRepository.findById(upgradedBookId).let { + mediaRepository.update( + it.copy( + bookId = importedBook.id, + status = Media.Status.OUTDATED, + ) + ) + } + + // copy read progress + readProgressRepository.findByBookId(upgradedBookId) + .map { it.copy(bookId = importedBook.id) } + .forEach { readProgressRepository.save(it) } + + // replace upgraded book by imported book in read lists + readListRepository.findAllByBook(upgradedBookId, filterOnLibraryIds = null) + .forEach { rl -> + readListRepository.update( + rl.copy( + bookIds = rl.bookIds.values.map { if (it == upgradedBookId) importedBook.id else it }.toIndexedMap() + ) + ) + } + + // delete upgraded book file on disk if it has not been replaced earlier + if (upgradedBookPath != null && !deletedUpgradedFile && upgradedBookPath.deleteIfExists()) + logger.info { "Deleted existing file: $upgradedBookPath" } + + // delete upgraded book + bookLifecycle.deleteOne(upgradedBookId) + } + + seriesLifecycle.sortBooks(series) + + taskReceiver.analyzeBook(importedBook) + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt index 16bda71d1..f7463870f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDao.kt @@ -33,6 +33,12 @@ class ReadProgressDao( .fetchInto(r) .map { it.toDomain() } + override fun findByBookId(bookId: String): Collection = + dsl.selectFrom(r) + .where(r.BOOK_ID.eq(bookId)) + .fetchInto(r) + .map { it.toDomain() } + override fun save(readProgress: ReadProgress) { dsl.insertInto(r, r.BOOK_ID, r.USER_ID, r.PAGE, r.COMPLETED) .values(readProgress.bookId, readProgress.userId, readProgress.page, readProgress.completed) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt index bd3de0ecd..3cf54f2d8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt @@ -31,6 +31,7 @@ import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault import org.gotson.komga.infrastructure.web.setCachePrivate import org.gotson.komga.interfaces.rest.dto.BookDto +import org.gotson.komga.interfaces.rest.dto.BookImportBatchDto import org.gotson.komga.interfaces.rest.dto.BookMetadataUpdateDto import org.gotson.komga.interfaces.rest.dto.PageDto import org.gotson.komga.interfaces.rest.dto.ReadListDto @@ -499,6 +500,27 @@ class BookController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @PostMapping("api/v1/books/import") + @PreAuthorize("hasRole('$ROLE_ADMIN')") + @ResponseStatus(HttpStatus.ACCEPTED) + fun importBooks( + @RequestBody bookImportBatch: BookImportBatchDto, + ) { + bookImportBatch.books.forEach { + try { + taskReceiver.importBook( + sourceFile = it.sourceFile, + seriesId = it.seriesId, + copyMode = bookImportBatch.copyMode, + destinationName = it.destinationName, + upgradeBookId = it.upgradeBookId, + ) + } catch (e: Exception) { + logger.error(e) { "Error while creating import task for: $it" } + } + } + } + private fun ResponseEntity.BodyBuilder.setNotModified(media: Media) = this.setCachePrivate().lastModified(getBookLastModified(media)) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookImportBatchDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookImportBatchDto.kt new file mode 100644 index 000000000..1b5c48fa2 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookImportBatchDto.kt @@ -0,0 +1,15 @@ +package org.gotson.komga.interfaces.rest.dto + +import org.gotson.komga.domain.model.CopyMode + +data class BookImportBatchDto( + val books: List = emptyList(), + val copyMode: CopyMode, +) + +data class BookImportDto( + val sourceFile: String, + val seriesId: String, + val upgradeBookId: String? = null, + val destinationName: String? = null, +) diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/model/Utils.kt b/komga/src/test/kotlin/org/gotson/komga/domain/model/Utils.kt index 4c9ba58a9..c91d2eab9 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/model/Utils.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/model/Utils.kt @@ -4,22 +4,22 @@ import com.github.f4b6a3.tsid.TsidCreator import java.net.URL import java.time.LocalDateTime -fun makeBook(name: String, fileLastModified: LocalDateTime = LocalDateTime.now(), libraryId: String = "", seriesId: String = ""): Book { +fun makeBook(name: String, fileLastModified: LocalDateTime = LocalDateTime.now(), libraryId: String = "", seriesId: String = "", url: URL? = null): Book { Thread.sleep(5) return Book( name = name, - url = URL("file:/$name"), + url = url ?: URL("file:/$name"), fileLastModified = fileLastModified, libraryId = libraryId, seriesId = seriesId ) } -fun makeSeries(name: String, libraryId: String = ""): Series { +fun makeSeries(name: String, libraryId: String = "", url: URL? = null): Series { Thread.sleep(5) return Series( name = name, - url = URL("file:/$name"), + url = url ?: URL("file:/$name"), fileLastModified = LocalDateTime.now(), libraryId = libraryId ) diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt new file mode 100644 index 000000000..c6c50264c --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookImporterTest.kt @@ -0,0 +1,366 @@ +package org.gotson.komga.domain.service + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import com.ninjasquad.springmockk.MockkBean +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.verify +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.gotson.komga.application.tasks.TaskReceiver +import org.gotson.komga.domain.model.Book +import org.gotson.komga.domain.model.BookPage +import org.gotson.komga.domain.model.CopyMode +import org.gotson.komga.domain.model.KomgaUser +import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.ReadList +import org.gotson.komga.domain.model.makeBook +import org.gotson.komga.domain.model.makeLibrary +import org.gotson.komga.domain.model.makeSeries +import org.gotson.komga.domain.persistence.BookRepository +import org.gotson.komga.domain.persistence.KomgaUserRepository +import org.gotson.komga.domain.persistence.LibraryRepository +import org.gotson.komga.domain.persistence.MediaRepository +import org.gotson.komga.domain.persistence.ReadListRepository +import org.gotson.komga.domain.persistence.ReadProgressRepository +import org.gotson.komga.domain.persistence.SeriesRepository +import org.gotson.komga.infrastructure.language.toIndexedMap +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.io.FileNotFoundException +import java.nio.file.FileAlreadyExistsException +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.createDirectories +import kotlin.io.path.createDirectory +import kotlin.io.path.createFile + +@ExtendWith(SpringExtension::class) +@SpringBootTest +class BookImporterTest( + @Autowired private val bookImporter: BookImporter, + @Autowired private val bookRepository: BookRepository, + @Autowired private val bookLifecycle: BookLifecycle, + @Autowired private val readProgressRepository: ReadProgressRepository, + @Autowired private val libraryRepository: LibraryRepository, + @Autowired private val seriesRepository: SeriesRepository, + @Autowired private val seriesLifecycle: SeriesLifecycle, + @Autowired private val mediaRepository: MediaRepository, + @Autowired private val userRepository: KomgaUserRepository, + @Autowired private val readListRepository: ReadListRepository, + @Autowired private val readListLifecycle: ReadListLifecycle, +) { + + @MockkBean + private lateinit var mockTaskReceiver: TaskReceiver + + private val library = makeLibrary("lib", "file:/library") + private val user1 = KomgaUser("user1@example.org", "", false) + private val user2 = KomgaUser("user2@example.org", "", false) + + @BeforeAll + fun init() { + libraryRepository.insert(library) + + userRepository.insert(user1) + userRepository.insert(user2) + } + + @BeforeEach + fun beforeEach() { + every { mockTaskReceiver.analyzeBook(any()) } just Runs + every { mockTaskReceiver.refreshBookMetadata(any(), any()) } just Runs + } + + @AfterAll + fun teardown() { + libraryRepository.deleteAll() + userRepository.deleteAll() + } + + @AfterEach + fun `clear repository`() { + seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id }) + } + + @Test + fun `given non-existent source file when importing then exception is thrown`() { + // given + val sourceFile = Paths.get("/non-existent") + + // when + val thrown = Assertions.catchThrowable { + bookImporter.importBook(sourceFile, makeSeries("a series"), CopyMode.COPY) + } + + // then + assertThat(thrown).isInstanceOf(FileNotFoundException::class.java) + } + + @Test + fun `given existing target when importing then exception is thrown`() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + // given + val sourceDir = fs.getPath("/source").createDirectory() + val sourceFile = sourceDir.resolve("source.cbz").createFile() + val destDir = fs.getPath("/dest").createDirectory() + destDir.resolve("source.cbz").createFile() + + val series = makeSeries("dest", url = destDir.toUri().toURL()) + + // when + val thrown = Assertions.catchThrowable { + bookImporter.importBook(sourceFile, series, CopyMode.COPY) + } + + // then + assertThat(thrown).isInstanceOf(FileAlreadyExistsException::class.java) + } + } + + @Test + fun `given existing target when importing with destination name then exception is thrown`() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + // given + val sourceDir = fs.getPath("/source").createDirectory() + val sourceFile = sourceDir.resolve("source.cbz").createFile() + val destDir = fs.getPath("/dest").createDirectory() + destDir.resolve("dest.cbz").createFile() + + val series = makeSeries("dest").copy(url = destDir.toUri().toURL()) + + // when + val thrown = Assertions.catchThrowable { + bookImporter.importBook(sourceFile, series, CopyMode.COPY, destinationName = "dest") + } + + // then + assertThat(thrown).isInstanceOf(FileAlreadyExistsException::class.java) + } + } + + @Test + fun `given book when importing then book is imported and series is sorted`() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + // given + val sourceDir = fs.getPath("/source").createDirectory() + val sourceFile = sourceDir.resolve("2.cbz").createFile() + val destDir = fs.getPath("/library/series").createDirectories() + + val existingBooks = listOf( + makeBook("1", libraryId = library.id), + makeBook("3", libraryId = library.id), + ) + val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id) + .also { series -> + seriesLifecycle.createSeries(series) + seriesLifecycle.addBooks(series, existingBooks) + seriesLifecycle.sortBooks(series) + } + + // when + bookImporter.importBook(sourceFile, series, CopyMode.COPY) + + // then + val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number } + assertThat(books).hasSize(3) + assertThat(books[0].id).isEqualTo(existingBooks[0].id) + assertThat(books[2].id).isEqualTo(existingBooks[1].id) + + with(books[1]) { + assertThat(id) + .isNotEqualTo(existingBooks[0].id) + .isNotEqualTo(existingBooks[1].id) + assertThat(number).isEqualTo(2) + assertThat(name).isEqualTo("2") + + val newMedia = mediaRepository.findById(id) + assertThat(newMedia.status).isEqualTo(Media.Status.UNKNOWN) + } + + verify(exactly = 1) { mockTaskReceiver.analyzeBook(any()) } + } + } + + @Test + fun `given existing book when importing with upgrade then existing book is deleted`() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + // given + val sourceDir = fs.getPath("/source").createDirectory() + val sourceFile = sourceDir.resolve("source.cbz").createFile() + val destDir = fs.getPath("/library/series").createDirectories() + val existingFile = destDir.resolve("4.cbz").createFile() + + val bookToUpgrade = makeBook("2", libraryId = library.id, url = existingFile.toUri().toURL()) + val otherBooks = listOf( + makeBook("1", libraryId = library.id), + makeBook("3", libraryId = library.id), + ) + val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id) + .also { series -> + seriesLifecycle.createSeries(series) + seriesLifecycle.addBooks(series, listOf(bookToUpgrade) + otherBooks) + seriesLifecycle.sortBooks(series) + } + + // when + bookImporter.importBook(sourceFile, series, CopyMode.MOVE, upgradeBookId = bookToUpgrade.id) + + // then + val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number } + assertThat(books).hasSize(3) + assertThat(books[0].id).isEqualTo(otherBooks[0].id) + assertThat(books[1].id).isEqualTo(otherBooks[1].id) + assertThat(books[2].id).isNotEqualTo(bookToUpgrade.id) + + assertThat(bookRepository.findByIdOrNull(bookToUpgrade.id)).isNull() + + val upgradedMedia = mediaRepository.findById(books[2].id) + assertThat(upgradedMedia.status).isEqualTo(Media.Status.OUTDATED) + + assertThat(Files.notExists(sourceFile)).isTrue + + verify(exactly = 1) { mockTaskReceiver.analyzeBook(any()) } + } + } + + @Test + fun `given existing book when importing with upgrade and same name then existing book is replaced`() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + // given + val sourceDir = fs.getPath("/source").createDirectory() + val sourceFile = sourceDir.resolve("source.cbz").createFile() + val destDir = fs.getPath("/library/series").createDirectories() + val existingFile = destDir.resolve("2.cbz").createFile() + + val bookToUpgrade = makeBook("2", libraryId = library.id, url = existingFile.toUri().toURL()) + val otherBooks = listOf( + makeBook("1", libraryId = library.id), + makeBook("3", libraryId = library.id), + ) + val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id) + .also { series -> + seriesLifecycle.createSeries(series) + seriesLifecycle.addBooks(series, listOf(bookToUpgrade) + otherBooks) + seriesLifecycle.sortBooks(series) + } + + // when + bookImporter.importBook(sourceFile, series, CopyMode.COPY, destinationName = "2", upgradeBookId = bookToUpgrade.id) + + // then + val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number } + assertThat(books).hasSize(3) + assertThat(books[0].id).isEqualTo(otherBooks[0].id) + assertThat(books[1].id).isNotEqualTo(bookToUpgrade.id) + assertThat(books[2].id).isEqualTo(otherBooks[1].id) + + assertThat(bookRepository.findByIdOrNull(bookToUpgrade.id)).isNull() + + val upgradedMedia = mediaRepository.findById(books[1].id) + assertThat(upgradedMedia.status).isEqualTo(Media.Status.OUTDATED) + + assertThat(Files.exists(sourceFile)).isTrue + + verify(exactly = 1) { mockTaskReceiver.analyzeBook(any()) } + } + } + + @Test + fun `given book with read progress when importing with upgrade then read progress is kept`() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + // given + val sourceDir = fs.getPath("/source").createDirectory() + val sourceFile = sourceDir.resolve("source.cbz").createFile() + val destDir = fs.getPath("/library/series").createDirectories() + val existingFile = destDir.resolve("1.cbz").createFile() + + val bookToUpgrade = makeBook("1", libraryId = library.id, url = existingFile.toUri().toURL()) + val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id) + .also { series -> + seriesLifecycle.createSeries(series) + seriesLifecycle.addBooks(series, listOf(bookToUpgrade)) + seriesLifecycle.sortBooks(series) + } + + mediaRepository.findById(bookToUpgrade.id).let { media -> + mediaRepository.update( + media.copy( + status = Media.Status.READY, + pages = (1..10).map { BookPage("$it", "image/jpeg") } + ) + ) + } + + bookLifecycle.markReadProgressCompleted(bookToUpgrade.id, user1) + bookLifecycle.markReadProgress(bookToUpgrade, user2, 4) + + // when + bookImporter.importBook(sourceFile, series, CopyMode.MOVE, upgradeBookId = bookToUpgrade.id) + + // then + val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number } + assertThat(books).hasSize(1) + + val progress = readProgressRepository.findByBookId(books[0].id) + assertThat(progress).hasSize(2) + with(progress.find { it.userId == user1.id }!!) { + assertThat(completed).isTrue + } + with(progress.find { it.userId == user2.id }!!) { + assertThat(completed).isFalse + assertThat(page).isEqualTo(4) + } + + verify(exactly = 1) { mockTaskReceiver.analyzeBook(any()) } + } + } + + @Test + fun `given book part of a read list when importing with upgrade then imported book replaces upgraded book in the read list`() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + // given + val sourceDir = fs.getPath("/source").createDirectory() + val sourceFile = sourceDir.resolve("source.cbz").createFile() + val destDir = fs.getPath("/library/series").createDirectories() + val existingFile = destDir.resolve("1.cbz").createFile() + + val bookToUpgrade = makeBook("1", libraryId = library.id, url = existingFile.toUri().toURL()) + val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id) + .also { series -> + seriesLifecycle.createSeries(series) + seriesLifecycle.addBooks(series, listOf(bookToUpgrade)) + seriesLifecycle.sortBooks(series) + } + + val readList = ReadList( + name = "readlist", + bookIds = listOf(bookToUpgrade.id).toIndexedMap(), + ) + readListLifecycle.addReadList(readList) + + // when + bookImporter.importBook(sourceFile, series, CopyMode.MOVE, upgradeBookId = bookToUpgrade.id) + + // then + val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number } + assertThat(books).hasSize(1) + + with(readListRepository.findByIdOrNull(readList.id)!!) { + assertThat(bookIds).hasSize(1) + assertThat(bookIds[0]).isEqualTo(books[0].id) + } + + verify(exactly = 1) { mockTaskReceiver.analyzeBook(any()) } + } + } +} From 13b304dd147f3102345c2edb85d41f87ccae1871 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 19 Apr 2021 17:20:01 +0800 Subject: [PATCH 10/39] feat(webui): import books Books can be imported directly into an existing Series --- komga-webui/src/components/FileImportRow.vue | 262 ++++++++++++++++++ komga-webui/src/components/PagesTable.vue | 69 +++++ komga-webui/src/components/RtlIcon.vue | 26 ++ .../dialogs/FileNameChooserDialog.vue | 134 +++++++++ .../components/dialogs/SeriesPickerDialog.vue | 127 +++++++++ .../dialogs/TransientBookDetailsDialog.vue | 117 ++++++++ .../dialogs/TransientBookViewerDialog.vue | 228 +++++++++++++++ komga-webui/src/functions/urls.ts | 4 + komga-webui/src/locales/en.json | 62 +++++ komga-webui/src/main.ts | 2 + .../plugins/komga-transientbooks.plugin.ts | 20 ++ komga-webui/src/router.ts | 6 + .../src/services/komga-books.service.ts | 16 +- .../services/komga-transientbooks.service.ts | 39 +++ komga-webui/src/types/enum-books.ts | 6 + komga-webui/src/types/komga-books.ts | 13 + komga-webui/src/types/komga-transientbooks.ts | 19 ++ komga-webui/src/views/BookImport.vue | 166 +++++++++++ komga-webui/src/views/Home.vue | 9 + 19 files changed, 1323 insertions(+), 2 deletions(-) create mode 100644 komga-webui/src/components/FileImportRow.vue create mode 100644 komga-webui/src/components/PagesTable.vue create mode 100644 komga-webui/src/components/RtlIcon.vue create mode 100644 komga-webui/src/components/dialogs/FileNameChooserDialog.vue create mode 100644 komga-webui/src/components/dialogs/SeriesPickerDialog.vue create mode 100644 komga-webui/src/components/dialogs/TransientBookDetailsDialog.vue create mode 100644 komga-webui/src/components/dialogs/TransientBookViewerDialog.vue create mode 100644 komga-webui/src/plugins/komga-transientbooks.plugin.ts create mode 100644 komga-webui/src/services/komga-transientbooks.service.ts create mode 100644 komga-webui/src/types/komga-transientbooks.ts create mode 100644 komga-webui/src/views/BookImport.vue diff --git a/komga-webui/src/components/FileImportRow.vue b/komga-webui/src/components/FileImportRow.vue new file mode 100644 index 000000000..85db4d905 --- /dev/null +++ b/komga-webui/src/components/FileImportRow.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/komga-webui/src/components/PagesTable.vue b/komga-webui/src/components/PagesTable.vue new file mode 100644 index 000000000..34f8dcf27 --- /dev/null +++ b/komga-webui/src/components/PagesTable.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/komga-webui/src/components/RtlIcon.vue b/komga-webui/src/components/RtlIcon.vue new file mode 100644 index 000000000..5df7d5122 --- /dev/null +++ b/komga-webui/src/components/RtlIcon.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/FileNameChooserDialog.vue b/komga-webui/src/components/dialogs/FileNameChooserDialog.vue new file mode 100644 index 000000000..de9e4453e --- /dev/null +++ b/komga-webui/src/components/dialogs/FileNameChooserDialog.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/SeriesPickerDialog.vue b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue new file mode 100644 index 000000000..21ed89a37 --- /dev/null +++ b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/TransientBookDetailsDialog.vue b/komga-webui/src/components/dialogs/TransientBookDetailsDialog.vue new file mode 100644 index 000000000..bd76887b9 --- /dev/null +++ b/komga-webui/src/components/dialogs/TransientBookDetailsDialog.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/TransientBookViewerDialog.vue b/komga-webui/src/components/dialogs/TransientBookViewerDialog.vue new file mode 100644 index 000000000..824d5d0d0 --- /dev/null +++ b/komga-webui/src/components/dialogs/TransientBookViewerDialog.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/komga-webui/src/functions/urls.ts b/komga-webui/src/functions/urls.ts index 849fc498f..1d4192ef0 100644 --- a/komga-webui/src/functions/urls.ts +++ b/komga-webui/src/functions/urls.ts @@ -47,3 +47,7 @@ export function collectionThumbnailUrl (collectionId: string): string { export function readListThumbnailUrl (readListId: string): string { return `${urls.originNoSlash}/api/v1/readlists/${readListId}/thumbnail` } + +export function transientBookPageUrl (transientBookId: string, page: number): string { + return `${urls.originNoSlash}/api/v1/transient-books/${transientBookId}/pages/${page}` +} diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index aafb92a01..770c27745 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -353,6 +353,44 @@ "shortcut_help": { "label_description": "Description", "label_key": "Key" + }, + "series_picker": { + "title": "Select Series", + "label_search_series": "Search Series" + }, + "filename_chooser": { + "title": "Destination File Name", + "label_source_filename": "Source File Name", + "field_destination_filename": "Destination file name", + "button_choose": "Choose", + "table": { + "order": "Order", + "existing_file": "Existing File" + } + }, + "transient_book_details": { + "title": "Book Details", + "title_comparison": "Book Comparison", + "label_candidate": "Candidate", + "label_existing": "Existing", + "label_name": "Name", + "label_size": "Size", + "label_format": "Format", + "label_pages": "Pages", + "pages_table": { + "index": "Index", + "filename": "File name", + "media_type": "Media type", + "width": "Width", + "height": "Height" + } + }, + "transient_book_viewer": { + "title": "Inspect Book", + "title_comparison": "Book Comparison", + "label_candidate": "Candidate", + "label_existing": "Existing", + "page_of_pages": "{page} / {pages}" } }, "enums": { @@ -374,6 +412,10 @@ "ENDED": "Ended", "HIATUS": "Hiatus", "ONGOING": "Ongoing" + }, + "copy_mode": { + "HARDLINK": "Hardlink/Copy Files", + "MOVE": "Move Files" } }, "error_codes": { @@ -519,5 +561,25 @@ "add_library": "Add library", "no_libraries_yet": "No libraries have been added yet!", "welcome_message": "Welcome to Komga" + }, + "book_import": { + "title": "Import", + "field_import_path": "Import from folder", + "button_browse": "Browse", + "button_scan": "Scan", + "table": { + "file_name": "File name", + "series": "Series", + "number": "Number", + "destination_name": "Destination name" + }, + "button_select_series": "Select Series", + "button_import": "Import", + "row": { + "warning_upgrade": "Existing book will be upgraded", + "error_analyze_first": "Book needs to be analyzed first", + "error_only_import_no_errors": "Can only import books without errors", + "error_choose_series": "Choose a series" + } } } diff --git a/komga-webui/src/main.ts b/komga-webui/src/main.ts index 967449d5d..98963f0f4 100644 --- a/komga-webui/src/main.ts +++ b/komga-webui/src/main.ts @@ -17,6 +17,7 @@ import komgaLibraries from './plugins/komga-libraries.plugin' import komgaReferential from './plugins/komga-referential.plugin' import komgaSeries from './plugins/komga-series.plugin' import komgaUsers from './plugins/komga-users.plugin' +import komgaTransientBooks from './plugins/komga-transientbooks.plugin' import vuetify from './plugins/vuetify' import './public-path' import router from './router' @@ -35,6 +36,7 @@ Vue.use(komgaReadLists, {http: Vue.prototype.$http}) Vue.use(komgaBooks, {http: Vue.prototype.$http}) Vue.use(komgaReferential, {http: Vue.prototype.$http}) Vue.use(komgaClaim, {http: Vue.prototype.$http}) +Vue.use(komgaTransientBooks, {http: Vue.prototype.$http}) Vue.use(komgaUsers, {store: store, http: Vue.prototype.$http}) Vue.use(komgaLibraries, {store: store, http: Vue.prototype.$http}) Vue.use(actuator, {http: Vue.prototype.$http}) diff --git a/komga-webui/src/plugins/komga-transientbooks.plugin.ts b/komga-webui/src/plugins/komga-transientbooks.plugin.ts new file mode 100644 index 000000000..231a2f411 --- /dev/null +++ b/komga-webui/src/plugins/komga-transientbooks.plugin.ts @@ -0,0 +1,20 @@ +import KomgaTransientBooksService from '@/services/komga-transientbooks.service' +import {AxiosInstance} from 'axios' +import _Vue from 'vue' + +let service: KomgaTransientBooksService + +export default { + install ( + Vue: typeof _Vue, + { http }: { http: AxiosInstance }) { + service = new KomgaTransientBooksService(http) + Vue.prototype.$komgaTransientBooks = service + }, +} + +declare module 'vue/types/vue' { + interface Vue { + $komgaTransientBooks: KomgaTransientBooksService; + } +} diff --git a/komga-webui/src/router.ts b/komga-webui/src/router.ts index 29373a691..03378c0d3 100644 --- a/komga-webui/src/router.ts +++ b/komga-webui/src/router.ts @@ -133,6 +133,12 @@ const router = new Router({ name: 'search', component: () => import(/* webpackChunkName: "search" */ './views/Search.vue'), }, + { + path: '/import', + name: 'import', + beforeEnter: adminGuard, + component: () => import(/* webpackChunkName: "book-import" */ './views/BookImport.vue'), + }, ], }, { diff --git a/komga-webui/src/services/komga-books.service.ts b/komga-webui/src/services/komga-books.service.ts index a47421754..e99042a72 100644 --- a/komga-webui/src/services/komga-books.service.ts +++ b/komga-webui/src/services/komga-books.service.ts @@ -1,5 +1,5 @@ -import { AxiosInstance } from 'axios' -import { BookDto, BookMetadataUpdateDto, PageDto, ReadProgressUpdateDto } from '@/types/komga-books' +import {AxiosInstance} from 'axios' +import {BookDto, BookImportBatchDto, BookMetadataUpdateDto, PageDto, ReadProgressUpdateDto} from '@/types/komga-books' const qs = require('qs') @@ -173,4 +173,16 @@ export default class KomgaBooksService { throw new Error(msg) } } + + async importBooks(batch: BookImportBatchDto) { + try { + await this.http.post(`${API_BOOKS}/import`, batch) + } catch (e) { + let msg = `An error occurred while trying to submit book import batch` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } } diff --git a/komga-webui/src/services/komga-transientbooks.service.ts b/komga-webui/src/services/komga-transientbooks.service.ts new file mode 100644 index 000000000..e9709048f --- /dev/null +++ b/komga-webui/src/services/komga-transientbooks.service.ts @@ -0,0 +1,39 @@ +import {AxiosInstance} from 'axios' +import {TransientBookDto} from "@/types/komga-transientbooks"; + +const API_TRANSIENT_BOOKS = '/api/v1/transient-books' + +export default class KomgaTransientBooksService { + private http: AxiosInstance + + constructor (http: AxiosInstance) { + this.http = http + } + + async scanForTransientBooks (path: string): Promise { + try { + return (await this.http.post(API_TRANSIENT_BOOKS, { + path: path, + })).data + } catch (e) { + let msg = `An error occurred while trying to scan for transient book` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async analyze (id: string): Promise { + try { + return (await this.http.post(`${API_TRANSIENT_BOOKS}/${id}/analyze`)).data + } catch (e) { + let msg = `An error occurred while trying to analyze transient book` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + +} diff --git a/komga-webui/src/types/enum-books.ts b/komga-webui/src/types/enum-books.ts index 74a7ddcfd..b509c53a1 100644 --- a/komga-webui/src/types/enum-books.ts +++ b/komga-webui/src/types/enum-books.ts @@ -18,3 +18,9 @@ export enum ReadStatus { IN_PROGRESS = 'IN_PROGRESS', READ = 'READ' } + +export enum CopyMode { + MOVE = 'MOVE', + COPY = 'COPY', + HARDLINK = 'HARDLINK', +} diff --git a/komga-webui/src/types/komga-books.ts b/komga-webui/src/types/komga-books.ts index d6aa0af3d..ea0398f4f 100644 --- a/komga-webui/src/types/komga-books.ts +++ b/komga-webui/src/types/komga-books.ts @@ -1,4 +1,5 @@ import {Context} from '@/types/context' +import {CopyMode} from "@/types/enum-books"; export interface BookDto { id: string, @@ -103,3 +104,15 @@ export interface BookFormat { type: string, color: string } + +export interface BookImportBatchDto{ + books: BookImportDto[], + copyMode: CopyMode, +} + +export interface BookImportDto { + sourceFile: string, + seriesId: string, + upgradeBookId?: string, + destinationName?: string, +} diff --git a/komga-webui/src/types/komga-transientbooks.ts b/komga-webui/src/types/komga-transientbooks.ts new file mode 100644 index 000000000..4780f8f5d --- /dev/null +++ b/komga-webui/src/types/komga-transientbooks.ts @@ -0,0 +1,19 @@ +import {PageDto} from "@/types/komga-books"; + +export interface ScanRequestDto { + path: string, +} + +export interface TransientBookDto { + id: string, + name: string, + url: string, + fileLastModified: string, + sizeBytes: number, + size: string, + status: string, + mediaType: string, + pages: PageDto[], + files: string[], + comment: string, +} diff --git a/komga-webui/src/views/BookImport.vue b/komga-webui/src/views/BookImport.vue new file mode 100644 index 000000000..22073f885 --- /dev/null +++ b/komga-webui/src/views/BookImport.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/komga-webui/src/views/Home.vue b/komga-webui/src/views/Home.vue index e2eaffef0..7018d5dde 100644 --- a/komga-webui/src/views/Home.vue +++ b/komga-webui/src/views/Home.vue @@ -69,6 +69,15 @@ + + + mdi-import + + + {{ $t('book_import.title') }} + + + mdi-cog From 237536e7be4f3236ca8e2efd6344530fd6436884 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 19 Apr 2021 17:25:46 +0800 Subject: [PATCH 11/39] refactor: use RtlIcon --- komga-webui/src/components/HorizontalScroller.vue | 10 +++++----- komga-webui/src/views/BrowseBook.vue | 13 +++++++------ komga-webui/src/views/BrowseSeries.vue | 5 +++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/komga-webui/src/components/HorizontalScroller.vue b/komga-webui/src/components/HorizontalScroller.vue index 3c7ea455d..2069e0fb2 100644 --- a/komga-webui/src/components/HorizontalScroller.vue +++ b/komga-webui/src/components/HorizontalScroller.vue @@ -7,14 +7,12 @@ - mdi-chevron-right - mdi-chevron-left + - mdi-chevron-left - mdi-chevron-right + @@ -31,9 +29,11 @@ diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt index 349a7fe7a..4bc466810 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt @@ -4,6 +4,8 @@ import org.gotson.komga.domain.model.BookPageContent import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException +import org.gotson.komga.domain.model.PathContainedInPath +import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.TransientBookRepository import org.springframework.stereotype.Service import java.nio.file.Paths @@ -13,10 +15,17 @@ class TransientBookLifecycle( private val transientBookRepository: TransientBookRepository, private val bookAnalyzer: BookAnalyzer, private val fileSystemScanner: FileSystemScanner, + private val libraryRepository: LibraryRepository, ) { fun scanAndPersist(filePath: String): List { - val books = fileSystemScanner.scanRootFolder(Paths.get(filePath)).values.flatten().map { BookWithMedia(it, Media()) } + val folderToScan = Paths.get(filePath) + + libraryRepository.findAll().forEach { library -> + if (folderToScan.startsWith(library.path())) throw PathContainedInPath("Cannot scan folder that is part of an existing library", "ERR_1017") + } + + val books = fileSystemScanner.scanRootFolder(folderToScan).values.flatten().map { BookWithMedia(it, Media()) } transientBookRepository.saveAll(books) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt index 312593eb7..f88ab239b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/TransientBooksController.kt @@ -3,7 +3,7 @@ package org.gotson.komga.interfaces.rest import com.jakewharton.byteunits.BinaryByteUnit import mu.KotlinLogging import org.gotson.komga.domain.model.BookWithMedia -import org.gotson.komga.domain.model.DirectoryNotFoundException +import org.gotson.komga.domain.model.CodedException import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.model.ROLE_ADMIN import org.gotson.komga.domain.persistence.TransientBookRepository @@ -43,7 +43,7 @@ class TransientBooksController( transientBookLifecycle.scanAndPersist(request.path) .sortedBy { it.book.path() } .map { it.toDto() } - } catch (e: DirectoryNotFoundException) { + } catch (e: CodedException) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.code) } From c8ffc15b763e0c85464ba423f43872fb304eb851 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Tue, 20 Apr 2021 16:01:16 +0800 Subject: [PATCH 19/39] refactor: add warning and info about book import feature --- komga-webui/src/locales/en.json | 5 ++++- komga-webui/src/views/BookImport.vue | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index d080a32d9..6d6b2f970 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -582,6 +582,9 @@ "error_analyze_first": "Book needs to be analyzed first", "error_only_import_no_errors": "Can only import books without errors", "error_choose_series": "Choose a series" - } + }, + "warning_early_feature": "Book Import is still an early feature, and must be used with caution. Make sure your source\n and destination files\n are backed up before using the Import feature.", + "info_part1": "This screen lets you import files that are outside your existing libraries. You can only import files into\n existing Series, in which case Komga will move or copy the files into the directory of the chosen Series.", + "info_part2": "If you choose a number for a book, and a book already exists with that number, then\n you will be able to compare the 2 books. If you decide to import the book, Komga will upgrade the existing book\n with the new one, effectively replacing the old file with the new." } } diff --git a/komga-webui/src/views/BookImport.vue b/komga-webui/src/views/BookImport.vue index c163d1f59..d8280d274 100644 --- a/komga-webui/src/views/BookImport.vue +++ b/komga-webui/src/views/BookImport.vue @@ -1,5 +1,12 @@