From ce075de642d0cb1bd3cad31d32586c2ff43bdb04 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 6 Oct 2026 07:49:13 +0000 Subject: [PATCH 1/2] fix(i18n): translate missing literal and plural keys Co-Authored-By: Codex --- plans/2026-10-02-missing-translation-keys.md | 2 +- plans/README.md | 4 +- src/locales/en.json | 184 ++++++++++++++++--- src/locales/es.json | 184 ++++++++++++++++--- src/locales/hu.json | 184 ++++++++++++++++--- src/pages/AskPage.tsx | 2 +- src/pages/GeneratePage.tsx | 2 +- src/test/i18n-keys.test.ts | 52 ++++++ src/test/i18n.test.ts | 28 +++ 9 files changed, 562 insertions(+), 80 deletions(-) create mode 100644 src/test/i18n-keys.test.ts create mode 100644 src/test/i18n.test.ts diff --git a/plans/2026-10-02-missing-translation-keys.md b/plans/2026-10-02-missing-translation-keys.md index a043e0b95..93b5a4372 100644 --- a/plans/2026-10-02-missing-translation-keys.md +++ b/plans/2026-10-02-missing-translation-keys.md @@ -1,6 +1,6 @@ # Missing translation keys -**Status:** Backlog. Found on 2026-10-02 by the agentic Hungarian checks (`agentic/hungarian.e2e.ts`). Recheck the list against current source before starting. +**Status:** Shipped in `chore/i18n-missing-keys`. The source scan found 88 missing literal keys and 40 missing count forms. Two more keys separate source and case save failures from chat save failures. ## Problem diff --git a/plans/README.md b/plans/README.md index a0996ad14..2f44512ff 100644 --- a/plans/README.md +++ b/plans/README.md @@ -10,7 +10,6 @@ Every plan lives in this folder and opens with a `**Status:**` line; a plan list ## Backlog -- **Missing translation keys.** 88 literal `t()` keys are in no catalog, so Spanish and Hungarian users see English in toasts, dialogs and Library actions. Plan: [2026-10-02 missing translation keys](2026-10-02-missing-translation-keys.md). - **Next 15.** Security and data protection, operational safety, code health, and product polish, ordered by risk. Items 3, 5, 6, 8 and 13 have shipped; the plan lists the follow-ups they left. Plan: [2026-10-01 next 15](2026-10-01-next-15.md). Item 15 covers the PR #63 decision. - **Leaner CI and two previews per PR.** Three-minute PR check, a staging Vercel project for per-PR staging previews, and Edge Function CORS for preview hosts. Plan: [2026-10-01 leaner CI and previews](2026-10-01-ci-and-preview-verification.md). @@ -20,8 +19,9 @@ Every plan lives in this folder and opens with a `**Status:**` line; a plan list ## Shipped -These plans are finished. Most of their files were removed and git history keeps them. The three linked below are kept as the record, and each says `**Status:** Shipped`. +These plans are finished. Most of their files were removed and git history keeps them. The linked plans below are kept as the record, and each says `**Status:** Shipped`. +- Missing translation keys: 90 literal keys and 40 count forms added to each catalog. Plan: [2026-10-02 missing translation keys](2026-10-02-missing-translation-keys.md). - Voice Sparring and Crossfire: #84. Plan kept as the record: [2026-10-01 voice sparring and crossfire](2026-10-01-voice-sparring-crossfire.md). - Debate-aware AI and smaller modules: #75. Plan kept as the record: [2026-09-25 debate AI quality modules](2026-09-25_074255-debate-ai-quality-modules.md). - Current Debate foundation: #72. Plan kept as the record: [2026-09-25 current debate foundation](2026-09-25_074255-current-debate-foundation.md). diff --git a/src/locales/en.json b/src/locales/en.json index 431e9a2ca..b7b17be02 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -238,7 +238,13 @@ "back": "Back", "cancel": "Cancel", "delete": "Delete", - "save": "Save" + "save": "Save", + "clear": "Clear", + "copied": "Copied", + "copy": "Copy", + "deleted": "Deleted", + "discuss": "Discuss", + "retry": "Retry" }, "aiClient": { "inputShortened": "Input was shortened", @@ -311,7 +317,11 @@ "wordsPerMinute": "{{count}} words per minute", "fillers": "Filler words", "longPauses": "Pauses of {{seconds}} seconds or more", - "pausesWithLongest": "{{count}} (longest {{seconds}} s)" + "pausesWithLongest": "{{count}} (longest {{seconds}} s)", + "pausesWithLongest_one": "{{count}} (longest {{seconds}} s)", + "pausesWithLongest_other": "{{count}} (longest {{seconds}} s)", + "wordsPerMinute_one": "{{count}} word per minute", + "wordsPerMinute_other": "{{count}} words per minute" }, "scorecard": { "categoryNotesDescription": "Specific reasons behind every score. For speeches you spoke aloud, Delivery uses the measured facts below and the structure of what you said, not how you sounded.", @@ -361,24 +371,43 @@ "flowchartImportedDesc": "Your case has been imported to Flowcharts.", "flowchartImportFailed": "Import Failed", "flowchartImportFailedDesc": "Could not build flowchart from case data.", - "flowchartNoData": "No valid case data to import." + "flowchartNoData": "No valid case data to import.", + "caseTitle": "Case title", + "complete": "Generate complete", + "completeDesc": "Your debate case is ready.", + "noUndo": "Nothing to undo", + "regenerated": "Section regenerated", + "regeneratedDesc": "The selected section has been refreshed.", + "regenerateUnavailable": "Cannot regenerate yet", + "regenerateUnavailableDesc": "Generate a case first, then retry a section.", + "saveCase": "Save case", + "undone": "Reverted", + "undoneDesc": "Loaded previous generation.", + "saveFailedDesc": "The case could not be saved. Please try again." }, "attack": { "emptyTitle": "No Mercy Mode", "emptyDesc": "Paste your argument to receive ruthless criticism, identify logical fallacies, and get actionable improvements.", - "failed": "Attack Analysis Failed" + "failed": "Attack Analysis Failed", + "complete": "Attack analysis complete", + "completeDesc": "The critique is ready." }, "brainstorm": { "emptyTitle": "Unlock Creative Angles", "emptyDesc": "Describe your motion to explore unconventional argument angles, identify tradeoffs, and find your strongest positions.", - "failed": "Brainstorming Failed" + "failed": "Brainstorming Failed", + "complete": "Brainstorm complete", + "completeDesc": "Your argument angles are ready." }, "factoids": { "emptyTitle": "Evidence That Wins", "emptyDesc": "Enter a claim to find supporting factoids with real citations, context, and guidance on how to use them effectively.", "failed": "Research Failed", "savedToLibrary": "Saved to Library", - "savedToLibraryDesc": "This evidence is now available for grounding." + "savedToLibraryDesc": "This evidence is now available for grounding.", + "complete": "Research complete", + "completeDesc": "Verified evidence is ready.", + "saveFailed": "Could not save source." }, "ask": { "failed": "Error", @@ -389,7 +418,13 @@ "inputTooLargeTitle": "Message is too large", "inputTooLargeDesc": "Remove some attachment text or shorten your question before sending.", "streamInterrupted": "The streamed response was interrupted before completion.", - "streamInterruptedDesc": "You can ask again to get a complete answer." + "streamInterruptedDesc": "You can ask again to get a complete answer.", + "chatSaved": "Chat saved", + "chatSavedDesc": "You can load it from History.", + "retryableFailedDesc": "Couldn't reach the AI. Check your connection and retry.", + "saveFailed": "Save failed", + "saveFailedDesc": "The chat could not be saved. Please try again.", + "sourceSaveFailedDesc": "The source could not be saved. Please try again." }, "chat": { "error": "Chat Error", @@ -758,7 +793,11 @@ "deactivateAction": "Deactivate announcement", "deleteTitle": "Delete this announcement?", "deleteDescription": "This removes the message for every user and cannot be undone.", - "deleteAction": "Delete announcement" + "deleteAction": "Delete announcement", + "characterCount_one": "{{count}}/{{max}} character", + "characterCount_other": "{{count}}/{{max}} characters", + "selectedCount_one": "{{count}} selected", + "selectedCount_other": "{{count}} selected" } }, "notFound": { @@ -1062,7 +1101,10 @@ "exportedGdocs": "Case exported to Google Docs!", "open": "Open", "exportFailed": "Failed to export to Google Docs", - "exportError": "An unexpected error occurred during export" + "exportError": "An unexpected error occurred during export", + "copiedToClipboard": "Copied to clipboard", + "copyFailed": "Unable to copy to clipboard", + "undo": "Undo" }, "attackForm": { "title": "Attack My Case", @@ -1129,7 +1171,9 @@ "exportFailedPdf": "Failed to export to PDF", "open": "Open", "exportFailedGdocs": "Failed to export to Google Docs", - "exportError": "An unexpected error occurred during export" + "exportError": "An unexpected error occurred during export", + "weaknessesCount_one": "{{count}} issue found, sorted by severity", + "weaknessesCount_other": "{{count}} issues found, sorted by severity" }, "brainstormForm": { "title": "Brainstorm Ideas", @@ -1174,7 +1218,9 @@ "exportedGdocs": "Brainstorm exported to Google Docs!", "open": "Open", "exportFailedGdocs": "Failed to export to Google Docs", - "exportError": "An unexpected error occurred during export" + "exportError": "An unexpected error occurred during export", + "noIdeasDesc": "Try a more specific motion or switch the output format, then brainstorm again.", + "noIdeasTitle": "No ideas returned" }, "factoidsForm": { "title": "Find Evidence", @@ -1230,7 +1276,9 @@ "exportedGdocs": "Factoids exported to Google Docs!", "open": "Open", "exportFailedGdocs": "Failed to export to Google Docs", - "exportError": "An unexpected error occurred during export" + "exportError": "An unexpected error occurred during export", + "noVerified": "No verified matching sources were found. Try a broader source type or timeframe.", + "useSuggestion": "Search this" }, "askChat": { "title": "Ask Your Coach", @@ -1283,7 +1331,12 @@ "researching": "Researching...", "analyzing": "Analyzing...", "generatingResponse": "Generating response...", - "almostThere": "Almost there..." + "almostThere": "Almost there...", + "save": "Save", + "messageCount_one": "{{count}} message", + "messageCount_other": "{{count}} messages", + "savedCount_one": "{{count}} saved", + "savedCount_other": "{{count}} saved" }, "limitModal": { "trialEnded": "Free Trial Ended", @@ -1406,7 +1459,9 @@ "2nr": "2NR", "2ar": "2AR" } - } + }, + "disableSound": "Disable sound", + "enableSound": "Enable sound" }, "flowchart": { "title": "Argument Flowchart", @@ -1471,7 +1526,13 @@ "opponentAttackDesc": "How they might respond", "rebuttalLabel": "Your Rebuttal", "rebuttalDesc": "How you counter their attack" - } + }, + "autoLayout": "Auto layout", + "edgeAttack": "Attack", + "edgeRebuttal": "Rebuttal", + "edgeSupport": "Support", + "exportFailedToast": "Could not export flowchart.", + "selectedNode": "Selected" }, "historyPage": { "title": "History", @@ -1514,7 +1575,11 @@ "deleteCaseLabel": "Delete {{title}}", "load": "Load", "resume": "Resume", - "messages": "messages" + "messages": "messages", + "caseDeleted": "Saved case deleted.", + "deleteCaseDesc": "This removes the saved case from your history.", + "deleteCaseTitle": "Delete saved case?", + "deleteFailed": "Could not delete saved case." }, "libraryPage": { "title": "Source Library", @@ -1590,7 +1655,37 @@ "sourceAdded": "Source Added", "sourceAddedDesc": "Your source is now available for grounding.", "clearFailed": "Failed to clear sources", - "clearConfirm": "Are you sure you want to clear all sources?" + "clearConfirm": "Are you sure you want to clear all sources?", + "activateFolder": "Activate", + "clearSuccess": "Library cleared.", + "copyFailed": "Unable to copy source.", + "deactivateFolder": "Deactivate", + "deleteFolderDesc": "Sources in this folder will move to General. The sources themselves will not be deleted.", + "deleteFolderTitle": "Delete folder?", + "deleteSourceDesc": "This removes the source from your synced library.", + "deleteSourceTitle": "Delete source?", + "folderActions": "Folder actions", + "folderActionsDesc": "Rename, delete, or change grounding for every source in this folder.", + "folderActivated": "Folder activated", + "folderActivatedDesc": "All sources in this folder are active.", + "folderDeactivated": "Folder deactivated", + "folderDeactivatedDesc": "Sources stay saved but will not ground AI outputs.", + "folderDeleted": "Folder deleted", + "folderDeletedDesc": "Sources were moved to General.", + "folderMergeConfirm": "A folder with this name already exists. Merge sources into it?", + "folderRenamed": "Folder renamed", + "folderRenamedDesc": "Sources in this folder were moved to the new name.", + "noMatchingSources": "No matching sources", + "noMatchingSourcesDesc": "Try a different search or folder.", + "renameFolder": "Rename folder", + "sourceCopied": "Source copied to clipboard.", + "sourceDeleted": "Source removed.", + "sourceUpdated": "Source updated", + "sourceUpdatedDesc": "Your source changes were saved.", + "typeLabel": "Type", + "untitledSource": "Untitled source", + "useForGrounding": "Use for AI grounding", + "useForGroundingDesc": "Inactive sources stay saved but are ignored by generation." }, "autoSavedSources": { "undo": "Undo auto-saved sources", @@ -1599,7 +1694,9 @@ "removedPartialTitle": "Some sources removed", "removedPartialDesc": "{{succeeded}} removed, {{failed}} failed to remove.", "removeFailed": "Could not remove auto-saved sources.", - "removeFailedDesc": "{{succeeded}} sources were removed. {{failed}} sources could not be removed." + "removeFailedDesc": "{{succeeded}} sources were removed. {{failed}} sources could not be removed.", + "removedDesc_one": "{{count}} auto-saved source removed from your library.", + "removedDesc_other": "{{count}} auto-saved sources removed from your library." }, "sharedPage": { "title": "Shared Content", @@ -1780,7 +1877,11 @@ "sparringKeyMoments": "Key moments", "sparringImprovementAreas": "Work on next", "sparringScorecardUnavailable": "A final scorecard is not available yet.", - "sparringNoTranscript": "No speeches were submitted for this round." + "sparringNoTranscript": "No speeches were submitted for this round.", + "factoidsSummary_one": "{{count}} Fact • {{verified}} Verified", + "factoidsSummary_other": "{{count}} Facts • {{verified}} Verified", + "topIdeasCount_one": "{{count}} Top Idea", + "topIdeasCount_other": "{{count}} Top Ideas" }, "sparring": { "pro": "Pro", @@ -1856,7 +1957,15 @@ "startRound": "Start round", "draftNotice": "Your draft is saved in this tab as you type. You can pause without losing your place.", "offlineCannotStart": "Reconnect to start a round. Your setup is kept while you are offline.", - "speakingOrder": "Speaking order" + "speakingOrder": "Speaking order", + "aboutMinutes_one": "About {{count}} minute", + "aboutMinutes_other": "About {{count}} minutes", + "aiRequestBreakdown_one": "{{count}} reply + 1 judge", + "aiRequestBreakdown_other": "{{count}} replies + 1 judge", + "speeches_one": "{{count}} speech", + "speeches_other": "{{count}} speeches", + "turns_one": "{{count}} turn", + "turns_other": "{{count}} turns" }, "round": { "inProgress": "Round in progress", @@ -1911,7 +2020,13 @@ "timeUsed": "{{time}} used", "estimatedTimeUsed": "{{time}} estimated", "youArePro": "You are Pro", - "youAreCon": "You are Con" + "youAreCon": "You are Con", + "submittedCount_one": "{{count}} submitted", + "submittedCount_other": "{{count}} submitted", + "totalWords_one": "{{count}} word", + "totalWords_other": "{{count}} words", + "wordCount_one": "{{count}} word", + "wordCount_other": "{{count}} words" }, "scorecard": { "eyebrow": "Round review", @@ -1947,7 +2062,9 @@ "fullTranscript": "Full transcript", "transcriptDescription": "{{count}} speeches from this {{format}} round.", "hideTranscript": "Hide transcript", - "showTranscript": "Show transcript" + "showTranscript": "Show transcript", + "transcriptDescription_one": "{{count}} speech from this {{format}} round.", + "transcriptDescription_other": "{{count}} speeches from this {{format}} round." } }, "langSwitcher": { @@ -2096,7 +2213,9 @@ "strong": "Strong", "moderate": "Moderate", "weak": "Weak" - } + }, + "usageExplanation_one": "Used in {{count}} saved case. Importance: {{importance}}. Evidence strength: {{strength}}.", + "usageExplanation_other": "Used in {{count}} saved cases. Importance: {{importance}}. Evidence strength: {{strength}}." }, "debate": { "motion": "Motion", @@ -2195,7 +2314,9 @@ }, "offerTitle": "Start a debate with this motion?", "offerDesc": "A debate keeps this motion, its sources, and your results together, and every tool uses it.", - "offerNotNow": "Not now, use it once" + "offerNotNow": "Not now, use it once", + "itemCount_one": "{{count}} item", + "itemCount_other": "{{count}} items" }, "debateContext": { "using": "Using {{list}}", @@ -2244,6 +2365,19 @@ "lastUnavailable_other": "{{count}} items were no longer in this debate." }, "themeToggle": { - "toggle": "Toggle theme" + "toggle": "Toggle theme", + "current": "Current theme: {{theme}}. Click to cycle." + }, + "googleDriveAuth": { + "connect": "Connect Google Drive", + "description": "To export to Google Docs, DebateLab needs permission to create files in your Google Drive.", + "failedDesc": "Please try again.", + "failedTitle": "Could not open Google", + "redirecting": "Redirecting...", + "scopeNote": "We request the limited drive.file permission, not full Drive access.", + "title": "Connect Google Drive" + }, + "offlineIndicator": { + "label": "Offline mode" } } diff --git a/src/locales/es.json b/src/locales/es.json index c5b5b6a25..9b974def0 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -238,7 +238,13 @@ "back": "Atrás", "cancel": "Cancelar", "delete": "Eliminar", - "save": "Guardar" + "save": "Guardar", + "clear": "Borrar", + "copied": "Copiado", + "copy": "Copiar", + "deleted": "Eliminado", + "discuss": "Discutir", + "retry": "Reintentar" }, "aiClient": { "inputShortened": "La entrada se acortó", @@ -311,7 +317,11 @@ "wordsPerMinute": "{{count}} palabras por minuto", "fillers": "Muletillas", "longPauses": "Pausas de {{seconds}} segundos o más", - "pausesWithLongest": "{{count}} (la más larga, {{seconds}} s)" + "pausesWithLongest": "{{count}} (la más larga, {{seconds}} s)", + "pausesWithLongest_one": "{{count}} (la más larga, {{seconds}} s)", + "pausesWithLongest_other": "{{count}} (la más larga, {{seconds}} s)", + "wordsPerMinute_one": "{{count}} palabra por minuto", + "wordsPerMinute_other": "{{count}} palabras por minuto" }, "scorecard": { "categoryNotesDescription": "Motivos específicos detrás de cada puntuación. En los discursos que pronunciaste, la presentación usa los datos medidos de abajo y la estructura de lo que dijiste, no cómo sonaste.", @@ -361,24 +371,43 @@ "flowchartImportedDesc": "Tu caso se importó a Diagramas.", "flowchartImportFailed": "Error al importar", "flowchartImportFailedDesc": "No se pudo construir el diagrama con los datos del caso.", - "flowchartNoData": "No hay datos de caso válidos para importar." + "flowchartNoData": "No hay datos de caso válidos para importar.", + "caseTitle": "Título del caso", + "complete": "Caso generado", + "completeDesc": "Tu caso de debate está listo.", + "noUndo": "No hay nada que deshacer", + "regenerated": "Sección regenerada", + "regeneratedDesc": "La sección seleccionada se ha actualizado.", + "regenerateUnavailable": "Aún no se puede regenerar", + "regenerateUnavailableDesc": "Primero genera un caso y luego vuelve a intentar regenerar una sección.", + "saveCase": "Guardar caso", + "undone": "Cambio deshecho", + "undoneDesc": "Se cargó la generación anterior.", + "saveFailedDesc": "No se pudo guardar el caso. Inténtalo de nuevo." }, "attack": { "emptyTitle": "Modo Sin Piedad", "emptyDesc": "Pega tu argumento para recibir críticas despiadadas, identificar falacias lógicas y obtener mejoras accionables.", - "failed": "Error en el Análisis de Ataque" + "failed": "Error en el Análisis de Ataque", + "complete": "Análisis de ataque completado", + "completeDesc": "La crítica está lista." }, "brainstorm": { "emptyTitle": "Desbloquea Ángulos Creativos", "emptyDesc": "Describe tu moción para explorar ángulos de argumentación no convencionales, identificar compromisos y encontrar tus posiciones más fuertes.", - "failed": "Error en la Lluvia de Ideas" + "failed": "Error en la Lluvia de Ideas", + "complete": "Lluvia de ideas completada", + "completeDesc": "Tus enfoques de argumentación están listos." }, "factoids": { "emptyTitle": "Evidencia Que Gana", "emptyDesc": "Ingresa una afirmación para encontrar datos de apoyo con citas reales, contexto y guía sobre cómo usarlos efectivamente.", "failed": "Error en la Investigación", "savedToLibrary": "Guardado en Biblioteca", - "savedToLibraryDesc": "Esta evidencia ahora está disponible para fundamentación." + "savedToLibraryDesc": "Esta evidencia ahora está disponible para fundamentación.", + "complete": "Investigación completada", + "completeDesc": "La evidencia verificada está lista.", + "saveFailed": "No se pudo guardar la fuente." }, "ask": { "failed": "Error", @@ -389,7 +418,13 @@ "inputTooLargeTitle": "El mensaje es demasiado grande", "inputTooLargeDesc": "Elimina parte del texto adjunto o acorta la pregunta antes de enviarla.", "streamInterrupted": "La respuesta en streaming se interrumpió antes de completarse.", - "streamInterruptedDesc": "Puedes preguntar de nuevo para obtener una respuesta completa." + "streamInterruptedDesc": "Puedes preguntar de nuevo para obtener una respuesta completa.", + "chatSaved": "Chat guardado", + "chatSavedDesc": "Puedes abrirlo desde el Historial.", + "retryableFailedDesc": "No se pudo conectar con la IA. Comprueba tu conexión e inténtalo de nuevo.", + "saveFailed": "No se pudo guardar", + "saveFailedDesc": "No se pudo guardar el chat. Inténtalo de nuevo.", + "sourceSaveFailedDesc": "No se pudo guardar la fuente. Inténtalo de nuevo." }, "chat": { "error": "Error de Chat", @@ -758,7 +793,11 @@ "deactivateAction": "Desactivar anuncio", "deleteTitle": "¿Eliminar este anuncio?", "deleteDescription": "Esto elimina el mensaje para todos los usuarios y no se puede deshacer.", - "deleteAction": "Eliminar anuncio" + "deleteAction": "Eliminar anuncio", + "characterCount_one": "{{count}}/{{max}} carácter", + "characterCount_other": "{{count}}/{{max}} caracteres", + "selectedCount_one": "{{count}} seleccionado", + "selectedCount_other": "{{count}} seleccionados" } }, "notFound": { @@ -1062,7 +1101,10 @@ "exportedGdocs": "¡Caso exportado a Google Docs!", "open": "Abrir", "exportFailed": "Error al exportar a Google Docs", - "exportError": "Ocurrió un error inesperado durante la exportación" + "exportError": "Ocurrió un error inesperado durante la exportación", + "copiedToClipboard": "Copiado al portapapeles", + "copyFailed": "No se pudo copiar al portapapeles", + "undo": "Deshacer" }, "attackForm": { "title": "Atacar Mi Caso", @@ -1129,7 +1171,9 @@ "exportFailedPdf": "Error al exportar a PDF", "open": "Abrir", "exportFailedGdocs": "Error al exportar a Google Docs", - "exportError": "Ocurrió un error inesperado durante la exportación" + "exportError": "Ocurrió un error inesperado durante la exportación", + "weaknessesCount_one": "{{count}} problema encontrado, ordenado por gravedad", + "weaknessesCount_other": "{{count}} problemas encontrados, ordenados por gravedad" }, "brainstormForm": { "title": "Lluvia de Ideas", @@ -1174,7 +1218,9 @@ "exportedGdocs": "¡Lluvia de ideas exportada a Google Docs!", "open": "Abrir", "exportFailedGdocs": "Error al exportar a Google Docs", - "exportError": "Ocurrió un error inesperado durante la exportación" + "exportError": "Ocurrió un error inesperado durante la exportación", + "noIdeasDesc": "Prueba con una moción más concreta o cambia el formato de salida y vuelve a generar ideas.", + "noIdeasTitle": "No se generaron ideas" }, "factoidsForm": { "title": "Buscar Evidencia", @@ -1230,7 +1276,9 @@ "exportedGdocs": "¡Datos exportados a Google Docs!", "open": "Abrir", "exportFailedGdocs": "Error al exportar a Google Docs", - "exportError": "Ocurrió un error inesperado durante la exportación" + "exportError": "Ocurrió un error inesperado durante la exportación", + "noVerified": "No se encontraron fuentes verificadas que coincidan. Prueba con otro tipo de fuente o un período más amplio.", + "useSuggestion": "Buscar esto" }, "askChat": { "title": "Pregunta a Tu Coach", @@ -1283,7 +1331,12 @@ "noSavedChatsDesc": "Los chats de coaching guardados aparecerán aquí.", "messageCount": "{{count}} mensajes", "savedCount": "{{count}} guardados", - "removeFile": "Eliminar {{name}}" + "removeFile": "Eliminar {{name}}", + "save": "Guardar", + "messageCount_one": "{{count}} mensaje", + "messageCount_other": "{{count}} mensajes", + "savedCount_one": "{{count}} guardado", + "savedCount_other": "{{count}} guardados" }, "limitModal": { "trialEnded": "Prueba Gratuita Finalizada", @@ -1406,7 +1459,9 @@ "2nr": "2NR", "2ar": "2AR" } - } + }, + "disableSound": "Desactivar sonido", + "enableSound": "Activar sonido" }, "flowchart": { "title": "Diagrama de Argumentos", @@ -1471,7 +1526,13 @@ "opponentAttackDesc": "Cómo podría responder", "rebuttalLabel": "Tu refutación", "rebuttalDesc": "Cómo respondes a su ataque" - } + }, + "autoLayout": "Organizar automáticamente", + "edgeAttack": "Ataque", + "edgeRebuttal": "Refutación", + "edgeSupport": "Apoyo", + "exportFailedToast": "No se pudo exportar el diagrama.", + "selectedNode": "Seleccionado" }, "historyPage": { "title": "Historial", @@ -1514,7 +1575,11 @@ "deleteCaseLabel": "Eliminar {{title}}", "load": "Cargar", "resume": "Reanudar", - "messages": "mensajes" + "messages": "mensajes", + "caseDeleted": "Se eliminó el caso guardado.", + "deleteCaseDesc": "Esto eliminará el caso guardado de tu historial.", + "deleteCaseTitle": "¿Eliminar el caso guardado?", + "deleteFailed": "No se pudo eliminar el caso guardado." }, "libraryPage": { "title": "Biblioteca de Fuentes", @@ -1590,7 +1655,37 @@ "syncedFolders": "Carpetas sincronizadas", "manualSource": "Fuente manual", "searchSources": "Buscar fuentes", - "searchPlaceholder": "Buscar en fuentes, carpetas o contenido..." + "searchPlaceholder": "Buscar en fuentes, carpetas o contenido...", + "activateFolder": "Activar", + "clearSuccess": "Se borró la biblioteca.", + "copyFailed": "No se pudo copiar la fuente.", + "deactivateFolder": "Desactivar", + "deleteFolderDesc": "Las fuentes de esta carpeta se moverán a General. Las fuentes no se eliminarán.", + "deleteFolderTitle": "¿Eliminar carpeta?", + "deleteSourceDesc": "Esto eliminará la fuente de tu biblioteca sincronizada.", + "deleteSourceTitle": "¿Eliminar fuente?", + "folderActions": "Acciones de carpeta", + "folderActionsDesc": "Cambia el nombre, elimina la carpeta o ajusta el uso de todas sus fuentes para fundamentar las respuestas de la IA.", + "folderActivated": "Carpeta activada", + "folderActivatedDesc": "Todas las fuentes de esta carpeta están activas.", + "folderDeactivated": "Carpeta desactivada", + "folderDeactivatedDesc": "Las fuentes siguen guardadas, pero no se usarán para fundamentar las respuestas de la IA.", + "folderDeleted": "Carpeta eliminada", + "folderDeletedDesc": "Las fuentes se movieron a General.", + "folderMergeConfirm": "Ya existe una carpeta con ese nombre. ¿Quieres combinar las fuentes en ella?", + "folderRenamed": "Carpeta renombrada", + "folderRenamedDesc": "Las fuentes de esta carpeta se movieron a la carpeta con el nuevo nombre.", + "noMatchingSources": "No hay fuentes que coincidan", + "noMatchingSourcesDesc": "Prueba otra búsqueda o carpeta.", + "renameFolder": "Cambiar nombre de carpeta", + "sourceCopied": "Fuente copiada al portapapeles.", + "sourceDeleted": "Fuente eliminada.", + "sourceUpdated": "Fuente actualizada", + "sourceUpdatedDesc": "Se guardaron los cambios de la fuente.", + "typeLabel": "Tipo", + "untitledSource": "Fuente sin título", + "useForGrounding": "Usar para fundamentar respuestas de la IA", + "useForGroundingDesc": "Las fuentes inactivas siguen guardadas, pero no se usan al generar contenido." }, "autoSavedSources": { "undo": "Deshacer fuentes autoguardadas", @@ -1599,7 +1694,9 @@ "removedPartialTitle": "Algunas fuentes eliminadas", "removedPartialDesc": "{{succeeded}} eliminadas, {{failed}} no se pudieron eliminar.", "removeFailed": "No se pudieron eliminar las fuentes autoguardadas.", - "removeFailedDesc": "Se eliminaron {{succeeded}} fuentes. No se pudieron eliminar {{failed}} fuentes." + "removeFailedDesc": "Se eliminaron {{succeeded}} fuentes. No se pudieron eliminar {{failed}} fuentes.", + "removedDesc_one": "Se eliminó {{count}} fuente guardada automáticamente de tu biblioteca.", + "removedDesc_other": "Se eliminaron {{count}} fuentes guardadas automáticamente de tu biblioteca." }, "sharedPage": { "title": "Contenido Compartido", @@ -1780,7 +1877,11 @@ "sparringKeyMoments": "Momentos clave", "sparringImprovementAreas": "Aspectos a mejorar", "sparringScorecardUnavailable": "Aún no hay una tarjeta final de puntuación.", - "sparringNoTranscript": "No se enviaron intervenciones en esta ronda." + "sparringNoTranscript": "No se enviaron intervenciones en esta ronda.", + "factoidsSummary_one": "{{count}} dato • Verificados: {{verified}}", + "factoidsSummary_other": "{{count}} datos • Verificados: {{verified}}", + "topIdeasCount_one": "{{count}} idea destacada", + "topIdeasCount_other": "{{count}} ideas destacadas" }, "sparring": { "pro": "A favor", @@ -1856,7 +1957,15 @@ "startRound": "Iniciar ronda", "draftNotice": "Tu borrador se guarda en esta pestaña mientras escribes. Puedes pausar sin perder tu lugar.", "offlineCannotStart": "Vuelve a conectarte para iniciar una ronda. La configuración se conserva mientras estás sin conexión.", - "speakingOrder": "Orden de intervención" + "speakingOrder": "Orden de intervención", + "aboutMinutes_one": "Aproximadamente {{count}} minuto", + "aboutMinutes_other": "Aproximadamente {{count}} minutos", + "aiRequestBreakdown_one": "{{count}} respuesta + 1 juez", + "aiRequestBreakdown_other": "{{count}} respuestas + 1 juez", + "speeches_one": "{{count}} intervención", + "speeches_other": "{{count}} intervenciones", + "turns_one": "{{count}} turno", + "turns_other": "{{count}} turnos" }, "round": { "inProgress": "Ronda en curso", @@ -1911,7 +2020,13 @@ "timeUsed": "{{time}} usados", "estimatedTimeUsed": "{{time}} estimados", "youArePro": "Estás a favor", - "youAreCon": "Estás en contra" + "youAreCon": "Estás en contra", + "submittedCount_one": "{{count}} enviada", + "submittedCount_other": "{{count}} enviadas", + "totalWords_one": "{{count}} palabra", + "totalWords_other": "{{count}} palabras", + "wordCount_one": "{{count}} palabra", + "wordCount_other": "{{count}} palabras" }, "scorecard": { "eyebrow": "Revisión de la ronda", @@ -1947,7 +2062,9 @@ "fullTranscript": "Transcripción completa", "transcriptDescription": "{{count}} intervenciones de esta ronda {{format}}.", "hideTranscript": "Ocultar transcripción", - "showTranscript": "Mostrar transcripción" + "showTranscript": "Mostrar transcripción", + "transcriptDescription_one": "{{count}} intervención de esta ronda {{format}}.", + "transcriptDescription_other": "{{count}} intervenciones de esta ronda {{format}}." } }, "langSwitcher": { @@ -2096,7 +2213,9 @@ "strong": "Fuerte", "moderate": "Moderada", "weak": "Débil" - } + }, + "usageExplanation_one": "Usada en {{count}} caso guardado. Importancia: {{importance}}. Fuerza de la evidencia: {{strength}}.", + "usageExplanation_other": "Usada en {{count}} casos guardados. Importancia: {{importance}}. Fuerza de la evidencia: {{strength}}." }, "debate": { "motion": "Moción", @@ -2195,7 +2314,9 @@ }, "offerTitle": "¿Iniciar un debate con esta moción?", "offerDesc": "Un debate mantiene juntos la moción, sus fuentes y tus resultados, y todas las herramientas lo usan.", - "offerNotNow": "Ahora no, usarla una vez" + "offerNotNow": "Ahora no, usarla una vez", + "itemCount_one": "{{count}} elemento", + "itemCount_other": "{{count}} elementos" }, "debateContext": { "using": "Usando {{list}}", @@ -2244,6 +2365,19 @@ "lastUnavailable_other": "{{count}} elementos ya no estaban en este debate." }, "themeToggle": { - "toggle": "Cambiar tema" + "toggle": "Cambiar tema", + "current": "Tema actual: {{theme}}. Haz clic para cambiarlo." + }, + "googleDriveAuth": { + "connect": "Conectar Google Drive", + "description": "Para exportar a Google Docs, DebateLab necesita permiso para crear archivos en tu Google Drive.", + "failedDesc": "Inténtalo de nuevo.", + "failedTitle": "No se pudo abrir Google", + "redirecting": "Redirigiendo...", + "scopeNote": "Solo solicitamos el permiso limitado drive.file, no acceso completo a Drive.", + "title": "Conectar Google Drive" + }, + "offlineIndicator": { + "label": "Modo sin conexión" } } diff --git a/src/locales/hu.json b/src/locales/hu.json index bc4a1fa0b..8e52f1c13 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -238,7 +238,13 @@ "back": "Vissza", "cancel": "Mégse", "delete": "Törlés", - "save": "Mentés" + "save": "Mentés", + "clear": "Törlés", + "copied": "Másolva", + "copy": "Másolás", + "deleted": "Törölve", + "discuss": "Megvitatás", + "retry": "Újrapróbálás" }, "aiClient": { "inputShortened": "A bemenet rövidítve lett", @@ -311,7 +317,11 @@ "wordsPerMinute": "{{count}} szó percenként", "fillers": "Töltelékszavak", "longPauses": "Legalább {{seconds}} másodperces szünetek", - "pausesWithLongest": "{{count}} (a leghosszabb {{seconds}} mp)" + "pausesWithLongest": "{{count}} (a leghosszabb {{seconds}} mp)", + "pausesWithLongest_one": "{{count}} (a leghosszabb {{seconds}} mp)", + "pausesWithLongest_other": "{{count}} (a leghosszabb {{seconds}} mp)", + "wordsPerMinute_one": "{{count}} szó percenként", + "wordsPerMinute_other": "{{count}} szó percenként" }, "scorecard": { "categoryNotesDescription": "Minden pontszám mögött konkrét indokok állnak. A szóban elmondott beszédeknél az előadást a lenti mért adatok és az elhangzottak felépítése alapján értékeljük, nem az alapján, hogyan hangzottál.", @@ -361,24 +371,43 @@ "flowchartImportedDesc": "Az eseted átkerült a folyamatábrákhoz.", "flowchartImportFailed": "Import sikertelen", "flowchartImportFailedDesc": "Nem sikerült folyamatábrát építeni az eset adataiból.", - "flowchartNoData": "Nincs érvényes esetadat az importhoz." + "flowchartNoData": "Nincs érvényes esetadat az importhoz.", + "caseTitle": "Az eset címe", + "complete": "A generálás elkészült", + "completeDesc": "Elkészült a vitaeseted.", + "noUndo": "Nincs mit visszavonni", + "regenerated": "A szakasz újragenerálva", + "regeneratedDesc": "A kiválasztott szakasz frissült.", + "regenerateUnavailable": "Még nem lehet újragenerálni", + "regenerateUnavailableDesc": "Előbb generálj egy esetet, majd próbáld újra a szakaszt.", + "saveCase": "Eset mentése", + "undone": "Visszaállítva", + "undoneDesc": "Az előző generálás betöltve.", + "saveFailedDesc": "Nem sikerült menteni az esetet. Próbáld újra." }, "attack": { "emptyTitle": "Kegyetlen Mód", "emptyDesc": "Illeszd be az érvedet kíméletlen kritikáért, logikai hibák azonosításáért és megvalósítható javításokért.", - "failed": "Támadás Elemzés Sikertelen" + "failed": "Támadás Elemzés Sikertelen", + "complete": "A támadáselemzés elkészült", + "completeDesc": "Elkészült a kritika." }, "brainstorm": { "emptyTitle": "Kreatív Szögek Feloldása", "emptyDesc": "Írd le az indítványodat szokatlan érvelési szögek felfedezéséhez, kompromisszumok azonosításához és legerősebb pozícióid megtalálásához.", - "failed": "Ötletelés Sikertelen" + "failed": "Ötletelés Sikertelen", + "complete": "Az ötletelés elkészült", + "completeDesc": "Elkészültek az érvelési ötleteid." }, "factoids": { "emptyTitle": "Győztes Bizonyítékok", "emptyDesc": "Adj meg egy állítást támogató tények kereséséhez valós hivatkozásokkal, kontextussal és útmutatóval a hatékony felhasználáshoz.", "failed": "Kutatás Sikertelen", "savedToLibrary": "Mentve a Könyvtárba", - "savedToLibraryDesc": "Ez a bizonyíték mostantól elérhető megalapozáshoz." + "savedToLibraryDesc": "Ez a bizonyíték mostantól elérhető megalapozáshoz.", + "complete": "A kutatás elkészült", + "completeDesc": "Elkészültek az ellenőrzött bizonyítékok.", + "saveFailed": "Nem sikerült menteni a forrást." }, "ask": { "failed": "Hiba", @@ -389,7 +418,13 @@ "inputTooLargeTitle": "Az üzenet túl nagy", "inputTooLargeDesc": "Küldés előtt távolíts el néhány csatolt szöveget, vagy rövidítsd le a kérdést.", "streamInterrupted": "A streamelt válasz a befejezés előtt megszakadt.", - "streamInterruptedDesc": "Kérdezz újra a teljes válaszért." + "streamInterruptedDesc": "Kérdezz újra a teljes válaszért.", + "chatSaved": "Chat mentve", + "chatSavedDesc": "Az Előzményekből újra megnyithatod.", + "retryableFailedDesc": "Nem sikerült elérni az MI-t. Ellenőrizd a kapcsolatot, majd próbáld újra.", + "saveFailed": "A mentés sikertelen", + "saveFailedDesc": "Nem sikerült menteni a chatet. Próbáld újra.", + "sourceSaveFailedDesc": "Nem sikerült menteni a forrást. Próbáld újra." }, "chat": { "error": "Chat Hiba", @@ -758,7 +793,11 @@ "deactivateAction": "Közlemény kikapcsolása", "deleteTitle": "Törlöd ezt a közleményt?", "deleteDescription": "Ez minden felhasználónál törli az üzenetet, és nem vonható vissza.", - "deleteAction": "Közlemény törlése" + "deleteAction": "Közlemény törlése", + "characterCount_one": "{{count}}/{{max}} karakter", + "characterCount_other": "{{count}}/{{max}} karakter", + "selectedCount_one": "{{count}} kiválasztva", + "selectedCount_other": "{{count}} kiválasztva" } }, "notFound": { @@ -1062,7 +1101,10 @@ "exportedGdocs": "Eset exportálva Google Docs-ba!", "open": "Megnyitás", "exportFailed": "Nem sikerült exportálni Google Docs-ba", - "exportError": "Váratlan hiba történt az exportálás során" + "exportError": "Váratlan hiba történt az exportálás során", + "copiedToClipboard": "Vágólapra másolva", + "copyFailed": "Nem sikerült a vágólapra másolni", + "undo": "Visszavonás" }, "attackForm": { "title": "Támadás az Esetem Ellen", @@ -1129,7 +1171,9 @@ "exportFailedPdf": "Nem sikerült exportálni PDF-be", "open": "Megnyitás", "exportFailedGdocs": "Nem sikerült exportálni Google Docs-ba", - "exportError": "Váratlan hiba történt az exportálás során" + "exportError": "Váratlan hiba történt az exportálás során", + "weaknessesCount_one": "{{count}} probléma található, súlyosság szerint rendezve", + "weaknessesCount_other": "{{count}} probléma található, súlyosság szerint rendezve" }, "brainstormForm": { "title": "Ötletelés", @@ -1174,7 +1218,9 @@ "exportedGdocs": "Ötletelés exportálva Google Docs-ba!", "open": "Megnyitás", "exportFailedGdocs": "Nem sikerült exportálni Google Docs-ba", - "exportError": "Váratlan hiba történt az exportálás során" + "exportError": "Váratlan hiba történt az exportálás során", + "noIdeasDesc": "Próbálj pontosabb tételt vagy más kimeneti formátumot, majd ötletelj újra.", + "noIdeasTitle": "Nem születtek ötletek" }, "factoidsForm": { "title": "Bizonyítékok Keresése", @@ -1230,7 +1276,9 @@ "exportedGdocs": "Tények exportálva Google Docs-ba!", "open": "Megnyitás", "exportFailedGdocs": "Nem sikerült exportálni Google Docs-ba", - "exportError": "Váratlan hiba történt az exportálás során" + "exportError": "Váratlan hiba történt az exportálás során", + "noVerified": "Nem találtunk megfelelő, ellenőrzött forrásokat. Próbálj más forrástípust vagy hosszabb időszakot.", + "useSuggestion": "Keresés erre" }, "askChat": { "title": "Kérdezd az Edződ", @@ -1283,7 +1331,12 @@ "noSavedChatsDesc": "A mentett edzői beszélgetések itt jelennek meg.", "messageCount": "{{count}} üzenet", "savedCount": "{{count}} mentve", - "removeFile": "{{name}} eltávolítása" + "removeFile": "{{name}} eltávolítása", + "save": "Mentés", + "messageCount_one": "{{count}} üzenet", + "messageCount_other": "{{count}} üzenet", + "savedCount_one": "{{count}} mentve", + "savedCount_other": "{{count}} mentve" }, "limitModal": { "trialEnded": "Ingyenes Próba Lejárt", @@ -1406,7 +1459,9 @@ "2nr": "2NR", "2ar": "2AR" } - } + }, + "disableSound": "Hang kikapcsolása", + "enableSound": "Hang bekapcsolása" }, "flowchart": { "title": "Érvelési Folyamatábra", @@ -1471,7 +1526,13 @@ "opponentAttackDesc": "Így válaszolhatnak", "rebuttalLabel": "A cáfolatod", "rebuttalDesc": "Így válaszolsz a támadásukra" - } + }, + "autoLayout": "Automatikus elrendezés", + "edgeAttack": "Támadás", + "edgeRebuttal": "Cáfolat", + "edgeSupport": "Alátámasztás", + "exportFailedToast": "Nem sikerült exportálni a folyamatábrát.", + "selectedNode": "Kiválasztva" }, "historyPage": { "title": "Előzmények", @@ -1514,7 +1575,11 @@ "deleteCaseLabel": "{{title}} törlése", "load": "Betöltés", "resume": "Folytatás", - "messages": "üzenet" + "messages": "üzenet", + "caseDeleted": "A mentett eset törölve.", + "deleteCaseDesc": "Ezzel eltávolítod a mentett esetet az előzményeidből.", + "deleteCaseTitle": "Törlöd a mentett esetet?", + "deleteFailed": "Nem sikerült törölni a mentett esetet." }, "libraryPage": { "title": "Forráskönyvtár", @@ -1590,7 +1655,37 @@ "syncedFolders": "Szinkronizált mappák", "manualSource": "Kézi forrás", "searchSources": "Források keresése", - "searchPlaceholder": "Keresés forrásokban, mappákban vagy tartalomban..." + "searchPlaceholder": "Keresés forrásokban, mappákban vagy tartalomban...", + "activateFolder": "Aktiválás", + "clearSuccess": "A könyvtár tartalma törölve.", + "copyFailed": "Nem sikerült másolni a forrást.", + "deactivateFolder": "Deaktiválás", + "deleteFolderDesc": "A mappában lévő források átkerülnek az Általános mappába. Maguk a források nem törlődnek.", + "deleteFolderTitle": "Törlöd a mappát?", + "deleteSourceDesc": "Ezzel eltávolítod a forrást a szinkronizált könyvtáradból.", + "deleteSourceTitle": "Törlöd a forrást?", + "folderActions": "Mappaműveletek", + "folderActionsDesc": "Nevezd át vagy töröld a mappát, illetve állítsd be, hogy a benne lévő forrásokat felhasználja-e az MI a válaszai megalapozásához.", + "folderActivated": "Mappa aktiválva", + "folderActivatedDesc": "A mappa összes forrása aktív.", + "folderDeactivated": "Mappa deaktiválva", + "folderDeactivatedDesc": "A források mentve maradnak, de az MI nem használja őket a válaszai megalapozásához.", + "folderDeleted": "Mappa törölve", + "folderDeletedDesc": "A források átkerültek az Általános mappába.", + "folderMergeConfirm": "Már van ilyen nevű mappa. Áthelyezed bele a forrásokat?", + "folderRenamed": "Mappa átnevezve", + "folderRenamedDesc": "A mappa forrásai az új nevű mappába kerültek.", + "noMatchingSources": "Nincs egyező forrás", + "noMatchingSourcesDesc": "Próbálj más keresést vagy mappát.", + "renameFolder": "Mappa átnevezése", + "sourceCopied": "A forrás a vágólapra másolva.", + "sourceDeleted": "Forrás eltávolítva.", + "sourceUpdated": "Forrás frissítve", + "sourceUpdatedDesc": "A forrás módosításai mentve.", + "typeLabel": "Típus", + "untitledSource": "Névtelen forrás", + "useForGrounding": "Felhasználás az MI válaszainak megalapozásához", + "useForGroundingDesc": "Az inaktív források mentve maradnak, de a generálás nem használja őket." }, "autoSavedSources": { "undo": "Automatikus források visszavonása", @@ -1599,7 +1694,9 @@ "removedPartialTitle": "Néhány forrás eltávolítva", "removedPartialDesc": "{{succeeded}} eltávolítva, {{failed}} eltávolítása sikertelen.", "removeFailed": "Nem sikerült eltávolítani az automatikusan mentett forrásokat.", - "removeFailedDesc": "{{succeeded}} forrás eltávolítva. {{failed}} forrás eltávolítása sikertelen." + "removeFailedDesc": "{{succeeded}} forrás eltávolítva. {{failed}} forrás eltávolítása sikertelen.", + "removedDesc_one": "{{count}} automatikusan mentett forrás eltávolítva a könyvtáradból.", + "removedDesc_other": "{{count}} automatikusan mentett forrás eltávolítva a könyvtáradból." }, "sharedPage": { "title": "Megosztott Tartalom", @@ -1780,7 +1877,11 @@ "sparringKeyMoments": "Kulcspillanatok", "sparringImprovementAreas": "Következő fejlesztési pontok", "sparringScorecardUnavailable": "Még nincs végleges értékelőlap.", - "sparringNoTranscript": "Ebben a fordulóban nem küldtek be beszédet." + "sparringNoTranscript": "Ebben a fordulóban nem küldtek be beszédet.", + "factoidsSummary_one": "{{count}} tény • Ellenőrzött: {{verified}}", + "factoidsSummary_other": "{{count}} tény • Ellenőrzött: {{verified}}", + "topIdeasCount_one": "{{count}} legjobb ötlet", + "topIdeasCount_other": "{{count}} legjobb ötlet" }, "sparring": { "pro": "Mellette", @@ -1856,7 +1957,15 @@ "startRound": "Forduló indítása", "draftNotice": "A piszkozat írás közben ezen a böngészőfülön mentődik. Szüneteltetheted anélkül, hogy elveszítenéd a helyed.", "offlineCannotStart": "Csatlakozz újra a forduló indításához. A beállításaid offline állapotban is megmaradnak.", - "speakingOrder": "Felszólalási sorrend" + "speakingOrder": "Felszólalási sorrend", + "aboutMinutes_one": "Körülbelül {{count}} perc", + "aboutMinutes_other": "Körülbelül {{count}} perc", + "aiRequestBreakdown_one": "{{count}} válasz + 1 bíró", + "aiRequestBreakdown_other": "{{count}} válasz + 1 bíró", + "speeches_one": "{{count}} beszéd", + "speeches_other": "{{count}} beszéd", + "turns_one": "{{count}} kör", + "turns_other": "{{count}} kör" }, "round": { "inProgress": "Forduló folyamatban", @@ -1911,7 +2020,13 @@ "timeUsed": "{{time}} felhasználva", "estimatedTimeUsed": "Becsült idő: {{time}}", "youArePro": "Te vagy mellette", - "youAreCon": "Te vagy ellene" + "youAreCon": "Te vagy ellene", + "submittedCount_one": "{{count}} beküldve", + "submittedCount_other": "{{count}} beküldve", + "totalWords_one": "{{count}} szó", + "totalWords_other": "{{count}} szó", + "wordCount_one": "{{count}} szó", + "wordCount_other": "{{count}} szó" }, "scorecard": { "eyebrow": "Forduló értékelése", @@ -1947,7 +2062,9 @@ "fullTranscript": "Teljes átirat", "transcriptDescription": "{{count}} beszéd ebből a(z) {{format}} fordulóból.", "hideTranscript": "Átirat elrejtése", - "showTranscript": "Átirat megjelenítése" + "showTranscript": "Átirat megjelenítése", + "transcriptDescription_one": "{{count}} beszéd ebből a(z) {{format}} fordulóból.", + "transcriptDescription_other": "{{count}} beszéd ebből a(z) {{format}} fordulóból." } }, "langSwitcher": { @@ -2096,7 +2213,9 @@ "strong": "Erős", "moderate": "Közepes", "weak": "Gyenge" - } + }, + "usageExplanation_one": "Felhasználva {{count}} mentett esetben. Fontosság: {{importance}}. Bizonyíték ereje: {{strength}}.", + "usageExplanation_other": "Felhasználva {{count}} mentett esetben. Fontosság: {{importance}}. Bizonyíték ereje: {{strength}}." }, "debate": { "motion": "Tétel", @@ -2195,7 +2314,9 @@ }, "offerTitle": "Indítasz egy vitát ezzel a tétellel?", "offerDesc": "A vita együtt tartja a tételt, a forrásait és az eredményeidet, és minden eszköz ezt használja.", - "offerNotNow": "Most nem, csak egyszer használom" + "offerNotNow": "Most nem, csak egyszer használom", + "itemCount_one": "{{count}} elem", + "itemCount_other": "{{count}} elem" }, "debateContext": { "using": "Felhasználva: {{list}}", @@ -2244,6 +2365,19 @@ "lastUnavailable_other": "{{count}} elem már nem volt ebben a vitában." }, "themeToggle": { - "toggle": "Téma váltása" + "toggle": "Téma váltása", + "current": "Jelenlegi téma: {{theme}}. Kattints a váltáshoz." + }, + "googleDriveAuth": { + "connect": "Google Drive csatlakoztatása", + "description": "A Google Docsba történő exportáláshoz a DebateLabnak engedélyre van szüksége fájlok létrehozásához a Google Drive-odban.", + "failedDesc": "Próbáld újra.", + "failedTitle": "Nem sikerült megnyitni a Google-t", + "redirecting": "Átirányítás...", + "scopeNote": "Csak a korlátozott drive.file engedélyt kérjük, nem teljes hozzáférést a Drive-hoz.", + "title": "Google Drive csatlakoztatása" + }, + "offlineIndicator": { + "label": "Offline mód" } } diff --git a/src/pages/AskPage.tsx b/src/pages/AskPage.tsx index 724cac98a..4b05c6815 100644 --- a/src/pages/AskPage.tsx +++ b/src/pages/AskPage.tsx @@ -321,7 +321,7 @@ export default function AskPage() { title: t('ask.saveFailed', 'Save failed'), description: error instanceof DebateRequiredError ? t('debate.sourceNeedsDebate', 'Start or select a debate before saving sources.') - : t('ask.saveFailedDesc', 'The source could not be saved. Please try again.'), + : t('ask.sourceSaveFailedDesc', 'The source could not be saved. Please try again.'), variant: 'destructive', }); if (saveResult && typeof saveResult.then === 'function') { diff --git a/src/pages/GeneratePage.tsx b/src/pages/GeneratePage.tsx index f9a817957..649cd8417 100644 --- a/src/pages/GeneratePage.tsx +++ b/src/pages/GeneratePage.tsx @@ -183,7 +183,7 @@ export default function GeneratePage() { console.error('Failed to save case:', error); toast({ title: t('ask.saveFailed', 'Save failed'), - description: t('ask.saveFailedDesc', 'The case could not be saved. Please try again.'), + description: t('generate.saveFailedDesc', 'The case could not be saved. Please try again.'), variant: 'destructive', }); } diff --git a/src/test/i18n-keys.test.ts b/src/test/i18n-keys.test.ts new file mode 100644 index 000000000..cea69bd4a --- /dev/null +++ b/src/test/i18n-keys.test.ts @@ -0,0 +1,52 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import ts from 'typescript'; +import { expect, test } from 'vitest'; +import en from '../locales/en.json'; + +const src = join(process.cwd(), 'src'); + +function files(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + if (entry.name === 'test') return []; + const path = join(dir, entry.name); + return entry.isDirectory() ? files(path) : /\.[jt]sx?$/.test(entry.name) ? [path] : []; + }); +} + +function hasTranslation(key: string): boolean { + let value: unknown = en; + for (const part of key.split('.')) { + if (typeof value !== 'object' || value === null || !(part in value)) return false; + value = Reflect.get(value, part); + } + return typeof value === 'string'; +} + +test('literal translation keys exist in the English catalog', () => { + const missing = new Set(); + + for (const path of files(src)) { + const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true); + function visit(node: ts.Node): void { + if (ts.isCallExpression(node) && node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0]) && + (node.expression.getText(source) === 't' || node.expression.getText(source) === 'i18n.t')) { + const key = node.arguments[0].text; + const hasCount = node.arguments.slice(1).some((arg) => ts.isObjectLiteralExpression(arg) && + arg.properties.some((prop) => 'name' in prop && prop.name && + (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) && prop.name.text === 'count')); + if (hasCount) { + for (const suffix of ['_one', '_other']) { + if (!hasTranslation(key + suffix)) missing.add(key + suffix); + } + } else if (!hasTranslation(key) && !(hasTranslation(key + '_one') && hasTranslation(key + '_other'))) { + missing.add(key); + } + } + ts.forEachChild(node, visit); + } + visit(source); + } + + expect([...missing].sort()).toEqual([]); +}); diff --git a/src/test/i18n.test.ts b/src/test/i18n.test.ts new file mode 100644 index 000000000..031e47a58 --- /dev/null +++ b/src/test/i18n.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from 'vitest'; +import en from '../locales/en.json'; +import es from '../locales/es.json'; +import hu from '../locales/hu.json'; +import i18n from '../i18n'; + +function keys(catalog: object, prefix = ''): string[] { + return Object.entries(catalog).flatMap(([key, value]) => { + const path = prefix + key; + return typeof value === 'string' ? [path] : value && typeof value === 'object' ? keys(value, path + '.') : []; + }); +} + +test('Spanish and Hungarian catalogs match English keys', () => { + const english = keys(en).sort(); + expect(keys(es).sort()).toEqual(english); + expect(keys(hu).sort()).toEqual(english); +}); + +test('Hungarian count keys resolve through i18next', async () => { + await i18n.changeLanguage('hu'); + try { + expect(i18n.t('askChat.messageCount', { count: 1 })).toBe('1 üzenet'); + expect(i18n.t('askChat.messageCount', { count: 2 })).toBe('2 üzenet'); + } finally { + await i18n.changeLanguage('en'); + } +}); From 6d72dcc0441414e88cc40621ecf592251cc19ff1 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 6 Oct 2026 07:57:28 +0000 Subject: [PATCH 2/2] test(i18n): keep only the catalog parity check; describe both tests The Hungarian plural assertion re-tested i18next; the key scan already requires _one and _other forms. docs/testing.md still described the i18n.test.ts removed in b9ef0b3. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/testing.md | 2 +- src/test/i18n.test.ts | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index e0963fc24..b85c23430 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -123,7 +123,7 @@ The server is never reused. CI uses retries and two workers. On failure, traces, `e2e/first-run.spec.ts` drives the same first run against stubbed Supabase routes in English and Hungarian, so it proves the client flow only. -`src/test/i18n.test.ts` requires identical key sets and non-empty values across all three catalogs. Its explicit identity allowlist is limited to values that should remain canonical, such as product names, file formats, prices, model-facing abbreviations, and debate-format codes. A matching key set and a passing allowlist test prove catalog structure, not Hungarian grammar or natural phrasing. +`src/test/i18n-keys.test.ts` fails when a literal `t()` key in `src/` is missing from `en.json`, or when a call that passes `count` lacks its `_one` and `_other` forms. Dynamic keys such as ``t(`nav.${key}`)`` are out of its reach. `src/test/i18n.test.ts` requires the same keys in all three catalogs. Together they prove every literal key has a Spanish and Hungarian entry, not that the translation is natural. The speech unit test proves that Hungarian selects `recognition.lang = 'hu-HU'`. Credential-free browser automation has no real microphone or speech service, so it does not prove recognition accuracy or hardware support. diff --git a/src/test/i18n.test.ts b/src/test/i18n.test.ts index 031e47a58..78b43bd46 100644 --- a/src/test/i18n.test.ts +++ b/src/test/i18n.test.ts @@ -2,7 +2,6 @@ import { expect, test } from 'vitest'; import en from '../locales/en.json'; import es from '../locales/es.json'; import hu from '../locales/hu.json'; -import i18n from '../i18n'; function keys(catalog: object, prefix = ''): string[] { return Object.entries(catalog).flatMap(([key, value]) => { @@ -16,13 +15,3 @@ test('Spanish and Hungarian catalogs match English keys', () => { expect(keys(es).sort()).toEqual(english); expect(keys(hu).sort()).toEqual(english); }); - -test('Hungarian count keys resolve through i18next', async () => { - await i18n.changeLanguage('hu'); - try { - expect(i18n.t('askChat.messageCount', { count: 1 })).toBe('1 üzenet'); - expect(i18n.t('askChat.messageCount', { count: 2 })).toBe('2 üzenet'); - } finally { - await i18n.changeLanguage('en'); - } -});