From 433bb4d9ae7fcee43008de03fc2e57c17f7e3f2f Mon Sep 17 00:00:00 2001 From: codebude Date: Mon, 8 Jun 2026 13:24:49 +0200 Subject: [PATCH 1/3] Add i18n translation files for es, fr and zh --- frontend/src/lib/i18n/locales/de.json | 5 +- frontend/src/lib/i18n/locales/en.json | 5 +- frontend/src/lib/i18n/locales/es.json | 650 ++++++++++++++++++++++++++ frontend/src/lib/i18n/locales/fr.json | 650 ++++++++++++++++++++++++++ frontend/src/lib/i18n/locales/zh.json | 650 ++++++++++++++++++++++++++ 5 files changed, 1958 insertions(+), 2 deletions(-) create mode 100644 frontend/src/lib/i18n/locales/es.json create mode 100644 frontend/src/lib/i18n/locales/fr.json create mode 100644 frontend/src/lib/i18n/locales/zh.json diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index 61befc0..66b8015 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -278,7 +278,10 @@ }, "languages": { "en": "Englisch", - "de": "Deutsch" + "de": "Deutsch", + "zh": "Chinesisch", + "es": "Spanisch", + "fr": "Französisch" }, "auth": { "login": "Anmelden", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index f091803..cd1162a 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -278,7 +278,10 @@ }, "languages": { "en": "English", - "de": "German" + "de": "German", + "zh": "Chinese", + "es": "Spanish", + "fr": "French" }, "auth": { "login": "Login", diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json new file mode 100644 index 0000000..d51595c --- /dev/null +++ b/frontend/src/lib/i18n/locales/es.json @@ -0,0 +1,650 @@ +{ + "app": { + "title": "LibrisLog", + "addBook": "Añadir libro", + "add": "Añadir", + "language": "Idioma" + }, + "nav": { + "dashboard": "Panel", + "library": "Biblioteca", + "timeline": "Cronología", + "statistics": "Estadísticas", + "data": "Datos", + "want_to_read": "Quiero leer", + "currently_reading": "Leyendo", + "read": "Leído", + "did_not_finish": "Abandonado" + }, + "statistics": { + "title": "Estadísticas", + "subtitle": "Información sobre tu viaje lector", + "avgBooksPerMonth": "Prom. libros/mes", + "busiestMonth": "Mes más activo", + "avgPageCount": "Prom. páginas/libro", + "mostPopularLanguage": "Idioma más frecuente", + "languageDistribution": "Libros por idioma", + "statusDistribution": "Libros por estado", + "pageBuckets": "Estadísticas de páginas", + "pagesToRead": "Páginas por leer", + "pagesRead": "Páginas leídas", + "pagesWasted": "Páginas desperdiciadas", + "pagesWastedFootnote": "\"Páginas desperdiciadas\" = página máxima alcanzada en libros marcados como \"No terminado\"", + "pagesReadPerMonth": "Páginas leídas por mes", + "booksFinishedPerMonth": "Libros terminados por mes", + "booksFinishedPerYear": "Libros terminados por año", + "topAuthors": "Autores destacados", + "rankedNumber": "#{rank}", + "coversForAuthor": "Portadas de {author}", + "booksCount": "{count} {count, plural, one {libro} other {libros}}", + "unknownLanguage": "Desconocido", + "pagesReadCalendar": "Actividad lectora (últimos 365 días)", + "noCalendarData": "No hay datos de lectura para el último año", + "pagesOver": "páginas en", + "daysLabel": "días", + "avgPerDay": "Prom. por día activo:", + "avgPerDayAll": "Prom. por día (365 días):", + "pagesPerDay": "páginas/día", + "loading": "Cargando estadísticas...", + "noData": "Aún no hay datos. ¡Empieza a leer y registrar libros para ver estadísticas!", + "resetZoom": "Restablecer zoom", + "sectionDistributions": "Distribuciones", + "sectionCharts": "Tendencias de lectura", + "sectionActivity": "Actividad", + "ratingStats": "Estadísticas de valoración", + "booksWithRating": "Libros valorados", + "booksWithoutRating": "Libros sin valorar", + "averageRating": "Valoración media", + "noRating": "Sin valoración", + "topRated": "Mejor valorados", + "worstRated": "Peor valorados", + "showMore": "Mostrar más" + }, + "dashboard": { + "title": "Panel de lectura", + "subtitle": "Una visión rápida de tu viaje lector", + "quoteTitle": "Cita del día", + "quoteUnavailable": "No hay ninguna cita disponible ahora.", + "totalBooks": "Total en biblioteca", + "booksRead": "Libros leídos", + "booksToRead": "Libros por leer", + "currentlyReading": "Leyendo ahora", + "nextToRead": "Siguiente lectura", + "viewAll": "Ver todo", + "searchAllBooks": "Buscar todos los libros", + "noSearchResults": "No se encontraron libros", + "noCurrentlyReading": "No estás leyendo ningún libro actualmente.", + "noNextToRead": "Aún no hay libros en tu lista de deseos.", + "popularTags": "Etiquetas populares" + }, + "status": { + "want_to_read": "Quiero leer", + "currently_reading": "Leyendo", + "read": "Leído", + "did_not_finish": "Abandonado" + }, + "common": { + "search": "Buscar", + "searchBooks": "Buscar libros...", + "result": "resultado", + "results": "resultados", + "save": "Guardar", + "saved": "Guardado", + "saveFailed": "Error al guardar", + "edit": "Editar", + "cancel": "Cancelar", + "confirm": "¿Confirmar?", + "delete": "Eliminar", + "deleting": "Eliminando...", + "back": "Volver", + "loadMore": "Cargar más", + "syncing": "Sincronizando...", + "noBooksYet": "Aún no hay libros aquí.", + "addFirstBook": "Añade tu primer libro", + "dateAdded": "Fecha de incorporación", + "rating": "Valoración", + "ratingSaved": "Valoración guardada", + "desc": "Desc.", + "asc": "Asc.", + "close": "Cerrar", + "clearForm": "Limpiar formulario", + "remove": "Quitar", + "copy": "Copiar", + "copied": "Copiado", + "showPassword": "Mostrar contraseña", + "required": "Obligatorio", + "saving": "Guardando...", + "loadingEllipsis": "...", + "starLabel": "{star} {star, plural, one {estrella} other {estrellas}}", + "clickToRate": "Haz clic en una estrella para valorar", + "actionFailed": "{action} falló", + "readMore": "Leer más", + "readLess": "Mostrar menos", + "serverStarting": "El servidor está iniciándose...", + "serverStartingDesc": "Por favor, espera mientras el servidor termina de arrancar." + }, + "book": { + "title": "Título", + "subtitle": "Subtítulo", + "author": "Autor", + "status": "Estado", + "isbn": "ISBN", + "publisher": "Editorial", + "year": "Año", + "pages": "Páginas", + "language": "Idioma", + "tags": "Etiquetas", + "tagsPlaceholder": "Escribe una etiqueta y pulsa Enter o coma", + "tagsHint": "Pulsa Enter o coma para añadir etiquetas. Backspace elimina la última.", + "notes": "Notas", + "blurb": "Descripción", + "about": "Acerca de", + "dateStarted": "Fecha de inicio", + "dateFinished": "Fecha de finalización", + "cover": "Portada", + "coverForAuthor": "Portada {index} de {author}", + "googleCovers": "Portadas de Google", + "autoSearchCovers": "Buscar portadas automáticamente", + "autoSearchInfo": "Haz clic en una portada para importarla como portada de este libro.", + "autoSearchNoCandidates": "No se encontraron candidatos para este ISBN.", + "autoSearchError": "Error al buscar portadas automáticamente.", + "autoSearchLoading": "Buscando fuentes de portadas...", + "autoSearchMetaUnknown": "Tamaño/resolución desconocidos", + "autoSearchMeta": "{size} - {resolution}", + "autoSearchSourceLabel": "Fuente: {source}", + "coverOf": "Portada de {title}", + "openDetailsHint": "Haz clic para ver detalles", + "readingProgress": "Progreso de lectura", + "currentPage": "Página", + "progressLog": "Registro de progreso", + "progressLogEmpty": "Aún no hay entradas de progreso.", + "setPageCountFirst": "Establece el total de páginas primero.", + "logDate": "Fecha", + "logPage": "Página", + "deleteEntry": "Eliminar", + "deleteEntryConfirm": "¿Eliminar esta entrada?", + "editEntry": "Editar", + "saveEntry": "Guardar", + "progressGraph": "Progreso en el tiempo", + "progressPromptTitle": "¿Establecer progreso de lectura?", + "progressPromptMessage": "¿Establecer el progreso de \"{title}\" al 100%?", + "progressPromptSet": "Establecer al 100%", + "progressPromptSkip": "Saltar" + }, + "addModal": { + "manual": "Manual", + "searchImport": "Buscar e importar", + "adding": "Añadiendo...", + "failedAdd": "Error al añadir libro", + "importFromFile": "Importar desde archivo" + }, + "import": { + "searchByTitleOrAuthor": "Buscar por título o autor...", + "enterIsbn": "Introduce el ISBN...", + "noResultsYet": "Aún sin resultados", + "noBooksFound": "No se encontraron libros", + "alreadyImported": "Ya importado", + "imported": "Importado", + "googleToo": "Buscar también en Google Books", + "googleSearching": "Buscando en Google Books...", + "googleAdded": "Resultados de Google Books añadidos: {count}", + "scan": "Escanear", + "scanIsbn": "Escanear código de barras ISBN", + "importFailed": "Importación fallida", + "searchFailed": "Búsqueda fallida", + "scannedIsbn": "ISBN escaneado: {isbn}", + "or": "o", + "sourceHardcoverSearching": "Buscando en Hardcover...", + "sourceHardcoverSkipped": "Hardcover omitido (sin token de API configurado)", + "sourceSkipped": "Google Books omitido (sin clave de API configurada)", + "sourceOpenLibrarySearching": "Buscando en Open Library...", + "sourceGoogleSearching": "Buscando en Google Books...", + "sourceBackendError": "Error de backend de {source} (revisa los registros del servidor)", + "sourceError": "Búsqueda fallida: {message}", + "resultCount": "{source} - {count} resultado{suffix}" + }, + "scanner": { + "title": "Escanear código de barras ISBN", + "help": "Apunta la cámara al código de barras de un libro. La búsqueda comienza automáticamente tras encontrar un ISBN válido.", + "startError": "No se pudo iniciar el escáner. Comprueba los permisos de la cámara.", + "noCamera": "No se encontró ningún dispositivo de cámara.", + "close": "Cerrar escáner" + }, + "coverPicker": { + "dropzone": "Arrastra una imagen aquí, o", + "browse": "examinar", + "pasteUrl": "O pega una URL de imagen...", + "useUrl": "Usar URL", + "urlInvalid": "No se pudo cargar la portada desde la URL. Verifica el enlace.", + "uploadFailed": "Subida fallida", + "previewAlt": "Vista previa de portada" + }, + "toasts": { + "dismiss": "Descartar", + "newVersion": "Una nueva versión ({version}) está disponible.", + "reload": "Recargar" + }, + "settings": { + "title": "Ajustes", + "languageTitle": "Idioma", + "timezone": "Zona horaria", + "timezoneHelp": "Mostrar fechas y horas en tu zona horaria local.", + "timezoneDetected": "Detectada: {tz}", + "timezoneSelected": "Seleccionada: {tz}", + "timezoneInvalid": "Selecciona una zona horaria válida de la lista.", + "themeTitle": "Tema", + "themeLight": "Claro", + "themeDark": "Oscuro", + "themeCustom": "Personalizar", + "themeSelect": "Selecciona un tema personalizado", + "timezonePlaceholder": "Buscar zona horaria...", + "apiDocsTitle": "Documentación de la API", + "apiDocsHelp": "Explora y prueba los endpoints del backend directamente desde la app.", + "apiDocsViewLabel": "Ver", + "apiDocsLoading": "Cargando documentación de la API", + "apiDocsFrameTitle": "Documentación de la API", + "apiDocsOpenNewTab": "Abrir documentación en una nueva pestaña" + }, + "sort": { + "smart": "Orden inteligente" + }, + "dateConflict": { + "started": { + "title": "Fecha de inicio ya establecida", + "message": "La fecha de inicio ya está establecida. ¿Quieres conservar {oldDate} o establecer {newDate} como nueva fecha de inicio?", + "keepOld": "Conservar {oldDate}", + "useNew": "Usar {newDate}" + }, + "finished": { + "title": "Fecha de finalización ya establecida", + "message": "La fecha de finalización ya está establecida. ¿Quieres conservar {oldDate} o establecer {newDate} como nueva fecha de finalización?", + "keepOld": "Conservar {oldDate}", + "useNew": "Usar {newDate}" + }, + "startedAfterFinished": { + "title": "El libro ya estaba terminado", + "message": "Este libro se terminó el {finishedDate}. ¿Qué deberíamos hacer?", + "keepFinished": "Conservar fecha de finalización", + "clearAndStart": "Borrar fecha de finalización y empezar hoy", + "keepDesc": "Conserva la fecha de finalización ({finishedDate}) y no establece una fecha de inicio.", + "clearDesc": "Elimina la fecha de finalización y establece hoy ({newStartDate}) como fecha de inicio." + } + }, + "search": { + "resultsCount": "{count, plural, one {resultado} other {resultados}} encontrados", + "noResults": "No se encontraron resultados", + "noResultsFor": "No se encontraron resultados para \"{query}\"", + "tryDifferentQuery": "Prueba con un término de búsqueda diferente" + }, + "languages": { + "en": "Inglés", + "de": "Alemán", + "zh": "Chino", + "es": "Español", + "fr": "Francés" + }, + "auth": { + "login": "Iniciar sesión", + "firstname": "Nombre", + "lastname": "Apellido", + "email": "Correo electrónico", + "password": "Contraseña", + "loginFailed": "Inicio de sesión fallido", + "setupTitle": "Crear cuenta de administrador", + "setupFailed": "Configuración fallida", + "createAdmin": "Crear administrador", + "invalidEmailError": "Introduce una dirección de correo electrónico válida", + "passwordComplexityError": "La contraseña no cumple los requisitos de complejidad" + }, + "user": { + "menu": "Menú de usuario", + "profile": "Perfil", + "about": "Acerca de", + "theme": "Tema", + "logout": "Cerrar sesión", + "apiKeys": "Claves de API", + "keyDescription": "Descripción (opcional)", + "addKey": "Añadir clave", + "newKeyShownOnce": "Copia esta clave ahora. Solo se muestra una vez", + "noDescription": "Sin descripción", + "newPassword": "Nueva contraseña" + }, + "admin": { + "title": "Administración", + "tabs": { + "users": "Usuarios", + "backup": "Copia de seguridad y restauración" + }, + "newUser": "Crear usuario", + "existingUsers": "Usuarios existentes", + "role": "Rol", + "create": "Crear", + "editing": "Editando usuario", + "edit": "Editar", + "saveChanges": "Guardar cambios", + "cancelEdit": "Cancelar edición", + "deleteConfirmTitle": "¿Realmente quieres eliminar este usuario?", + "deleteConfirmBody": "Esta acción no se puede deshacer.", + "cannotChangeOwnRole": "No puedes cambiar tu propio rol de administrador.", + "requiredFieldError": "Rellena todos los campos obligatorios.", + "selfDeleteHint": "Para eliminar tu propia cuenta, usa Perfil > Zona de peligro.", + "backup": { + "title": "Copia de seguridad", + "description": "Descarga una copia completa de tu biblioteca, incluyendo libros, portadas y datos.", + "download": "Descargar copia", + "success": "Copia descargada correctamente", + "failed": "Error al descargar la copia", + "inProgress": "Creando copia de seguridad..." + }, + "restore": { + "title": "Restaurar", + "description": "Restaura tu biblioteca desde un archivo de copia de seguridad anterior.", + "warning": "Advertencia: La restauración reemplazará TODOS los datos actuales. Asegúrate de tener una copia reciente antes de continuar.", + "upload": "Subir y restaurar", + "success": "Restauración completada. Se restauraron {books} libros.", + "failed": "Error al restaurar", + "inProgress": "Restaurando copia de seguridad...", + "validationFailed": "No se pudo validar el archivo de copia", + "invalidBackup": "Estructura de copia de seguridad no válida", + "confirmTitle": "Confirmar restauración", + "confirmBody": "¿Estás seguro de que quieres restaurar desde esta copia? Esto reemplazará todos los datos actuales y no se puede deshacer.", + "confirmWarning": "Esta acción es irreversible. Todos los datos actuales se perderán.", + "confirm": "Restaurar ahora", + "backupDate": "Fecha de copia", + "backupVersion": "Versión de la app", + "coversCount": "Portadas" + } + }, + "password": { + "requirementsTitle": "Requisitos de contraseña", + "minLength": "Al menos 8 caracteres", + "uppercase": "Al menos una mayúscula", + "lowercase": "Al menos una minúscula", + "number": "Al menos un número", + "special": "Al menos un carácter especial", + "strongEnough": "Suficientemente segura", + "notReady": "No cumple los requisitos" + }, + "error": { + "isbnAlreadyExists": "Este ISBN ya está siendo usado por otro libro.", + "dateInFuture": "La fecha no puede ser futura.", + "dateStartedAfterFinished": "La fecha de inicio no puede ser posterior a la de finalización.", + "dateFinishedRequiredForRead": "Un libro terminado debe tener una fecha de fin. Cambia el estado si quieres eliminar la fecha de finalización.", + "invalidLanguageCode": "El idioma debe ser un código ISO de 2 letras (por ejemplo: EN, DE, FR).", + "invalidConfirmationPhrase": "La frase de confirmación no coincide.", + "cannotDeleteLastAdmin": "No se puede eliminar la cuenta: eres el último administrador", + "cannotDeleteOwnAccountHere": "No puedes eliminar tu propia cuenta aquí. Usa Perfil > Zona de peligro.", + "importMalformedEvent": "Se recibió un evento de servidor malformado durante la importación.", + "importUnsupportedContentType": "Tipo de contenido no admitido. Usa archivos CSV o JSON.", + "emailAlreadyRegistered": "Esta dirección de correo ya está registrada.", + "userNotFound": "Usuario no encontrado.", + "cannotChangeOwnRole": "No puedes cambiar tu propio rol de administrador.", + "authorRequired": "El autor es obligatorio.", + "pageCountRequired": "El número de páginas es obligatorio.", + "importTempFileCreateFailed": "No se pudo crear el archivo temporal de importación. Inténtalo de nuevo.", + "fileTooLarge": "El archivo es demasiado grande. Prueba con un archivo más pequeño o revisa los límites del servidor.", + "exportNoDatasets": "Selecciona al menos un conjunto de datos para exportar.", + "batchUpdateFailed": "La actualización por lotes falló debido a un error inesperado. No se guardaron cambios.", + "tooManyBooksSelected": "Demasiados libros seleccionados. Selecciona como máximo {max} a la vez.", + "importMappingNameConflict": "Ya existe una asignación con este nombre.", + "importMappingNotFound": "Asignación de importación no encontrada.", + "importFileNotFound": "Archivo de importación no encontrado. Vuelve a subir el archivo." + }, + "oidc": { + "orContinueWith": "o continuar con", + "loginWithProvider": "Continuar con {provider}", + "profileTitle": "Inicio de sesión único", + "notLinked": "Tu cuenta aún no está vinculada.", + "linkButton": "Vincular cuenta de {provider}", + "unlinkButton": "Desvincular cuenta", + "linkedAs": "Vinculado con {provider}", + "linkSuccess": "Cuenta vinculada correctamente", + "linkStartFailed": "No se pudo iniciar la vinculación", + "unlinkSuccess": "Cuenta desvinculada", + "unlinkFailed": "No se pudo desvincular la cuenta", + "signingIn": "Iniciando sesión...", + "linkingAccount": "Vinculando cuenta..." + }, + "profile": { + "sectionNav": "En esta página", + "profileSaveSuccess": "Perfil guardado", + "profileSaveFailed": "Error al guardar el perfil", + "passwordChangeSuccess": "Contraseña cambiada", + "passwordChangeFailed": "Error al cambiar la contraseña", + "dataManagement": { + "title": "Gestionar mis datos", + "description": "Exporta tu biblioteca o importa libros desde un archivo CSV/JSON.", + "link": "Importar / Exportar", + "missingCoversDescription": "Asigna rápidamente portadas faltantes con sugerencias automáticas.", + "missingCoversLink": "Gestionar portadas faltantes" + }, + "dangerZone": { + "title": "Zona de peligro", + "subtitle": "Acciones irreversibles que eliminan permanentemente tus datos o cuenta.", + "resetData": { + "title": "Restablecer todos los datos personales", + "description": "Elimina todos tus libros, etiquetas y progreso de lectura, pero conserva tu cuenta y ajustes.", + "warning": "Esto no se puede deshacer.", + "placeholder": "Escribe la frase de confirmación", + "hint": "Escribe exactamente: DELETE ALL MY DATA", + "confirmationPhrase": "DELETE ALL MY DATA", + "button": "Restablecer todos los datos", + "success": "Se eliminaron {books} libros, {tags} etiquetas y {entries} entradas de progreso.", + "failed": "Error al restablecer datos" + }, + "deleteAccount": { + "title": "Eliminar cuenta", + "description": "Elimina tu cuenta y todos los datos asociados de forma permanente.", + "warning": "Esto es permanente y no se puede deshacer.", + "placeholder": "Escribe la frase de confirmación", + "hint": "Escribe exactamente: DELETE MY ACCOUNT", + "confirmationPhrase": "DELETE MY ACCOUNT", + "button": "Eliminar mi cuenta", + "success": "Cuenta eliminada. Redirigiendo al inicio de sesión...", + "failed": "Error al eliminar la cuenta", + "lastAdminError": "No se puede eliminar la cuenta: eres el último administrador" + } + } + }, + "timeline": { + "title": "Cronología de lectura", + "subtitle": "Una vista cronológica de los libros que has terminado", + "viewInLibrary": "Ver todo en la biblioteca", + "noReadBooks": "Aún no hay libros terminados en tu biblioteca.", + "goToLibrary": "Ir a la biblioteca" + }, + "data": { + "title": "Gestión de datos", + "subtitle": "Importa y exporta los datos de tu biblioteca personal", + "tabs": { + "export": "Exportar", + "import": "Importar" + }, + "export": { + "title": "Exportar", + "description": "Elige conjuntos de datos y formato, luego descarga un archivo ZIP.", + "datasets": { + "books": "Libros", + "progress": "Progreso de lectura", + "tags": "Etiquetas", + "covers": "Archivos de portada" + }, + "button": "Exportar datos", + "exporting": "Exportando...", + "success": "Exportación lista. Descarga iniciada.", + "errors": { + "noDatasets": "Selecciona al menos un conjunto de datos.", + "failed": "Exportación fallida." + } + }, + "import": { + "title": "Importar", + "description": "Sube un archivo CSV o JSON, asigna campos, valida y luego importa.", + "parse": "Analizar archivo", + "parsing": "Analizando...", + "fileSummary": "Filas: {rows}, campos: {fields}", + "mappingTitle": "Asignación de campos", + "mappingActionsTitle": "Gestionar asignaciones", + "mappingName": "Nombre de asignación", + "loadSavedMapping": "Asignaciones guardadas", + "noSavedMappings": "Aún no hay asignaciones guardadas. Guarda la asignación actual para reutilizarla después.", + "missingFieldsTitle": "Algunos campos de origen de la asignación guardada no están presentes en este archivo:", + "missingFieldEntry": "{target} ← {source}", + "selectMapping": "Seleccionar asignación guardada", + "loadMapping": "Cargar asignación", + "readonlyMapping": "solo lectura", + "deleteMapping": "Eliminar asignación", + "deleteMappingTitle": "Eliminar asignación guardada", + "showPreview": "Mostrar vista previa de asignación", + "createProgressForRead": "Crear entrada de progreso al 100% para libros importados como 'Leído'", + "hidePreview": "Ocultar vista previa de asignación", + "previewNoMappedFields": "Aún no hay campos asignados. Asigna campos de origen a campos de destino para previsualizar valores.", + "transformLabel": "Transformación (Python)", + "transformPlaceholder": "p. ej. value.upper()", + "previewTitle": "Vista previa", + "previewButton": "Generar", + "previewLoading": "Generando...", + "previewStale": "La vista previa está desactualizada", + "previewRow": "Fila {row}", + "errorRow": "Fila {row}", + "previewSource": "Origen", + "previewTransformed": "Transformado", + "none": "(ninguno)", + "requiredField": "= campo obligatorio", + "changeFile": "Cambiar archivo", + "coverUrlHint": "Espera una URL HTTP(S) a una imagen. No se admiten rutas de archivo locales ni datos base64.", + "transformHelp": "Parámetros disponibles y ejemplos", + "transformHelpValue": "El valor original del campo de origen asignado", + "transformHelpRow": "Todos los campos de origen como diccionario, p. ej. row['title']", + "transformHelpContext": "Diccionario de contexto con número de fila y total (context['row_num'], context['total_rows'])", + "transformHelpReturn": "Las expresiones simples se devuelven automáticamente; usa return explícito para código multilínea", + "transformHelpImports": "Importaciones de Python disponibles: datetime, re, json, math", + "transformError": "La regla de transformación para {field} no es válida: {error}", + "saveMapping": "Guardar asignación", + "refreshMappings": "Actualizar asignaciones", + "mappingSaved": "Asignación guardada", + "mappingDeleted": "Asignación eliminada", + "mappingMissingFields": "La asignación cargada tiene {count} campos de origen faltantes.", + "validationTitle": "Simulación", + "simulate": "Simular", + "validating": "Validando...", + "validationOk": "Validación superada.", + "validationNotOk": "La validación encontró problemas.", + "rollbackAll": "Revertir todo si hay error", + "continueOnError": "Continuar si hay error", + "importNow": "Importar ahora", + "importing": "Importando...", + "cancelled": "Importación cancelada.", + "confirmImportTitle": "¿Iniciar importación?", + "confirmDestructive": "Esto escribe datos en tu biblioteca y no se puede deshacer automáticamente.", + "deleteMappingConfirm": "¿Eliminar esta asignación guardada?", + "dropzone": "Arrastra y suelta un archivo CSV/JSON, o", + "browse": "examinar", + "fileInputLabel": "Elegir archivo CSV o JSON", + "showLess": "Mostrar menos", + "showAllIssues": "Mostrar todos los problemas ({count})", + "showAllFailures": "Mostrar todas las filas fallidas ({count})", + "completed": "Importación completada. Importados: {imported}, fallidos: {failed}", + "errors": { + "parseFailed": "Error al analizar el archivo.", + "saveMappingFailed": "Error al guardar la asignación.", + "deleteMappingFailed": "Error al eliminar la asignación.", + "loadMappingsFailed": "Error al cargar las asignaciones.", + "loadMappingFailed": "Error al cargar la asignación.", + "validateFailed": "Validación fallida.", + "previewFailed": "Error al cargar la vista previa.", + "executeFailed": "Importación fallida." + } + } + }, + "dataHygiene": { + "authorRequired": "El autor no puede estar vacío.", + "pageCountPositive": "El número de páginas debe ser mayor que 0.", + "title": "Higiene de datos", + "description": "Encuentra y corrige libros con metadatos faltantes en tu biblioteca.", + "attributes": { + "author": "Autor", + "isbn": "ISBN", + "publisher": "Editorial", + "published_year": "Año", + "blurb": "Descripción", + "language": "Idioma", + "subtitle": "Subtítulo", + "page_count": "Nº de páginas", + "cover_url": "Portada" + }, + "matchAny": "Coincidir cualquiera", + "matchAll": "Coincidir todos", + "noMissingBooks": "No se encontraron libros con atributos faltantes que coincidan con tus filtros.", + "total": "{count} {count, plural, one {libro} other {libros}} encontrados", + "loadMore": "Cargar más", + "loading": "Comprobando tu biblioteca...", + "selectAll": "Seleccionar todo", + "deselectAll": "Deseleccionar todo", + "nSelected": "{count} {count, plural, one {libro} other {libros}} seleccionados", + "batchEditTitle": "Edición por lotes", + "batchFieldLabel": "Campo a actualizar", + "batchFieldPlaceholder": "Selecciona un campo...", + "batchValueLabel": "Nuevo valor", + "batchValuePlaceholder": "Introduce el nuevo valor", + "applyBatch": "Aplicar a seleccionados", + "confirmTitle": "¿Actualizar {count} {count, plural, one {libro} other {libros}}?", + "confirmBody": "Esto establecerá \"{field}\" a \"{value}\" para los siguientes libros:", + "confirmApply": "Aplicar actualización", + "confirmCancel": "Cancelar", + "success": "{updated} {updated, plural, one {libro} other {libros}} actualizados. {skipped} ya tenían este valor.", + "updateFailed": "Error en la actualización por lotes.", + "loadFailed": "Error al cargar los datos.", + "tooManySelected": "Selecciona como máximo 500 libros a la vez.", + "noAttributeSelected": "Selecciona al menos un atributo para buscar.", + "noFieldSelected": "Selecciona un campo para actualizar.", + "noValueEntered": "Introduce un valor para establecer.", + "sectionFilters": "Filtros", + "sectionResults": "Resultados", + "showingCount": "Mostrando {shown} de {total} libros", + "allSet": "¡Tu biblioteca está en excelente estado! Todos los libros tienen metadatos completos.", + "allSetFiltered": "¡Tu biblioteca está en excelente estado! Todos los libros tienen metadatos completos para los atributos seleccionados.", + "tableHeaderMissing": "Faltante", + "remaining": "restantes", + "andXMore": "...y {count} más" + }, + "about": { + "title": "Acerca de LibrisLog", + "description": "Una aplicación web de seguimiento de libros para gestionar tus listas de lectura, importar libros desde fuentes en línea y realizar un seguimiento de tu progreso lector, todo a través de un panel moderno.", + "author": "Autor", + "version": "Versión", + "technologies": "Tecnologías utilizadas", + "thankYou": "Agradecimientos", + "thankYouText": "LibrisLog no existiría sin las increíbles bibliotecas y frameworks de código abierto sobre los que se construye. Nuestro agradecimiento a todos los desarrolladores que contribuyen a estos proyectos.", + "frontend": "Frontend", + "backend": "Backend", + "devTools": "Herramientas de desarrollo", + "documentation": "Documentación" + }, + "missingCovers": { + "title": "Gestionar portadas faltantes", + "header": "{count} {count, plural, one {libro} other {libros}} sin portada", + "bookInfo": "{title} de {author}", + "isbnLabel": "ISBN: {isbn}", + "noIsbn": "Este libro no tiene ISBN. La búsqueda automática de portadas no está disponible.", + "noCandidates": "No se pudo determinar ninguna portada automáticamente.", + "searchGoogle": "Buscar portada en Google", + "searchGoogleAria": "Abrir búsqueda de imágenes de Google para este libro en una nueva pestaña", + "manualUrlLabel": "O pega una URL de imagen de portada", + "manualUrlPlaceholder": "https://example.com/cover.jpg", + "manualUrlSave": "Guardar portada", + "manualUrlInvalid": "Introduce una URL HTTP(S) válida", + "manualUrlNotHttps": "Advertencia: la URL no es HTTPS. Prefiere HTTPS por seguridad.", + "skip": "Saltar", + "skipAria": "Saltar este libro e ir al siguiente", + "coverSaved": "Portada guardada", + "coverSaveFailed": "Error al guardar la portada", + "allDone": "¡Todos los libros tienen portada! Buen trabajo.", + "allDoneSub": "Todos los libros de tu biblioteca tienen ahora una imagen de portada.", + "loadingBook": "Cargando siguiente libro...", + "loadingCandidates": "Buscando fuentes de portadas...", + "keyboardHint": "Consejo: pulsa 1\u20139 para seleccionar una portada, \u2192 para saltar", + "candidatesError": "Búsqueda de portadas fallida. Aún puedes usar la importación manual.", + "retry": "Reintentar" + } +} diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json new file mode 100644 index 0000000..cfbc08c --- /dev/null +++ b/frontend/src/lib/i18n/locales/fr.json @@ -0,0 +1,650 @@ +{ + "app": { + "title": "LibrisLog", + "addBook": "Ajouter un livre", + "add": "Ajouter", + "language": "Langue" + }, + "nav": { + "dashboard": "Tableau de bord", + "library": "Bibliothèque", + "timeline": "Chronologie", + "statistics": "Statistiques", + "data": "Données", + "want_to_read": "À lire", + "currently_reading": "En cours", + "read": "Lu", + "did_not_finish": "Abandonné" + }, + "statistics": { + "title": "Statistiques", + "subtitle": "Aperçu de ton parcours de lecture", + "avgBooksPerMonth": "Moy. livres/mois", + "busiestMonth": "Mois le plus actif", + "avgPageCount": "Moy. pages/livre", + "mostPopularLanguage": "Langue la plus fréquente", + "languageDistribution": "Livres par langue", + "statusDistribution": "Livres par statut", + "pageBuckets": "Statistiques de pages", + "pagesToRead": "Pages à lire", + "pagesRead": "Pages lues", + "pagesWasted": "Pages gaspillées", + "pagesWastedFootnote": "\"Pages gaspillées\" = page maximale atteinte pour les livres marqués \"Abandonné\"", + "pagesReadPerMonth": "Pages lues par mois", + "booksFinishedPerMonth": "Livres terminés par mois", + "booksFinishedPerYear": "Livres terminés par an", + "topAuthors": "Auteurs populaires", + "rankedNumber": "#{rank}", + "coversForAuthor": "Couvertures de {author}", + "booksCount": "{count} {count, plural, one {livre} other {livres}}", + "unknownLanguage": "Inconnue", + "pagesReadCalendar": "Activité de lecture (365 derniers jours)", + "noCalendarData": "Aucune donnée de lecture pour l'année écoulée", + "pagesOver": "pages sur", + "daysLabel": "jours", + "avgPerDay": "Moy. par jour actif :", + "avgPerDayAll": "Moy. par jour (365 jours) :", + "pagesPerDay": "pages/jour", + "loading": "Chargement des statistiques...", + "noData": "Aucune donnée disponible. Commence à lire et à enregistrer des livres pour voir les statistiques !", + "resetZoom": "Réinitialiser le zoom", + "sectionDistributions": "Répartitions", + "sectionCharts": "Tendances de lecture", + "sectionActivity": "Activité", + "ratingStats": "Statistiques d'évaluation", + "booksWithRating": "Livres évalués", + "booksWithoutRating": "Livres non évalués", + "averageRating": "Note moyenne", + "noRating": "Aucune note", + "topRated": "Les mieux notés", + "worstRated": "Les moins bien notés", + "showMore": "Afficher plus" + }, + "dashboard": { + "title": "Tableau de bord de lecture", + "subtitle": "Un aperçu rapide de ton parcours de lecture", + "quoteTitle": "Citation du jour", + "quoteUnavailable": "Aucune citation disponible pour le moment.", + "totalBooks": "Total dans la bibliothèque", + "booksRead": "Livres lus", + "booksToRead": "Livres à lire", + "currentlyReading": "En cours de lecture", + "nextToRead": "À lire ensuite", + "viewAll": "Voir tout", + "searchAllBooks": "Rechercher dans tous les livres", + "noSearchResults": "Aucun livre trouvé", + "noCurrentlyReading": "Tu ne lis aucun livre actuellement.", + "noNextToRead": "Aucun livre dans ta liste d'envies.", + "popularTags": "Étiquettes populaires" + }, + "status": { + "want_to_read": "À lire", + "currently_reading": "En cours", + "read": "Lu", + "did_not_finish": "Abandonné" + }, + "common": { + "search": "Rechercher", + "searchBooks": "Rechercher des livres...", + "result": "résultat", + "results": "résultats", + "save": "Enregistrer", + "saved": "Enregistré", + "saveFailed": "Échec de l'enregistrement", + "edit": "Modifier", + "cancel": "Annuler", + "confirm": "Confirmer ?", + "delete": "Supprimer", + "deleting": "Suppression...", + "back": "Retour", + "loadMore": "Charger plus", + "syncing": "Synchronisation...", + "noBooksYet": "Aucun livre ici pour le moment.", + "addFirstBook": "Ajoute ton premier livre", + "dateAdded": "Date d'ajout", + "rating": "Note", + "ratingSaved": "Note enregistrée", + "desc": "Déc.", + "asc": "Asc.", + "close": "Fermer", + "clearForm": "Effacer le formulaire", + "remove": "Retirer", + "copy": "Copier", + "copied": "Copié", + "showPassword": "Afficher le mot de passe", + "required": "Requis", + "saving": "Enregistrement...", + "loadingEllipsis": "...", + "starLabel": "{star} {star, plural, one {étoile} other {étoiles}}", + "clickToRate": "Clique sur une étoile pour noter", + "actionFailed": "{action} a échoué", + "readMore": "Lire la suite", + "readLess": "Afficher moins", + "serverStarting": "Le serveur démarre...", + "serverStartingDesc": "Veuillez patienter pendant le démarrage du serveur." + }, + "book": { + "title": "Titre", + "subtitle": "Sous-titre", + "author": "Auteur", + "status": "Statut", + "isbn": "ISBN", + "publisher": "Éditeur", + "year": "Année", + "pages": "Pages", + "language": "Langue", + "tags": "Étiquettes", + "tagsPlaceholder": "Saisis une étiquette et appuie sur Entrée ou virgule", + "tagsHint": "Appuie sur Entrée ou virgule pour ajouter des étiquettes. Retour arrière supprime la dernière.", + "notes": "Notes", + "blurb": "Description", + "about": "À propos", + "dateStarted": "Date de début", + "dateFinished": "Date de fin", + "cover": "Couverture", + "coverForAuthor": "Couverture {index} de {author}", + "googleCovers": "Couvertures Google", + "autoSearchCovers": "Rechercher des couvertures", + "autoSearchInfo": "Clique sur une couverture pour l'importer comme couverture de ce livre.", + "autoSearchNoCandidates": "Aucune couverture trouvée pour cet ISBN.", + "autoSearchError": "Échec de la recherche automatique de couvertures.", + "autoSearchLoading": "Recherche de sources de couvertures...", + "autoSearchMetaUnknown": "Taille/résolution inconnue", + "autoSearchMeta": "{size} - {resolution}", + "autoSearchSourceLabel": "Source : {source}", + "coverOf": "Couverture de {title}", + "openDetailsHint": "Clique pour ouvrir les détails", + "readingProgress": "Progression de lecture", + "currentPage": "Page", + "progressLog": "Journal de progression", + "progressLogEmpty": "Aucune entrée de progression.", + "setPageCountFirst": "Définis d'abord le nombre total de pages.", + "logDate": "Date", + "logPage": "Page", + "deleteEntry": "Supprimer", + "deleteEntryConfirm": "Supprimer cette entrée ?", + "editEntry": "Modifier", + "saveEntry": "Enregistrer", + "progressGraph": "Progression dans le temps", + "progressPromptTitle": "Définir la progression ?", + "progressPromptMessage": "Définir la progression de \"{title}\" à 100 % ?", + "progressPromptSet": "Définir à 100 %", + "progressPromptSkip": "Passer" + }, + "addModal": { + "manual": "Manuel", + "searchImport": "Rechercher et importer", + "adding": "Ajout...", + "failedAdd": "Échec de l'ajout du livre", + "importFromFile": "Importer depuis un fichier" + }, + "import": { + "searchByTitleOrAuthor": "Rechercher par titre ou auteur...", + "enterIsbn": "Saisis l'ISBN...", + "noResultsYet": "Aucun résultat", + "noBooksFound": "Aucun livre trouvé", + "alreadyImported": "Déjà importé", + "imported": "Importé", + "googleToo": "Rechercher aussi sur Google Books", + "googleSearching": "Recherche sur Google Books...", + "googleAdded": "Résultats Google Books ajoutés : {count}", + "scan": "Scanner", + "scanIsbn": "Scanner le code-barres ISBN", + "importFailed": "Échec de l'importation", + "searchFailed": "Échec de la recherche", + "scannedIsbn": "ISBN scanné : {isbn}", + "or": "ou", + "sourceHardcoverSearching": "Recherche sur Hardcover...", + "sourceHardcoverSkipped": "Hardcover ignoré (aucun jeton API configuré)", + "sourceSkipped": "Google Books ignoré (aucune clé API configurée)", + "sourceOpenLibrarySearching": "Recherche sur Open Library...", + "sourceGoogleSearching": "Recherche sur Google Books...", + "sourceBackendError": "Erreur backend {source} (vérifie les journaux du serveur)", + "sourceError": "Échec de la recherche : {message}", + "resultCount": "{source} - {count} résultat{suffix}" + }, + "scanner": { + "title": "Scanner un code-barres ISBN", + "help": "Dirige la caméra vers le code-barres d'un livre. La recherche démarre automatiquement après avoir trouvé un ISBN valide.", + "startError": "Impossible de démarrer le scanner. Vérifie les autorisations de la caméra.", + "noCamera": "Aucun appareil photo trouvé.", + "close": "Fermer le scanner" + }, + "coverPicker": { + "dropzone": "Glisse et dépose une image ici, ou", + "browse": "parcourir", + "pasteUrl": "Ou colle une URL d'image...", + "useUrl": "Utiliser l'URL", + "urlInvalid": "Impossible de charger la couverture depuis l'URL. Vérifie le lien.", + "uploadFailed": "Échec du téléversement", + "previewAlt": "Aperçu de la couverture" + }, + "toasts": { + "dismiss": "Ignorer", + "newVersion": "Une nouvelle version ({version}) est disponible.", + "reload": "Recharger" + }, + "settings": { + "title": "Paramètres", + "languageTitle": "Langue", + "timezone": "Fuseau horaire", + "timezoneHelp": "Afficher les dates et heures dans ton fuseau horaire local.", + "timezoneDetected": "Détecté : {tz}", + "timezoneSelected": "Sélectionné : {tz}", + "timezoneInvalid": "Sélectionne un fuseau horaire valide dans la liste.", + "themeTitle": "Thème", + "themeLight": "Clair", + "themeDark": "Sombre", + "themeCustom": "Personnaliser", + "themeSelect": "Choisir un thème personnalisé", + "timezonePlaceholder": "Rechercher un fuseau horaire...", + "apiDocsTitle": "Documentation de l'API", + "apiDocsHelp": "Explore et teste les points d'accès du backend directement depuis l'application.", + "apiDocsViewLabel": "Voir", + "apiDocsLoading": "Chargement de la documentation", + "apiDocsFrameTitle": "Documentation de l'API", + "apiDocsOpenNewTab": "Ouvrir la documentation dans un nouvel onglet" + }, + "sort": { + "smart": "Tri intelligent" + }, + "dateConflict": { + "started": { + "title": "Date de début déjà définie", + "message": "La date de début est déjà définie. Veux-tu conserver {oldDate} ou définir {newDate} comme nouvelle date de début ?", + "keepOld": "Conserver {oldDate}", + "useNew": "Utiliser {newDate}" + }, + "finished": { + "title": "Date de fin déjà définie", + "message": "La date de fin est déjà définie. Veux-tu conserver {oldDate} ou définir {newDate} comme nouvelle date de fin ?", + "keepOld": "Conserver {oldDate}", + "useNew": "Utiliser {newDate}" + }, + "startedAfterFinished": { + "title": "Livre déjà terminé", + "message": "Ce livre a été terminé le {finishedDate}. Que devons-nous faire ?", + "keepFinished": "Garder la date de fin", + "clearAndStart": "Effacer la date de fin et commencer aujourd'hui", + "keepDesc": "Conserve la date de fin ({finishedDate}) et ne définit pas de date de début.", + "clearDesc": "Supprime la date de fin et définit aujourd'hui ({newStartDate}) comme date de début." + } + }, + "search": { + "resultsCount": "{count, plural, one {résultat} other {résultats}} trouvés", + "noResults": "Aucun résultat trouvé", + "noResultsFor": "Aucun résultat trouvé pour \"{query}\"", + "tryDifferentQuery": "Essaie un autre terme de recherche" + }, + "languages": { + "en": "Anglais", + "de": "Allemand", + "zh": "Chinois", + "es": "Espagnol", + "fr": "Français" + }, + "auth": { + "login": "Connexion", + "firstname": "Prénom", + "lastname": "Nom", + "email": "E-mail", + "password": "Mot de passe", + "loginFailed": "Échec de la connexion", + "setupTitle": "Créer un compte administrateur", + "setupFailed": "Échec de la configuration", + "createAdmin": "Créer un administrateur", + "invalidEmailError": "Veuillez saisir une adresse e-mail valide", + "passwordComplexityError": "Le mot de passe ne répond pas aux exigences de complexité" + }, + "user": { + "menu": "Menu utilisateur", + "profile": "Profil", + "about": "À propos", + "theme": "Thème", + "logout": "Déconnexion", + "apiKeys": "Clés API", + "keyDescription": "Description (facultative)", + "addKey": "Ajouter une clé", + "newKeyShownOnce": "Copie cette clé maintenant. Elle n'est affichée qu'une seule fois", + "noDescription": "Aucune description", + "newPassword": "Nouveau mot de passe" + }, + "admin": { + "title": "Administration", + "tabs": { + "users": "Utilisateurs", + "backup": "Sauvegarde et restauration" + }, + "newUser": "Créer un utilisateur", + "existingUsers": "Utilisateurs existants", + "role": "Rôle", + "create": "Créer", + "editing": "Modification de l'utilisateur", + "edit": "Modifier", + "saveChanges": "Enregistrer les modifications", + "cancelEdit": "Annuler la modification", + "deleteConfirmTitle": "Veux-tu vraiment supprimer cet utilisateur ?", + "deleteConfirmBody": "Cette action est irréversible.", + "cannotChangeOwnRole": "Tu ne peux pas modifier ton propre rôle d'administrateur.", + "requiredFieldError": "Veuillez remplir tous les champs obligatoires.", + "selfDeleteHint": "Pour supprimer ton propre compte, utilise Profil > Zone de danger.", + "backup": { + "title": "Sauvegarde", + "description": "Télécharge une sauvegarde complète de ta bibliothèque, y compris les livres, couvertures et données.", + "download": "Télécharger la sauvegarde", + "success": "Sauvegarde téléchargée avec succès", + "failed": "Échec du téléchargement de la sauvegarde", + "inProgress": "Création de la sauvegarde..." + }, + "restore": { + "title": "Restauration", + "description": "Restaure ta bibliothèque à partir d'un fichier de sauvegarde précédent.", + "warning": "Attention : la restauration remplacera TOUTES les données actuelles. Assure-toi d'avoir une sauvegarde récente avant de continuer.", + "upload": "Téléverser et restaurer", + "success": "Restauration réussie. {books} livres restaurés.", + "failed": "Échec de la restauration", + "inProgress": "Restauration de la sauvegarde...", + "validationFailed": "Impossible de valider le fichier de sauvegarde", + "invalidBackup": "Structure de sauvegarde invalide", + "confirmTitle": "Confirmer la restauration", + "confirmBody": "Es-tu sûr de vouloir restaurer à partir de cette sauvegarde ? Cela remplacera toutes les données actuelles et ne peut pas être annulé.", + "confirmWarning": "Cette action est irréversible. Toutes les données actuelles seront perdues.", + "confirm": "Restaurer maintenant", + "backupDate": "Date de sauvegarde", + "backupVersion": "Version de l'application", + "coversCount": "Couvertures" + } + }, + "password": { + "requirementsTitle": "Exigences du mot de passe", + "minLength": "Au moins 8 caractères", + "uppercase": "Au moins une lettre majuscule", + "lowercase": "Au moins une lettre minuscule", + "number": "Au moins un chiffre", + "special": "Au moins un caractère spécial", + "strongEnough": "Assez fort", + "notReady": "Pas encore prêt" + }, + "error": { + "isbnAlreadyExists": "Cet ISBN est déjà utilisé par un autre livre.", + "dateInFuture": "La date ne peut pas être dans le futur.", + "dateStartedAfterFinished": "La date de début ne peut pas être postérieure à la date de fin.", + "dateFinishedRequiredForRead": "Un livre terminé doit avoir une date de fin. Modifie le statut si tu veux supprimer la date de fin.", + "invalidLanguageCode": "La langue doit être un code ISO à 2 lettres (par exemple : EN, DE, FR).", + "invalidConfirmationPhrase": "La phrase de confirmation ne correspond pas.", + "cannotDeleteLastAdmin": "Impossible de supprimer le compte : tu es le dernier administrateur", + "cannotDeleteOwnAccountHere": "Tu ne peux pas supprimer ton propre compte ici. Utilise Profil > Zone de danger.", + "importMalformedEvent": "Événement serveur malformé reçu lors de l'importation.", + "importUnsupportedContentType": "Type de contenu non pris en charge. Utilise des fichiers CSV ou JSON.", + "emailAlreadyRegistered": "Cette adresse e-mail est déjà enregistrée.", + "userNotFound": "Utilisateur introuvable.", + "cannotChangeOwnRole": "Tu ne peux pas modifier ton propre rôle d'administrateur.", + "authorRequired": "L'auteur est obligatoire.", + "pageCountRequired": "Le nombre de pages est obligatoire.", + "importTempFileCreateFailed": "Impossible de créer le fichier temporaire d'importation. Veuillez réessayer.", + "fileTooLarge": "Le fichier est trop volumineux. Essaie un fichier plus petit ou vérifie les limites du serveur.", + "exportNoDatasets": "Sélectionne au moins un ensemble de données à exporter.", + "batchUpdateFailed": "La mise à jour par lot a échoué en raison d'une erreur inattendue. Aucune modification n'a été enregistrée.", + "tooManyBooksSelected": "Trop de livres sélectionnés. Sélectionne au maximum {max} à la fois.", + "importMappingNameConflict": "Un mappage avec ce nom existe déjà.", + "importMappingNotFound": "Mappage d'importation introuvable.", + "importFileNotFound": "Fichier d'importation introuvable. Veuillez téléverser le fichier à nouveau." + }, + "oidc": { + "orContinueWith": "ou continuer avec", + "loginWithProvider": "Continuer avec {provider}", + "profileTitle": "Authentification unique", + "notLinked": "Ton compte n'est pas encore lié.", + "linkButton": "Lier le compte {provider}", + "unlinkButton": "Délier le compte", + "linkedAs": "Lié avec {provider}", + "linkSuccess": "Compte lié avec succès", + "linkStartFailed": "Impossible de démarrer la liaison", + "unlinkSuccess": "Compte délié", + "unlinkFailed": "Impossible de délier le compte", + "signingIn": "Connexion en cours...", + "linkingAccount": "Liaison du compte..." + }, + "profile": { + "sectionNav": "Sur cette page", + "profileSaveSuccess": "Profil enregistré", + "profileSaveFailed": "Échec de l'enregistrement du profil", + "passwordChangeSuccess": "Mot de passe modifié", + "passwordChangeFailed": "Échec de la modification du mot de passe", + "dataManagement": { + "title": "Gérer mes données", + "description": "Exporte ta bibliothèque ou importe des livres depuis un fichier CSV/JSON.", + "link": "Importer / Exporter", + "missingCoversDescription": "Attribue rapidement les couvertures manquantes avec des suggestions automatiques.", + "missingCoversLink": "Gérer les couvertures manquantes" + }, + "dangerZone": { + "title": "Zone de danger", + "subtitle": "Actions irréversibles qui suppriment définitivement tes données ou ton compte.", + "resetData": { + "title": "Réinitialiser toutes les données personnelles", + "description": "Supprime tous tes livres, étiquettes et progression de lecture tout en conservant ton compte et tes paramètres.", + "warning": "Cette action est irréversible.", + "placeholder": "Saisis la phrase de confirmation", + "hint": "Saisis exactement : DELETE ALL MY DATA", + "confirmationPhrase": "DELETE ALL MY DATA", + "button": "Réinitialiser toutes les données", + "success": "{books} livres, {tags} étiquettes et {entries} entrées de progression supprimés.", + "failed": "Échec de la réinitialisation" + }, + "deleteAccount": { + "title": "Supprimer le compte", + "description": "Supprime définitivement ton compte et toutes les données associées.", + "warning": "Cette action est permanente et irréversible.", + "placeholder": "Saisis la phrase de confirmation", + "hint": "Saisis exactement : DELETE MY ACCOUNT", + "confirmationPhrase": "DELETE MY ACCOUNT", + "button": "Supprimer mon compte", + "success": "Compte supprimé. Redirection vers la connexion...", + "failed": "Échec de la suppression du compte", + "lastAdminError": "Impossible de supprimer le compte : tu es le dernier administrateur" + } + } + }, + "timeline": { + "title": "Chronologie de lecture", + "subtitle": "Une vue chronologique des livres que tu as terminés", + "viewInLibrary": "Voir tout dans la bibliothèque", + "noReadBooks": "Aucun livre terminé dans ta bibliothèque.", + "goToLibrary": "Aller à la bibliothèque" + }, + "data": { + "title": "Gestion des données", + "subtitle": "Importe et exporte les données de ta bibliothèque personnelle", + "tabs": { + "export": "Exporter", + "import": "Importer" + }, + "export": { + "title": "Exporter", + "description": "Choisis les ensembles de données et le format, puis télécharge une archive ZIP.", + "datasets": { + "books": "Livres", + "progress": "Progression de lecture", + "tags": "Étiquettes", + "covers": "Fichiers de couverture" + }, + "button": "Exporter les données", + "exporting": "Exportation...", + "success": "Export prêt. Téléchargement commencé.", + "errors": { + "noDatasets": "Sélectionne au moins un ensemble de données.", + "failed": "Échec de l'exportation." + } + }, + "import": { + "title": "Importer", + "description": "Téléverse un fichier CSV ou JSON, mappe les champs, valide, puis importe.", + "parse": "Analyser le fichier", + "parsing": "Analyse...", + "fileSummary": "Lignes : {rows}, champs : {fields}", + "mappingTitle": "Correspondance des champs", + "mappingActionsTitle": "Gérer les mappages", + "mappingName": "Nom du mappage", + "loadSavedMapping": "Mappages enregistrés", + "noSavedMappings": "Aucun mappage enregistré. Enregistre le mappage actuel pour le réutiliser plus tard.", + "missingFieldsTitle": "Certains champs source du mappage enregistré ne sont pas présents dans ce fichier :", + "missingFieldEntry": "{target} ← {source}", + "selectMapping": "Sélectionner un mappage", + "loadMapping": "Charger le mappage", + "readonlyMapping": "lecture seule", + "deleteMapping": "Supprimer le mappage", + "deleteMappingTitle": "Supprimer le mappage enregistré", + "showPreview": "Afficher l'aperçu du mappage", + "createProgressForRead": "Créer une entrée de progression à 100 % pour les livres importés comme 'Lu'", + "hidePreview": "Masquer l'aperçu du mappage", + "previewNoMappedFields": "Aucun champ mappé. Assigne des champs source aux champs cible pour prévisualiser les valeurs.", + "transformLabel": "Transformation (Python)", + "transformPlaceholder": "ex. value.upper()", + "previewTitle": "Aperçu", + "previewButton": "Générer", + "previewLoading": "Génération...", + "previewStale": "L'aperçu est obsolète", + "previewRow": "Ligne {row}", + "errorRow": "Ligne {row}", + "previewSource": "Source", + "previewTransformed": "Transformé", + "none": "(aucun)", + "requiredField": "= champ obligatoire", + "changeFile": "Changer de fichier", + "coverUrlHint": "Attend une URL HTTP(S) vers une image. Les chemins de fichiers locaux et les données base64 ne sont pas pris en charge.", + "transformHelp": "Paramètres disponibles et exemples", + "transformHelpValue": "La valeur brute du champ source mappé", + "transformHelpRow": "Tous les champs source sous forme de dict, ex. row['title']", + "transformHelpContext": "Dict de contexte avec numéro de ligne et total (context['row_num'], context['total_rows'])", + "transformHelpReturn": "Les expressions simples sont auto-renvoyées ; utilise return explicite pour le code multiligne", + "transformHelpImports": "Importations Python disponibles : datetime, re, json, math", + "transformError": "La règle de transformation pour {field} est invalide : {error}", + "saveMapping": "Enregistrer le mappage", + "refreshMappings": "Actualiser les mappages", + "mappingSaved": "Mappage enregistré", + "mappingDeleted": "Mappage supprimé", + "mappingMissingFields": "Le mappage chargé a {count} champs source manquants.", + "validationTitle": "Simulation", + "simulate": "Simuler", + "validating": "Validation...", + "validationOk": "Validation réussie.", + "validationNotOk": "La validation a trouvé des problèmes.", + "rollbackAll": "Tout annuler en cas d'erreur", + "continueOnError": "Continuer en cas d'erreur", + "importNow": "Importer maintenant", + "importing": "Importation...", + "cancelled": "Importation annulée.", + "confirmImportTitle": "Lancer l'importation ?", + "confirmDestructive": "Cela écrit des données dans ta bibliothèque et ne peut pas être annulé automatiquement.", + "deleteMappingConfirm": "Supprimer ce mappage enregistré ?", + "dropzone": "Glisse et dépose un fichier CSV/JSON, ou", + "browse": "parcourir", + "fileInputLabel": "Choisir un fichier CSV ou JSON", + "showLess": "Afficher moins", + "showAllIssues": "Afficher tous les problèmes ({count})", + "showAllFailures": "Afficher toutes les lignes échouées ({count})", + "completed": "Importation terminée. Importés : {imported}, échoués : {failed}", + "errors": { + "parseFailed": "Échec de l'analyse du fichier.", + "saveMappingFailed": "Échec de l'enregistrement du mappage.", + "deleteMappingFailed": "Échec de la suppression du mappage.", + "loadMappingsFailed": "Échec du chargement des mappages.", + "loadMappingFailed": "Échec du chargement du mappage.", + "validateFailed": "Échec de la validation.", + "previewFailed": "Échec du chargement de l'aperçu.", + "executeFailed": "Échec de l'importation." + } + } + }, + "dataHygiene": { + "authorRequired": "L'auteur ne peut pas être vide.", + "pageCountPositive": "Le nombre de pages doit être supérieur à 0.", + "title": "Hygiène des données", + "description": "Trouve et corrige les livres avec des métadonnées manquantes dans ta bibliothèque.", + "attributes": { + "author": "Auteur", + "isbn": "ISBN", + "publisher": "Éditeur", + "published_year": "Année", + "blurb": "Description", + "language": "Langue", + "subtitle": "Sous-titre", + "page_count": "Nombre de pages", + "cover_url": "Couverture" + }, + "matchAny": "Correspond à l'un", + "matchAll": "Correspond à tous", + "noMissingBooks": "Aucun livre avec des attributs manquants correspondant à tes filtres.", + "total": "{count} {count, plural, one {livre} other {livres}} trouvés", + "loadMore": "Charger plus", + "loading": "Vérification de ta bibliothèque...", + "selectAll": "Tout sélectionner", + "deselectAll": "Tout désélectionner", + "nSelected": "{count} {count, plural, one {livre} other {livres}} sélectionnés", + "batchEditTitle": "Édition par lot", + "batchFieldLabel": "Champ à mettre à jour", + "batchFieldPlaceholder": "Sélectionne un champ...", + "batchValueLabel": "Nouvelle valeur", + "batchValuePlaceholder": "Saisis la nouvelle valeur", + "applyBatch": "Appliquer aux sélectionnés", + "confirmTitle": "Mettre à jour {count} {count, plural, one {livre} other {livres}} ?", + "confirmBody": "Cela définira \"{field}\" à \"{value}\" pour les livres suivants :", + "confirmApply": "Appliquer la mise à jour", + "confirmCancel": "Annuler", + "success": "{updated} {updated, plural, one {livre} other {livres}} mis à jour. {skipped} avaient déjà cette valeur.", + "updateFailed": "Échec de la mise à jour par lot.", + "loadFailed": "Échec du chargement des données.", + "tooManySelected": "Sélectionne au maximum 500 livres à la fois.", + "noAttributeSelected": "Sélectionne au moins un attribut à rechercher.", + "noFieldSelected": "Sélectionne un champ à mettre à jour.", + "noValueEntered": "Saisis une valeur à définir.", + "sectionFilters": "Filtres", + "sectionResults": "Résultats", + "showingCount": "Affichage de {shown} sur {total} livres", + "allSet": "Ta bibliothèque est en pleine forme ! Tous les livres ont des métadonnées complètes.", + "allSetFiltered": "Ta bibliothèque est en pleine forme ! Tous les livres ont des métadonnées complètes pour les attributs sélectionnés.", + "tableHeaderMissing": "Manquant", + "remaining": "restants", + "andXMore": "...et {count} autres" + }, + "about": { + "title": "À propos de LibrisLog", + "description": "Une application web de suivi de livres pour gérer tes listes de lecture, importer des livres depuis des sources en ligne et suivre ta progression de lecture — le tout via un tableau de bord moderne.", + "author": "Auteur", + "version": "Version", + "technologies": "Technologies utilisées", + "thankYou": "Remerciements", + "thankYouText": "LibrisLog n'existerait pas sans les incroyables bibliothèques et frameworks open source sur lesquels il repose. Nos remerciements vont à tous les développeurs qui contribuent à ces projets.", + "frontend": "Frontend", + "backend": "Backend", + "devTools": "Outils de développement", + "documentation": "Documentation" + }, + "missingCovers": { + "title": "Gérer les couvertures manquantes", + "header": "{count} {count, plural, one {Livre} other {Livres}} sans couverture", + "bookInfo": "{title} de {author}", + "isbnLabel": "ISBN : {isbn}", + "noIsbn": "Ce livre n'a pas d'ISBN. La recherche automatique de couverture n'est pas disponible.", + "noCandidates": "Aucune couverture n'a pu être déterminée automatiquement.", + "searchGoogle": "Rechercher sur Google", + "searchGoogleAria": "Ouvrir la recherche d'images Google pour ce livre dans un nouvel onglet", + "manualUrlLabel": "Ou colle une URL d'image de couverture", + "manualUrlPlaceholder": "https://example.com/cover.jpg", + "manualUrlSave": "Enregistrer la couverture", + "manualUrlInvalid": "Veuillez saisir une URL HTTP(S) valide", + "manualUrlNotHttps": "Attention : l'URL n'est pas en HTTPS. Préfère HTTPS pour la sécurité.", + "skip": "Passer", + "skipAria": "Passer ce livre et passer au suivant", + "coverSaved": "Couverture enregistrée", + "coverSaveFailed": "Échec de l'enregistrement de la couverture", + "allDone": "Tous les livres ont une couverture ! Bon travail.", + "allDoneSub": "Chaque livre de ta bibliothèque a maintenant une image de couverture.", + "loadingBook": "Chargement du livre suivant...", + "loadingCandidates": "Recherche de sources de couvertures...", + "keyboardHint": "Astuce : appuie sur 1\u20139 pour sélectionner une couverture, \u2192 pour passer", + "candidatesError": "Recherche de couverture échouée. Tu peux toujours utiliser l'importation manuelle.", + "retry": "Réessayer" + } +} diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json new file mode 100644 index 0000000..6b3a880 --- /dev/null +++ b/frontend/src/lib/i18n/locales/zh.json @@ -0,0 +1,650 @@ +{ + "app": { + "title": "LibrisLog", + "addBook": "添加图书", + "add": "添加", + "language": "语言" + }, + "nav": { + "dashboard": "仪表盘", + "library": "书库", + "timeline": "时间线", + "statistics": "统计", + "data": "数据", + "want_to_read": "想读", + "currently_reading": "在读", + "read": "已读", + "did_not_finish": "弃读" + }, + "statistics": { + "title": "统计", + "subtitle": "你的阅读旅程数据分析", + "avgBooksPerMonth": "平均本数/月", + "busiestMonth": "最活跃月份", + "avgPageCount": "平均页数/本", + "mostPopularLanguage": "阅读最多的语言", + "languageDistribution": "按语言分布", + "statusDistribution": "按状态分布", + "pageBuckets": "页数统计", + "pagesToRead": "待读页数", + "pagesRead": "已读页数", + "pagesWasted": "浪费页数", + "pagesWastedFootnote": "\"浪费\" = 标记为\"弃读\"的图书中达到的最大页数", + "pagesReadPerMonth": "每月阅读页数", + "booksFinishedPerMonth": "每月读完数量", + "booksFinishedPerYear": "每年读完数量", + "topAuthors": "热门作者", + "rankedNumber": "#{rank}", + "coversForAuthor": "{author} 的图书封面", + "booksCount": "{count} 本书", + "unknownLanguage": "未知", + "pagesReadCalendar": "阅读活动(最近365天)", + "noCalendarData": "过去一年没有阅读数据", + "pagesOver": "页,共", + "daysLabel": "天", + "avgPerDay": "活跃日平均:", + "avgPerDayAll": "365天日均:", + "pagesPerDay": "页/天", + "loading": "正在加载统计...", + "noData": "暂无数据。开始阅读并记录图书以查看统计数据!", + "resetZoom": "重置缩放", + "sectionDistributions": "分布", + "sectionCharts": "阅读趋势", + "sectionActivity": "活动", + "ratingStats": "评分统计", + "booksWithRating": "已评分图书", + "booksWithoutRating": "未评分图书", + "averageRating": "平均评分", + "noRating": "暂无评分", + "topRated": "评分最高", + "worstRated": "评分最低", + "showMore": "显示更多" + }, + "dashboard": { + "title": "阅读仪表盘", + "subtitle": "你的阅读旅程概览", + "quoteTitle": "每日一言", + "quoteUnavailable": "暂无可用名言。", + "totalBooks": "书库总数", + "booksRead": "已读书籍", + "booksToRead": "待读书籍", + "currentlyReading": "正在阅读", + "nextToRead": "下一本想读", + "viewAll": "查看全部", + "searchAllBooks": "搜索所有图书", + "noSearchResults": "未找到图书", + "noCurrentlyReading": "你当前没有在读的图书。", + "noNextToRead": "你的想读列表还没有图书。", + "popularTags": "热门标签" + }, + "status": { + "want_to_read": "想读", + "currently_reading": "在读", + "read": "已读", + "did_not_finish": "弃读" + }, + "common": { + "search": "搜索", + "searchBooks": "搜索图书...", + "result": "个结果", + "results": "个结果", + "save": "保存", + "saved": "已保存", + "saveFailed": "保存失败", + "edit": "编辑", + "cancel": "取消", + "confirm": "确认?", + "delete": "删除", + "deleting": "删除中...", + "back": "返回", + "loadMore": "加载更多", + "syncing": "同步中...", + "noBooksYet": "这里还没有图书。", + "addFirstBook": "添加第一本书", + "dateAdded": "添加日期", + "rating": "评分", + "ratingSaved": "评分已保存", + "desc": "降序", + "asc": "升序", + "close": "关闭", + "clearForm": "清空表单", + "remove": "移除", + "copy": "复制", + "copied": "已复制", + "showPassword": "显示密码", + "required": "必填", + "saving": "保存中...", + "loadingEllipsis": "...", + "starLabel": "{star} 星", + "clickToRate": "点击星级进行评分", + "actionFailed": "{action} 失败", + "readMore": "展开", + "readLess": "收起", + "serverStarting": "服务器正在启动...", + "serverStartingDesc": "请等待服务器启动完成。" + }, + "book": { + "title": "标题", + "subtitle": "副标题", + "author": "作者", + "status": "状态", + "isbn": "ISBN", + "publisher": "出版社", + "year": "年份", + "pages": "页数", + "language": "语言", + "tags": "标签", + "tagsPlaceholder": "输入标签并按 Enter 或逗号", + "tagsHint": "按 Enter 或逗号添加标签。Backspace 删除最后一个标签。", + "notes": "笔记", + "blurb": "简介", + "about": "关于本书", + "dateStarted": "开始日期", + "dateFinished": "完成日期", + "cover": "封面", + "coverForAuthor": "{author} 的图书封面 {index}", + "googleCovers": "Google 封面", + "autoSearchCovers": "自动搜索封面", + "autoSearchInfo": "点击封面以将其导入为本书封面。", + "autoSearchNoCandidates": "未找到此 ISBN 的封面候选项。", + "autoSearchError": "自动搜索封面失败。", + "autoSearchLoading": "正在搜索封面来源...", + "autoSearchMetaUnknown": "未知大小/分辨率", + "autoSearchMeta": "{size} - {resolution}", + "autoSearchSourceLabel": "来源:{source}", + "coverOf": "{title} 的封面", + "openDetailsHint": "点击查看详情", + "readingProgress": "阅读进度", + "currentPage": "页", + "progressLog": "进度记录", + "progressLogEmpty": "暂无进度记录。", + "setPageCountFirst": "请先设置总页数。", + "logDate": "日期", + "logPage": "页码", + "deleteEntry": "删除", + "deleteEntryConfirm": "删除此记录?", + "editEntry": "编辑", + "saveEntry": "保存", + "progressGraph": "进度变化", + "progressPromptTitle": "设置阅读进度?", + "progressPromptMessage": "将 \"{title}\" 的阅读进度设置为 100%?", + "progressPromptSet": "设为 100%", + "progressPromptSkip": "跳过" + }, + "addModal": { + "manual": "手动添加", + "searchImport": "搜索并导入", + "adding": "添加中...", + "failedAdd": "添加图书失败", + "importFromFile": "从文件导入" + }, + "import": { + "searchByTitleOrAuthor": "按标题或作者搜索...", + "enterIsbn": "输入 ISBN...", + "noResultsYet": "暂无结果", + "noBooksFound": "未找到图书", + "alreadyImported": "已导入", + "imported": "已导入", + "googleToo": "同时搜索 Google Books", + "googleSearching": "正在搜索 Google Books...", + "googleAdded": "Google Books 结果已添加:{count}", + "scan": "扫描", + "scanIsbn": "扫描 ISBN 条码", + "importFailed": "导入失败", + "searchFailed": "搜索失败", + "scannedIsbn": "已扫描 ISBN:{isbn}", + "or": "或", + "sourceHardcoverSearching": "正在搜索 Hardcover...", + "sourceHardcoverSkipped": "Hardcover 已跳过(未配置 API 令牌)", + "sourceSkipped": "Google Books 已跳过(未配置 API 密钥)", + "sourceOpenLibrarySearching": "正在搜索 Open Library...", + "sourceGoogleSearching": "正在搜索 Google Books...", + "sourceBackendError": "{source} 后端错误(请检查后端日志)", + "sourceError": "搜索失败:{message}", + "resultCount": "{source} - {count} 个结果" + }, + "scanner": { + "title": "扫描 ISBN 条码", + "help": "将摄像头对准图书条码。识别到有效 ISBN 后自动开始搜索。", + "startError": "无法启动条码扫描器。请检查摄像头权限。", + "noCamera": "未找到摄像头设备。", + "close": "关闭扫描器" + }, + "coverPicker": { + "dropzone": "拖放图片到此处,或", + "browse": "浏览", + "pasteUrl": "或粘贴图片 URL...", + "useUrl": "使用 URL", + "urlInvalid": "无法从 URL 加载封面。请检查链接。", + "uploadFailed": "上传失败", + "previewAlt": "封面预览" + }, + "toasts": { + "dismiss": "关闭", + "newVersion": "新版本 ({version}) 已可用。", + "reload": "重新加载" + }, + "settings": { + "title": "设置", + "languageTitle": "语言", + "timezone": "时区", + "timezoneHelp": "以本地时区显示日期和时间。", + "timezoneDetected": "检测到:{tz}", + "timezoneSelected": "已选择:{tz}", + "timezoneInvalid": "请从列表中选择有效的时区。", + "themeTitle": "主题", + "themeLight": "浅色", + "themeDark": "深色", + "themeCustom": "自定义", + "themeSelect": "选择自定义主题", + "timezonePlaceholder": "搜索时区...", + "apiDocsTitle": "API 文档", + "apiDocsHelp": "直接在应用中探索和测试后端接口。", + "apiDocsViewLabel": "查看", + "apiDocsLoading": "正在加载 API 文档", + "apiDocsFrameTitle": "API 文档", + "apiDocsOpenNewTab": "在新标签页中打开 API 文档" + }, + "sort": { + "smart": "智能排序" + }, + "dateConflict": { + "started": { + "title": "开始日期已设置", + "message": "开始日期已设置。你想保留 {oldDate} 还是将 {newDate} 设为新的开始日期?", + "keepOld": "保留 {oldDate}", + "useNew": "使用 {newDate}" + }, + "finished": { + "title": "完成日期已设置", + "message": "完成日期已设置。你想保留 {oldDate} 还是将 {newDate} 设为新的完成日期?", + "keepOld": "保留 {oldDate}", + "useNew": "使用 {newDate}" + }, + "startedAfterFinished": { + "title": "本书已完成", + "message": "本书已于 {finishedDate} 完成。你想怎么做?", + "keepFinished": "保留完成日期", + "clearAndStart": "清除完成日期并从今天开始", + "keepDesc": "保留完成日期({finishedDate}),不设置开始日期。", + "clearDesc": "删除完成日期并将今天({newStartDate})设为开始日期。" + } + }, + "search": { + "resultsCount": "找到 {count} 个结果", + "noResults": "未找到结果", + "noResultsFor": "未找到 \"{query}\" 的结果", + "tryDifferentQuery": "尝试其他搜索词" + }, + "languages": { + "en": "英语", + "de": "德语", + "zh": "中文", + "es": "西班牙语", + "fr": "法语" + }, + "auth": { + "login": "登录", + "firstname": "名", + "lastname": "姓", + "email": "邮箱", + "password": "密码", + "loginFailed": "登录失败", + "setupTitle": "创建管理员账户", + "setupFailed": "设置失败", + "createAdmin": "创建管理员", + "invalidEmailError": "请输入有效的邮箱地址", + "passwordComplexityError": "密码未满足复杂度要求" + }, + "user": { + "menu": "用户菜单", + "profile": "个人资料", + "about": "关于", + "theme": "主题", + "logout": "退出登录", + "apiKeys": "API 密钥", + "keyDescription": "描述(可选)", + "addKey": "添加密钥", + "newKeyShownOnce": "立即复制此密钥。仅显示一次", + "noDescription": "无描述", + "newPassword": "新密码" + }, + "admin": { + "title": "管理", + "tabs": { + "users": "用户", + "backup": "备份与恢复" + }, + "newUser": "创建用户", + "existingUsers": "现有用户", + "role": "角色", + "create": "创建", + "editing": "正在编辑用户", + "edit": "编辑", + "saveChanges": "保存更改", + "cancelEdit": "取消编辑", + "deleteConfirmTitle": "确定要删除此用户吗?", + "deleteConfirmBody": "此操作无法撤消。", + "cannotChangeOwnRole": "你不能更改自己的管理员角色。", + "requiredFieldError": "请填写所有必填字段。", + "selfDeleteHint": "要删除自己的账户,请使用“个人资料” > “危险区域”。", + "backup": { + "title": "备份", + "description": "下载包含所有图书、封面和数据的完整备份。", + "download": "下载备份", + "success": "备份下载成功", + "failed": "备份下载失败", + "inProgress": "正在创建备份..." + }, + "restore": { + "title": "恢复", + "description": "从之前的备份文件恢复你的书库。", + "warning": "警告:恢复将替换所有当前数据。确保在继续之前有最近的备份。", + "upload": "上传并恢复", + "success": "恢复成功完成。已恢复 {books} 本书。", + "failed": "恢复失败", + "inProgress": "正在恢复备份...", + "validationFailed": "无法验证备份文件", + "invalidBackup": "无效的备份文件结构", + "confirmTitle": "确认恢复", + "confirmBody": "你确定要从此备份恢复吗?这将替换所有当前数据且无法撤消。", + "confirmWarning": "此操作不可撤消。所有当前数据将丢失。", + "confirm": "立即恢复", + "backupDate": "备份日期", + "backupVersion": "应用版本", + "coversCount": "封面" + } + }, + "password": { + "requirementsTitle": "密码要求", + "minLength": "至少 8 个字符", + "uppercase": "至少一个大写字母", + "lowercase": "至少一个小写字母", + "number": "至少一个数字", + "special": "至少一个特殊字符", + "strongEnough": "强度足够", + "notReady": "不满足要求" + }, + "error": { + "isbnAlreadyExists": "此 ISBN 已被另一本书使用。", + "dateInFuture": "日期不能为未来。", + "dateStartedAfterFinished": "开始日期不能晚于完成日期。", + "dateFinishedRequiredForRead": "已读完的书必须有结束日期。如果要移除结束日期,请更改状态。", + "invalidLanguageCode": "语言必须是 2 位 ISO 代码(例如:EN、DE、FR)。", + "invalidConfirmationPhrase": "确认短语不匹配。", + "cannotDeleteLastAdmin": "无法删除账户:你是最后一个管理员", + "cannotDeleteOwnAccountHere": "你不能在这里删除自己的账户。请使用“个人资料” > “危险区域”。", + "importMalformedEvent": "导入期间收到格式错误的服务器事件。", + "importUnsupportedContentType": "不支持的上传内容类型。请使用 CSV 或 JSON 文件。", + "emailAlreadyRegistered": "此邮箱地址已注册。", + "userNotFound": "未找到用户。", + "cannotChangeOwnRole": "你不能更改自己的管理员角色。", + "authorRequired": "作者为必填。", + "pageCountRequired": "页数为必填。", + "importTempFileCreateFailed": "无法创建临时导入文件。请重试。", + "fileTooLarge": "文件过大。请尝试更小的文件或检查服务器限制。", + "exportNoDatasets": "请至少选择一个数据集进行导出。", + "batchUpdateFailed": "批量更新因意外错误失败。未保存任何更改。", + "tooManyBooksSelected": "选择的图书过多。一次最多选择 {max} 本。", + "importMappingNameConflict": "同名映射已存在。", + "importMappingNotFound": "未找到导入映射。", + "importFileNotFound": "未找到导入文件。请重新上传文件。" + }, + "oidc": { + "orContinueWith": "或继续使用", + "loginWithProvider": "使用 {provider} 继续", + "profileTitle": "单点登录", + "notLinked": "你的账户尚未关联。", + "linkButton": "关联 {provider} 账户", + "unlinkButton": "取消关联", + "linkedAs": "已关联 {provider}", + "linkSuccess": "账户关联成功", + "linkStartFailed": "无法开始账户关联", + "unlinkSuccess": "已取消关联", + "unlinkFailed": "无法取消账户关联", + "signingIn": "正在登录...", + "linkingAccount": "正在关联账户..." + }, + "profile": { + "sectionNav": "本页内容", + "profileSaveSuccess": "个人资料已保存", + "profileSaveFailed": "保存个人资料失败", + "passwordChangeSuccess": "密码已更改", + "passwordChangeFailed": "更改密码失败", + "dataManagement": { + "title": "管理我的数据", + "description": "导出你的书库或从 CSV/JSON 文件导入图书。", + "link": "导入 / 导出", + "missingCoversDescription": "使用自动建议快速分配缺失封面。", + "missingCoversLink": "管理缺失封面" + }, + "dangerZone": { + "title": "危险区域", + "subtitle": "不可撤消的操作,将永久删除你的数据或账户。", + "resetData": { + "title": "重置所有个人数据", + "description": "删除所有图书、标签和阅读进度,但保留账户和设置。", + "warning": "此操作无法撤消。", + "placeholder": "输入确认短语", + "hint": "准确输入:DELETE ALL MY DATA", + "confirmationPhrase": "DELETE ALL MY DATA", + "button": "重置所有数据", + "success": "已删除 {books} 本书、{tags} 个标签和 {entries} 条进度记录。", + "failed": "重置数据失败" + }, + "deleteAccount": { + "title": "删除账户", + "description": "永久删除你的账户及所有关联数据。", + "warning": "此操作是永久性的,无法撤消。", + "placeholder": "输入确认短语", + "hint": "准确输入:DELETE MY ACCOUNT", + "confirmationPhrase": "DELETE MY ACCOUNT", + "button": "删除我的账户", + "success": "账户已删除。正在跳转到登录页面...", + "failed": "删除账户失败", + "lastAdminError": "无法删除账户:你是最后一个管理员" + } + } + }, + "timeline": { + "title": "阅读时间线", + "subtitle": "你已完成阅读的图书的时间线视图", + "viewInLibrary": "在书库中查看全部", + "noReadBooks": "你的书库中还没有已读完的图书。", + "goToLibrary": "前往书库" + }, + "data": { + "title": "数据管理", + "subtitle": "导入和导出你的个人书库数据", + "tabs": { + "export": "导出", + "import": "导入" + }, + "export": { + "title": "导出", + "description": "选择数据集和格式,然后下载 ZIP 归档。", + "datasets": { + "books": "图书", + "progress": "阅读进度", + "tags": "标签", + "covers": "封面文件" + }, + "button": "导出数据", + "exporting": "导出中...", + "success": "导出就绪。已开始下载。", + "errors": { + "noDatasets": "请至少选择一个数据集。", + "failed": "导出失败。" + } + }, + "import": { + "title": "导入", + "description": "上传 CSV 或 JSON 文件,映射字段,验证,然后导入。", + "parse": "解析文件", + "parsing": "解析中...", + "fileSummary": "行数:{rows},字段:{fields}", + "mappingTitle": "字段映射", + "mappingActionsTitle": "管理映射", + "mappingName": "映射名称", + "loadSavedMapping": "已保存的映射", + "noSavedMappings": "暂无已保存的映射。保存当前映射以在以后重复使用。", + "missingFieldsTitle": "已保存映射中的部分源字段在此文件中不存在:", + "missingFieldEntry": "{target} ← {source}", + "selectMapping": "选择已保存的映射", + "loadMapping": "加载映射", + "readonlyMapping": "只读", + "deleteMapping": "删除映射", + "deleteMappingTitle": "删除已保存的映射", + "showPreview": "显示映射预览", + "createProgressForRead": "为导入为“已读”的图书创建 100% 进度记录", + "hidePreview": "隐藏映射预览", + "previewNoMappedFields": "尚未映射字段。请将源字段分配给目标字段以预览值。", + "transformLabel": "转换 (Python)", + "transformPlaceholder": "例如:value.upper()", + "previewTitle": "预览", + "previewButton": "生成", + "previewLoading": "生成中...", + "previewStale": "预览已过期", + "previewRow": "第 {row} 行", + "errorRow": "第 {row} 行", + "previewSource": "来源", + "previewTransformed": "转换后", + "none": "(无)", + "requiredField": "= 必填字段", + "changeFile": "更改文件", + "coverUrlHint": "期望指向图片的 HTTP(S) URL。不支持本地文件路径和 base64 数据。", + "transformHelp": "可用参数和示例", + "transformHelpValue": "映射的源字段的原始值", + "transformHelpRow": "所有源字段作为字典,例如 row['title']", + "transformHelpContext": "包含行号和总行数的上下文字典 (context['row_num'], context['total_rows'])", + "transformHelpReturn": "单个表达式自动返回;多行代码请使用显式 return", + "transformHelpImports": "可用的 Python 导入:datetime, re, json, math", + "transformError": "{field} 的转换规则无效:{error}", + "saveMapping": "保存映射", + "refreshMappings": "刷新映射", + "mappingSaved": "映射已保存", + "mappingDeleted": "映射已删除", + "mappingMissingFields": "加载的映射有 {count} 个缺失的源字段。", + "validationTitle": "模拟", + "simulate": "模拟", + "validating": "验证中...", + "validationOk": "验证通过。", + "validationNotOk": "验证发现问题。", + "rollbackAll": "出错时全部回滚", + "continueOnError": "出错时继续", + "importNow": "立即导入", + "importing": "导入中...", + "cancelled": "导入已取消。", + "confirmImportTitle": "开始导入?", + "confirmDestructive": "这将向你的书库写入数据,无法自动撤消。", + "deleteMappingConfirm": "删除此已保存的映射?", + "dropzone": "拖放 CSV/JSON 文件,或", + "browse": "浏览", + "fileInputLabel": "选择 CSV 或 JSON 文件", + "showLess": "收起", + "showAllIssues": "显示所有问题 ({count})", + "showAllFailures": "显示所有失败行 ({count})", + "completed": "导入完成。已导入:{imported},失败:{failed}", + "errors": { + "parseFailed": "解析文件失败。", + "saveMappingFailed": "保存映射失败。", + "deleteMappingFailed": "删除映射失败。", + "loadMappingsFailed": "加载映射失败。", + "loadMappingFailed": "加载映射失败。", + "validateFailed": "验证失败。", + "previewFailed": "加载预览失败。", + "executeFailed": "导入失败。" + } + } + }, + "dataHygiene": { + "authorRequired": "作者不能为空。", + "pageCountPositive": "页数必须大于 0。", + "title": "数据清理", + "description": "查找并修复书库中缺少元数据的图书。", + "attributes": { + "author": "作者", + "isbn": "ISBN", + "publisher": "出版社", + "published_year": "年份", + "blurb": "简介", + "language": "语言", + "subtitle": "副标题", + "page_count": "页数", + "cover_url": "封面" + }, + "matchAny": "匹配任一", + "matchAll": "匹配全部", + "noMissingBooks": "未找到符合筛选条件的缺少属性的图书。", + "total": "找到 {count} 本书", + "loadMore": "加载更多", + "loading": "正在检查你的书库...", + "selectAll": "全选", + "deselectAll": "取消全选", + "nSelected": "已选 {count} 本书", + "batchEditTitle": "批量编辑", + "batchFieldLabel": "要更新的字段", + "batchFieldPlaceholder": "选择一个字段...", + "batchValueLabel": "新值", + "batchValuePlaceholder": "输入新值", + "applyBatch": "应用于所选", + "confirmTitle": "更新 {count} 本书?", + "confirmBody": "这会将下列图书的 \"{field}\" 设置为 \"{value}\":", + "confirmApply": "执行更新", + "confirmCancel": "取消", + "success": "已更新 {updated} 本书。{skipped} 本已为此值。", + "updateFailed": "批量更新失败。", + "loadFailed": "加载数据失败。", + "tooManySelected": "一次最多选择 500 本书。", + "noAttributeSelected": "请至少选择一个属性进行搜索。", + "noFieldSelected": "请选择一个要更新的字段。", + "noValueEntered": "请输入要设置的值。", + "sectionFilters": "筛选条件", + "sectionResults": "结果", + "showingCount": "显示 {shown} 本,共 {total} 本", + "allSet": "你的书库状态良好!所有图书元数据完整。", + "allSetFiltered": "你的书库状态良好!所选属性的所有图书元数据完整。", + "tableHeaderMissing": "缺失", + "remaining": "剩余", + "andXMore": "...还有 {count} 个" + }, + "about": { + "title": "关于 LibrisLog", + "description": "一款图书追踪 Web 应用,用于管理阅读列表、从在线来源导入图书以及追踪阅读进度——一切尽在现代仪表盘中。", + "author": "作者", + "version": "版本", + "technologies": "使用的技术", + "thankYou": "致谢", + "thankYouText": "LibrisLog 的诞生离不开其构建所依赖的优秀开源库和框架。我们感谢所有为这些项目做出贡献的开发者。", + "frontend": "前端", + "backend": "后端", + "devTools": "开发工具", + "documentation": "文档" + }, + "missingCovers": { + "title": "管理缺失封面", + "header": "{count} 本图书缺少封面", + "bookInfo": "{title} - {author}", + "isbnLabel": "ISBN:{isbn}", + "noIsbn": "本书没有 ISBN。无法自动搜索封面。", + "noCandidates": "无法自动确定封面。", + "searchGoogle": "在 Google 上搜索封面", + "searchGoogleAria": "在新标签页中打开此书的 Google 图片搜索", + "manualUrlLabel": "或粘贴封面图片 URL", + "manualUrlPlaceholder": "https://example.com/cover.jpg", + "manualUrlSave": "保存封面", + "manualUrlInvalid": "请输入有效的 HTTP(S) URL", + "manualUrlNotHttps": "警告:URL 不是 HTTPS。出于安全考虑,建议使用 HTTPS。", + "skip": "跳过", + "skipAria": "跳过此书并前往下一本", + "coverSaved": "封面已保存", + "coverSaveFailed": "保存封面失败", + "allDone": "所有图书都有封面了!做得好。", + "allDoneSub": "你书库中的每本书现在都有封面。", + "loadingBook": "正在加载下一本书...", + "loadingCandidates": "正在搜索封面来源...", + "keyboardHint": "提示:按 1\u20139 选择封面,按 \u2192 跳过", + "candidatesError": "封面搜索失败。你仍可使用手动导入。", + "retry": "重试" + } +} From 36abfaa54e38f41ce54c8d6fbeb26e6321bb8281 Mon Sep 17 00:00:00 2001 From: codebude Date: Mon, 8 Jun 2026 13:25:02 +0200 Subject: [PATCH 2/3] Load new languages into app --- frontend/src/lib/i18n/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/i18n/index.ts b/frontend/src/lib/i18n/index.ts index 151d1e3..b72c01e 100644 --- a/frontend/src/lib/i18n/index.ts +++ b/frontend/src/lib/i18n/index.ts @@ -1,7 +1,7 @@ import { addMessages, init, locale, register, waitLocale, _ } from 'svelte-i18n'; import { api } from '$lib/api'; -export const SUPPORTED_LOCALES = ['en', 'de'] as const; +export const SUPPORTED_LOCALES = ['en', 'de', 'zh', 'es', 'fr'] as const; export type AppLocale = (typeof SUPPORTED_LOCALES)[number]; const DEFAULT_LOCALE: AppLocale = 'en'; @@ -12,6 +12,9 @@ const configuredDefaultLocale: AppLocale = isSupportedLocale(envLocale) ? envLoc register('en', () => import('./locales/en.json')); register('de', () => import('./locales/de.json')); +register('zh', () => import('./locales/zh.json')); +register('es', () => import('./locales/es.json')); +register('fr', () => import('./locales/fr.json')); addMessages('en', {}); From 2694485a1aeec074ba4b8696a75ee5eb426244c0 Mon Sep 17 00:00:00 2001 From: codebude Date: Mon, 8 Jun 2026 13:25:14 +0200 Subject: [PATCH 3/3] Update readme and docs regarding new languages --- README.md | 2 +- docs/about.md | 2 +- docs/guide/configuration.md | 2 +- docs/guide/developer-setup.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3844daa..183db06 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Open **http://localhost:8001** and create your account. - **Cover art from multiple sources.** Automatic search across AbeBooks, Open Library, Amazon, and Hardcover — plus manual upload or URL paste. - **Full REST API.** OpenAPI-documented backend you can script against — build your own frontend, connect home automation, or pipe data into your own tools. - **Lightweight.** Two Docker containers, one SQLite database. -- **Bilingual UI.** English and German with a localization framework ready for more languages. +- **Multi-language UI.** English, German, Spanish, French, and Chinese (Simplified) — with a localization framework ready for more languages. --- diff --git a/docs/about.md b/docs/about.md index 91d168f..0b213aa 100644 --- a/docs/about.md +++ b/docs/about.md @@ -11,7 +11,7 @@ LibrisLog is a **multi-user book tracking web application** designed for readers - **Cover Management**: Automatic cover image scraping from multiple sources with manual override - **Data Portability**: Export/import library as JSON or CSV. Full backup and restore functionality - **REST API**: Full API with OpenAPI documentation for programmatic access -- **Multilingual**: English and German UI support +- **Multilingual**: English, German, Spanish, French, and Chinese (Simplified) UI support - **Themes**: Light, dark, and custom DaisyUI themes with persistent preferences ## Technology Stack diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 9851a3d..b5078d8 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -58,7 +58,7 @@ All configuration is done via environment variables in a `.env` file at the proj | Variable | Description | Default | |----------|-------------|---------| -| `PUBLIC_DEFAULT_LOCALE` | Default UI language (`en` or `de`) | `en` | +| `PUBLIC_DEFAULT_LOCALE` | Default UI language (`en`, `de`, `zh`, `es`, or `fr`) | `en` | ## Import Limits diff --git a/docs/guide/developer-setup.md b/docs/guide/developer-setup.md index 9c54588..2f7e2bb 100644 --- a/docs/guide/developer-setup.md +++ b/docs/guide/developer-setup.md @@ -20,7 +20,7 @@ When building, you can override these arguments: |----------|-------------|---------| | `APP_VERSION` | Application version string | `v0.0.0-dev` | | `GIT_SHA` | Git commit hash for version display | `unknown` | -| `PUBLIC_DEFAULT_LOCALE` | Default UI language (`en` or `de`) | `en` | +| `PUBLIC_DEFAULT_LOCALE` | Default UI language (`en`, `de`, `zh`, `es`, or `fr`) | `en` | Example: